ステージ選択マップの道筋を自動配置する

[PONOS Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ponos)の15日目の記事です。

## はじめに
スマホ向けのゲームを作っています。
今回はステージセレクト画面を担当したときの話ですが、意外と一筋縄ではいかなかったので当時のことを思い出しながら書きます。

## 目的

ステージ選択画面のマップ上にステージ選択ボタンが並ぶのですが、それらの繋がりがわかるよう道筋として白い点を並べます。※以後、黄色い丸を「ステージボタン」、間に並ぶ白い点を「ドット」と呼びます。

理想とするのは以下の図のような形ですが、ステージクリア型のパズルゲームなどでよく見かける絵面ですね。
![スクリーンショット 2021-11-21 20.44.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/1da78f88-992e-d0c9-23cd-3fb889637927.png)

背景となる緑のフィールドには崖や川や木などがあり、そこを避けるように配置します。背景制作に合わせてこの白いドットまでデザイナーさんにポチポチと配置してもらっていたのですが、流石に気の毒と思い自動配置することにしました。

## 要件
1. Map上にステージボタンを配置するだけでドットが自動で配置される
2. Map上のドットは等間隔に並ぶ（違和感なく見えれば良い）
3. ステージボタンの直径がドットの4倍程度ある
4. ドットの並びは直線的で良い

## 試作

#### ダメだったパターンその1
まずはUnityらしく、ステージボタン間にLineレンダラーでポリゴンの帯を生成し、テクスチャリピートでドットの並びを表現することを考えましたが、直感的にうまくいかなそうと感じたので試作するまでもなく取り下げました。試作段階で表現の自由度を狭めてしまうことも懸念しました。

#### ダメだったパターンその2
とりあえずドットを動的生成して配置してみたものです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/abf93753-386b-1a1d-b902-187be0c44bd9.png" width=50%>
ドットとドットの間隔が均一でない。ダメ！

ただ、それ以外の要件が満たせていることをこの時点で確認できました。この方向で進めます。

一応この時点のスクリプトになります。ボツですけど。
```csharp:sample1.cs
        // 必ず3点の場合
        const int DotsNumber = 7;
        public void ConstructTrail_3point (Transform start, Transform end)
        {
            for (int i = 0; i < DotsNumber; i++) {
                // 両端の2個は生成しない
                if ((i < 2) || (i > DotsNumber - 3)) {
                    continue;
                }
                //　ドットの生成
                GameObject go = Instantiate<GameObject> (dotPrefab, Vector3.zero, Quaternion.identity);
                go.name = dotPrefab.name;
                go.transform.SetParent (this.transform);
                go.transform.localPosition = start.localPosition - (start.localPosition - end.localPosition) / (DotsNumber - 1) * i;
            }
        }        
```
始点と終点を含む7つのポイントを計算で求め、その両端の2点（計4点）を無視するという考えでした。


#### ダメだったパターンその3
等間隔に並べるという部分に焦点を絞って作り直しました。
過程1：等間隔に配置
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/e011fbd3-b3f2-415d-5aeb-dc5c350a1acd.png" width=50%>
過程2：ステージボタンに重なるドットを消す
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/c57bdb50-7d59-be55-6d1f-ca3007549162.png" width=50%>
一見良さげに見えるけどステージボタンとドットとの距離が一定ではない。まだダメ！

でも惜しいぞ。あと一歩！

#### OKパターン
ではこれでどうか？
![スクリーンショット 2021-11-21 20.51.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/ee0ad69a-6893-419a-63d5-8abf7a4d84f7.png)
おっ、イケてるんじゃない？

予めステージボタンサイズ分のマージンをとっておき、残った線分上で等間隔かつ中央寄せにドットを配置してみました。

スクリプトの一部ですが、こんな感じでベクトル計算しています。
```csharp:sample2.cs  
        // 等間隔にドットを配置する
        public void ConstructTrail (Transform start, Transform end)
        {
            float length = map_Setting.dotsLength;
            float margin = map_Setting.dotsMargin;

            float distance = Vector3.Distance (start.localPosition, end.localPosition);
            int count = (int)((distance - margin * 2f) / length) + 1;
            Vector3 vec = (start.localPosition - end.localPosition).normalized;
            float difference = distance - length * (count - 1) - margin * 2f;
            for (int i = 0; i < count; i++) {
                //　ドットの生成
                GameObject go = Instantiate<GameObject> (dotPrefab, Vector3.zero, Quaternion.identity);
                go.name = dotPrefab.name;
                go.transform.SetParent (this.transform);
                go.transform.localPosition = start.localPosition - vec * margin - vec * length * (float)i - vec * difference * 0.5f;
            }
        }
```


更にテストしてみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/fc6647a5-aa86-9533-0514-81a93cfd296d.png" width=50%>
ステージボタンの間隔が広がっても狭まってもきっちり対応できていることが確認できました

これでよし！

## まとめ
以上、ゲーム制作の中でちょっぴり苦労したことを思い出しつつ記事にしてみました。
結果を知れば簡単なことかもしれませんが、こうして試行錯誤する過程も大事にしていきたいです。

次回は@MilayYadokariさんです！