Unityの六角形マップ:パスファインダー、プレイヤースクワッド、アニメーション

パート1〜3:グリッド、色、セルの高さ



パート4〜7:粗さ、川、および道路



パート8-11:水、地形、城壁



パート12〜15:保存と読み込み、テクスチャ、距離



パート16〜19:道を見つける、プレイヤーチーム、アニメーション



パート20-23:戦争の霧、地図調査、手続き生成



パート24〜27:水循環、侵食、バイオーム、円柱地図



パート16:道を見つける





セル間の距離を計算したら、それらの間のパスを見つけることに進みました。



このパートから始めて、六角形のマップチュートリアルがUnity 5.6.0で作成されます。 5.6には、いくつかのプラットフォームのアセンブリでテクスチャの配列を破壊するバグがあることに注意してください。 テクスチャ配列インスペクターにIs Readableを含めることで回避できます。









旅行の計画



ハイライトされたセル



2つのセル間のパスを検索するには、まずこれらのセルを選択する必要があります。 1つのセルを選択して地図上の検索を監視するだけではありません。 たとえば、最初のセルを選択してから、最後のセルを選択します。 この場合、強調表示されると便利です。 したがって、そのような機能を追加しましょう。 強調表示の複雑または効果的な方法を作成するまで、開発に役立つものを作成するだけです。



アウトラインテクスチャ



セルを選択する簡単な方法の1つは、セルにパスを追加することです。 これを行う最も簡単な方法は、六角形の輪郭を含むテクスチャを使用することです。 ここでは、そのようなテクスチャをダウンロードできます。 六角形の白い輪郭を除いて透明です。 白色にしたので、将来は必要に応じて色付けできるようになります。









黒の背景にセルの概要



テクスチャをインポートし、そのTexture TypeSpriteに設定します。 そのスプライトモードは、デフォルト設定でシングルに設定されます。 これは非常に白いテクスチャなので、 sRGBに変換する必要はありません。 アルファチャネルは透明度を表しているため、 Alpha is Transparencyを有効にします。 また、 フィルターモードテクスチャをTrilinearに設定します。そうしないと、パスのミップトランジションが目立ちやすくなる可能性があります。









テクスチャインポートオプション



セルごとに1つのスプライト



最速の方法は、可能な輪郭をセルに追加し、それぞれ独自のスプライトを追加することです。 新しいゲームオブジェクトを作成し、Imageコンポーネント( Component / UI / Image )を追加して、アウトラインスプライトを割り当てます。 次に、 Hex Cell Labelプレハブインスタンスをシーンに挿入し、スプライトオブジェクトをその子にし、変更をプレハブに適用してから、プレハブを取り除きます。















プレハブの子選択要素



各セルにはスプライトがありますが、大きすぎます。 輪郭をセルの中心に一致させるには、スプライトの変換コンポーネントの高さを17に変更します。









レリーフによって部分的に隠された選択スプライト



すべての上に描く



輪郭はセルの端の領域に重ねられるため、レリーフのジオメトリの下に表示されることがよくあります。 このため、回路の一部が消えます。 これはスプライトを垂直方向にわずかに上げることで回避できますが、ブレークの場合はできません。 代わりに、次のことができます。常に他のすべての上にスプライトを描画します。 これを行うには、独自のスプライトシェーダーを作成します。 標準のUnityスプライトシェーダーをコピーして、それにいくつかの変更を加えるだけで十分です。



Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex SpriteVert #pragma fragment SpriteFrag #pragma target 2.0 #pragma multi_compile_instancing #pragma multi_compile _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnitySprites.cginc" ENDCG } } }
      
      





最初の変更は、深度バッファを無視し、Zテストが常に成功するようにすることです。



  ZWrite Off ZTest Always
      
      





2番目の変更点は、残りのすべての透明ジオメトリの後にレンダリングすることです。 透過性キューに10を追加するのに十分です。



  "Queue"="Transparent+10"
      
      





このシェーダーが使用する新しいマテリアルを作成します。 デフォルト値に従って、そのすべてのプロパティを無視できます。 次に、スプ​​ライトプレハブにこのマテリアルを使用させます。















独自のスプライトマテリアルを使用します



これで、選択範囲の輪郭が常に表示されます。 セルがより高い浮き彫りの下に隠れていても、その輪郭は他のすべての上に描画されます。 見た目は美しくないかもしれませんが、選択したセルは常に表示されているので便利です。









深度バッファを無視する



選択制御



すべてのセルを同時に強調表示することは望ましくありません。 実際、最初はすべて選択を解除する必要があります。 HighlightプレハブオブジェクトのImageコンポーネントを無効にすることでこれを実装できます。









無効な画像コンポーネント



セルの選択を有効にするには、 HexCell



メソッドをEnableHighlight



追加しEnableHighlight



uiRect



唯一の子を取り、Imageコンポーネントを含める必要があります。 DisableHighlight



メソッドも作成します。



  public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; }
      
      





最後に、オンにしたときにバックライトに色相を与えるように色を指定できます。



  public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; }
      
      





ユニティパッケージ



道を見つける



セルを選択できるようになったので、次に進んで2つのセルを選択し、それらの間のパスを見つける必要があります。 最初にセルを選択し、次に検索をセル間のパスに制限し、最後にこのパスを表示する必要があります。



検索開始



検索の開始点と終了点の2つの異なるセルを選択する必要があります。 最初の検索セルを選択するには、左のShiftキーを押しながらマウスをクリックするとします。 この場合、セルは青で強調表示されます。 さらに検索するには、このセルへのリンクを保存する必要があります。 さらに、新しい開始セルを選択する場合、古いセルの選択を無効にする必要があります。 したがって、 HexMapEditor



フィールドをsearchFromCell



追加しsearchFromCell







  HexCell previousCell, searchFromCell;
      
      





HandleInput



HandleInput



HandleInput



Input.GetKey(KeyCode.LeftShift)



を使用して、Shiftキーを押しながらテストできます。



  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); }
      
      











見どころ



検索エンドポイント



セルまでのすべての距離を探す代わりに、2つの特定のセル間のパスを探しています。 したがって、 HexGrid.FindDistancesTo



名前をHexGrid.FindDistancesTo



に変更し、2番目のHexCell



パラメーターを指定し、 Search



メソッドを変更します。



  public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … }
      
      





これで、 HexMapEditor.HandleInput



は、 searchFromCell



およびsearchFromCell



を引数として使用して、変更されたメソッドを呼び出す必要があります。 また、どのセルから検索するかがわかっている場合にのみ検索できます。 また、開始点と終了点が一致する場合、わざわざ検索する必要はありません。



  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); }
      
      





検索に戻ると、最初に以前の選択をすべて取り除く必要があります。 したがって、距離をリセットするときにHexGrid.Search



が選択をオフにします。 これにより初期セルの照明もオフになるため、再度オンにします。 この段階で、エンドポイントを強調表示することもできます。 彼女を赤にしましょう。



  IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … }
      
      











潜在的なパスのエンドポイント



検索を制限する



この時点で、検索アルゴリズムは、開始セルから到達可能なすべてのセルまでの距離を計算します。 しかし、もう必要ありません。 最終セルまでの最終距離が見つかったらすぐに停止できます。 つまり、現在のセルが有限の場合、アルゴリズムループを終了できます。



  while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } }
      
      











終点で停止



エンドポイントに到達できない場合はどうなりますか?
その後、アルゴリズムは、到達可能なすべてのセルが見つかるまで機能し続けます。 早期終了の可能性がなければ、古いFindDistancesTo



メソッドとしてFindDistancesTo



ます。


パス表示



パスの始点と終点の間の距離を見つけることはできますが、実際のパスがどうなるかはまだわかりません。 それを見つけるには、各セルに到達する方法を追跡する必要があります。 しかし、それを行う方法は?



セルを境界線に追加するとき、現在のセルの隣にあるため、これを行います。 1つの例外は開始セルです。 他のすべてのセルは、現在のセルを介して到達しています。 各セルに到達したセルを追跡すると、結果としてセルのネットワークが取得されます。 より正確には、ルートが開始点であるツリーのようなネットワーク。 エンドポイントに到達した後、これを使用してパスを構築できます。









センターへのパスを記述するツリーネットワーク



HexCell



別のセルへのリンクを追加することにより、この情報を保存できます。 このデータをシリアル化する必要はないため、これには標準プロパティを使用します。



  public HexCell PathFrom { get; set; }
      
      





HexGrid.Search



、境界線に追加するときに、隣接セルのPathFrom



値を現在のセルに設定します。 さらに、近隣へのより短い方法を見つけた場合、このリンクを変更する必要があります。



  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; }
      
      





終点に到達したら、これらのリンクをたどって開始セルに戻り、選択することでパスを視覚化できます。



  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
      
      











パスが見つかりました



多くの場合、いくつかの最短経路が存在することを考慮する価値があります。 見つかったものは、セルの処理順序によって異なります。 一部のパスは良く見えるかもしれませんが、他のパスは悪いかもしれませんが、決して短いパスはありません。 これについては後で説明します。



検索の開始を変更



開始点を選択した後、終了点を変更すると、新しい検索がトリガーされます。 新しい開始セルを選択するときにも同じことが起こるはずです。 これを可能にするには、 HexMapEditor



もエンドポイントを記憶する必要があります。



  HexCell previousCell, searchFromCell, searchToCell;
      
      





このフィールドを使用して、新しい始まりを選択するときに新しい検索を開始することもできます。



  else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); }
      
      





さらに、開始点と終了点が等しくならないようにする必要があります。



  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … }
      
      





ユニティパッケージ



よりスマートな検索



アルゴリズムは最短経路を見つけますが、明らかにこの経路の一部にならない点を探すのに多くの時間を費やします。 少なくともそれは明らかです。 アルゴリズムは地図を見下ろすことはできません;ある方向の検索が無意味になることを見ることができません。 彼は終点から反対方向に向かっているという事実にもかかわらず、道路を移動することを好みます。 検索をよりスマートにすることは可能ですか?



現時点では、次に処理する必要があるセルを選択するとき、セルから先頭までの距離のみを考慮します。 もっと賢くしたい場合は、終点までの距離も考慮する必要があります。 残念ながら、私たちは彼をまだ知りません。 ただし、残りの距離の推定値を作成できます。 この推定値をセルまでの距離に追加すると、このセルを通過するパスの全長がわかります。 次に、それを使用してセル検索の優先順位を設定できます。



検索ヒューリスティック



正確に既知のデータの代わりに推定または推測を使用する場合、これは検索ヒューリスティックを使用して呼び出されます。 このヒューリスティックは、残りの距離の最良の推測を表します。 検索する各セルに対してこの値を決定する必要があるため、 HexCell



整数プロパティを追加します。 シリアル化する必要はないので、別の標準プロパティで十分です。



  public int SearchHeuristic { get; set; }
      
      





残りの距離についてどのように仮定しますか? 最も理想的なケースでは、終点までまっすぐに続く道路があります。 その場合、距離は、このセルと最終セルの座標間の変更されていない距離に等しくなります。 ヒューリスティックでこれを活用しましょう。



ヒューリスティックは以前に移動したパスに依存しないため、検索プロセスでは一定です。 したがって、 HexGrid.Search



が境界にセルを追加するときに一度だけ計算する必要があります。



  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); }
      
      





検索の優先度



これから、セルまでの距離とそのヒューリスティックに基づいて、検索の優先順位を決定します。 この値のプロパティをHexCell



追加しましょう。



  public int SearchPriority { get { return distance + SearchHeuristic; } }
      
      





これが機能HexGrid.Search



には、このプロパティを使用して境界線を並べ替えるHexGrid.Search



します。



  frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) );
      
      

















ヒューリスティックなしで、かつヒューリスティックで検索



有効なヒューリスティック



新しい検索優先順位のおかげで、結果として実際にアクセスするセルが少なくなります。 ただし、均一なマップでは、アルゴリズムは間違った方向のセルを処理します。 これは、デフォルトでは、各移動ステップのコストが5であり、ステップごとのヒューリスティックが1だけ加算するためです。つまり、ヒューリスティックの影響はあまり強くありません。



すべてのカードを移動するコストが同じ場合、ヒューリスティックを決定するときに同じコストを使用できます。 この場合、これは現在のヒューリスティックに5を掛けたものになります。これにより、処理されるセルの数が大幅に削減されます。









ヒューリスティックの使用×5



ただし、地図上に道路がある場合、残りの距離を過大評価することができます。 その結果、アルゴリズムは間違いを犯し、実際には最短ではないパスを作成する可能性があります。















過大評価された有効なヒューリスティック



最短経路が確実に見つかるようにするには、残りの距離を過大評価しないようにする必要があります。 このアプローチは有効なヒューリスティックと呼ばれます。 移動の最小コストは1なので、ヒューリスティックの決定に同じコストを使用する以外に選択肢はありません。



厳密に言えば、さらに低いコストを使用することは非常に普通ですが、これはヒューリスティックをより弱くするだけです。 可能な最小のヒューリスティックはゼロです。これにより、ダイクストラのアルゴリズムのみが得られます。 ゼロ以外のヒューリスティックでは、アルゴリズムはA * (「Aスター」と発音)と呼ばれます。



なぜA *と呼ばれるのですか?
Dijkstraのアルゴリズムにヒューリスティックを追加するというアイデアは、最初にNiels Nilssonによって提案されました。 彼は自分のバージョンをA1と名付けました。 バートラム・ラファエルは後に彼がA2と呼んだ最高のバージョンを思いついた。 その後、Peter Hartは、優れたヒューリスティックを使用して、A2が最適であること、つまり、より良いバージョンは存在できないことを証明しました。 これにより、彼はアルゴリズムA *を呼び出して、改善できないこと、つまりA3またはA4が表示されないことを示しました。 そのため、A *アルゴリズムを使用するのが最善ですが、ヒューリスティックと同じくらい優れています。


ユニティパッケージ



優先キュー



A *は良いアルゴリズムですが、境界を格納するためにリストを使用するため、実装はそれほど効果的ではありません。これは各反復でソートする必要があります。 前の部分で述べたように、優先度キューが必要ですが、その標準実装は存在しません。 したがって、自分で作成しましょう。



順番は、優先度に基づいてキューから設定および除外する操作をサポートする必要があります。 また、すでにキューにあるセルの優先度の変更をサポートする必要があります。 理想的には、これを実装して、ソートおよび割り当てられたメモリの検索を最小限に抑えます。 さらに、シンプルなままにする必要があります。



独自のキューを作成する



必要な共通メソッドで新しいHexCellPriorityQueue



クラスを作成します。 単純なリストを使用して、キューの内容を追跡します。 さらに、キューをクリアして再利用できるように、 Clear



メソッドを追加します。



 using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } }
      
      





セル自体にセルの優先順位を保存します。 つまり、キューにセルを追加する前に、その優先度を設定する必要があります。 しかし、優先順位が変更された場合、古い優先順位が何であったかを知ることはおそらく役立つでしょう。 これをパラメーターとしてChange



追加しましょう。



  public void Change (HexCell cell, int oldPriority) { }
      
      





また、キューにあるセルの数を知ることも役立つので、このためにCount



プロパティを追加しましょう。 対応するインクリメントとデクリメントを実行するフィールドを使用してください。



  int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; }
      
      





キューに追加



セルがキューに追加されたら、まずその優先度をインデックスとして使用して、リストを単純な配列として扱いましょう。



  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; }
      
      





ただし、これはリストが十分に長い場合にのみ機能します。それ以外の場合は境界を越えます。 必要な長さに達するまで空のアイテムをリストに追加することにより、これを回避できます。 これらの空の要素はセルを参照しないため、リストにnull



を追加して作成できます。



  int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell;
      
      











穴のあるリスト



しかし、これは優先度ごとに1つのセルのみを格納する方法であり、多くの場合、複数のセルが格納されます。 同じ優先度のすべてのセルを追跡するには、別のリストを使用する必要があります。 優先度ごとに実際のリストを使用できますが、 HexCell



にプロパティを追加してそれらを結合することもできます。 これにより、リンクリストと呼ばれる一連のセルを作成できます。



  public HexCell NextWithSamePriority { get; set; }
      
      





チェーンを作成するには、 HexCellPriorityQueue.Enqueue



を使用して、新しく追加されたセルに、削除する前に同じ優先度で現在の値を強制的に参照させます。



  cell.NextWithSamePriority = list[priority]; list[priority] = cell;
      
      











リンクリストのリスト



キューから削除



優先度キューからセルを取得するには、空でない最小のインデックスでリンクリストにアクセスする必要があります。 したがって、リストが見つかるまでループ内を巡回します。 見つからない場合、キューは空であり、 null



を返しnull







見つかったチェーンから、すべてのセルの優先順位が同じなので、任意のセルを返すことができます。 最も簡単な方法は、チェーンの先頭からセルを返すことです。



  public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; }
      
      





残りのチェーンへのリンクを維持するには、新しい開始と同じ優先度を持つ次のセルを使用します。 この優先度レベルにセルが1つしかなかった場合、要素はnull



なり、将来スキップされます。



  if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; }
      
      





最小追跡



このアプローチは機能しますが、セルが受信されるたびにリストを反復処理します。 最小の空でないインデックスを見つけることは避けられませんが、毎回ゼロから始める必要はありません。 代わりに、最低優先順位を追跡し、それで検索を開始できます。 最初は、最小値は本質的に無限大です。



  int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; }
      
      





セルをキューに追加するとき、必要に応じて最小値を変更します。



  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … }
      
      





そして、キューから撤回するとき、ゼロから始めるのではなく、少なくとも反復のためにリストを使用します。



  public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; }
      
      





これにより、優先順位リストループでのバイパスにかかる時間が大幅に短縮されます。



優先順位の変更



セルの優先度を変更するときは、セルが属するリンクリストから削除する必要があります。 これを行うには、見つかるまでチェーンをたどる必要があります。



古い優先順位リストの先頭が現在のセルになることを宣言することから始めましょう。また、次のセルも追跡します。 このインデックスによって少なくとも1つのセルがあることがわかっているため、すぐに次のセルを取得できます。



  public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; }
      
      





現在のセルが変更されたセルである場合、これは先頭のセルであり、キューから引き出したように切り取ることができます。



  HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; }
      
      





そうでない場合は、変更されたセルの前のセルに入るまでチェーンをたどる必要があります。 変更されたセルへのリンクが含まれています。



  if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } }
      
      





この時点で、リンクされたリストから変更されたセルを削除して、スキップできます。



  while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority;
      
      





セルを削除した後、新しい優先度のリストに表示されるようにセルを再度追加する必要があります。



  public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); }
      
      





Enqueue



メソッドはカウンターをインクリメントしますが、実際には新しいセルを追加するわけではありません。 したがって、これを補うために、カウンターを減らす必要があります。



  Enqueue(cell); count -= 1;
      
      





キューの使用



これで、 HexGrid



優先キューを利用できます。 これは、すべての検索操作で再利用可能な単一のインスタンスで実行できます。



  HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … }
      
      





ループを開始する前に、メソッドSearch



を最初にキューfromCell



追加する必要があり、各反復はキューからのセルの出力から始まります。これにより、古い境界コードが置き換えられます。



  WaitForSeconds delay = new WaitForSeconds(1 / 60f); // List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; // frontier.Add(fromCell); searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { yield return delay; HexCell current = searchFrontier.Dequeue(); // frontier.RemoveAt(0); … }
      
      





コードを変更して、近隣を追加および変更します。変更の前に、古い優先順位を覚えています。



  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); // frontier.Add(neighbor); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); }
      
      





さらに、境界線を並べ替える必要がなくなりました。



 // frontier.Sort( // (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) // );
      
      







優先キューを使用した検索



前述のように、検出される最短パスはセルの処理順序によって異なります。私たちの順番は、ソートされたリストの順序とは異なる順序を作成するので、他の方法を得ることができます。各優先度のリンクリストの先頭に追加および削除するため、キューよりもスタックに似ています。最後に追加されたセルが最初に処理されます。このアプローチの副作用は、アルゴリズムがジグザグになりやすいことです。したがって、ジグザグパスの可能性も高くなります。幸いなことに、これらのパスは通常良く見えるので、この副作用は私たちにとって良いことです。















unitypackage 優先度のソートされたリストとキュー







パート17:限られた動き





このパートでは、動きを動きに分割し、可能な限り検索を高速化します。









いくつかの動きからの旅行



増分移動



六角形ネットを使用する戦略ゲームは、ほとんど常にターンベースです。マップ上を移動するユニットの速度は制限されており、1ターンの移動距離が制限されます。



スピード



限られた動き、アドオンへのサポートを提供するHexGrid.FindPath



HexGrid.Search



整数パラメータspeed



1回の動きの可動範囲を決定します。



  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … }
      
      





ゲーム内の異なるタイプのユニットは異なる速度を使用します。騎兵隊は速く、歩兵隊は遅いなどです。まだユニットがありませんので、今のところは一定の速度を使用します。24の値を取りましょう。これはかなり大きい値であり、5(移動のデフォルトコスト)で割り切れません。引数として一定速度FindPath



追加しますHexMapEditor.HandleInput







  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); }
      
      





動き



パスに沿って移動する総コストを追跡することに加えて、パスに沿って移動するのに必要な移動数を知る必要があります。ただし、この情報を各セルに保存する必要はありません。移動距離を速度で除算することで取得できます。これらは整数であるため、整数除算を使用します。つまり、24を超えない合計距離はコース0に対応します。これは、現在のコースでパス全体を完了できることを意味します。終点が30の距離にある場合、これはターン1でなければなりません。終点に到達するには、ユニットは現在のターンと次のターンの一部ですべての動きを費やす必要があります。



現在のセルとその内部のすべての隣接セルのコースを決定しましょうHexGrid.Search



現在のセルのコースは、隣接サイクルを巡回する直前に一度だけ計算できます。隣人の距離は、距離がわかるとすぐに判断できます。



  int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … }
      
      





失われた動き



隣人の動きが現在の動きよりも大きい場合は、動きの境界を越えました。隣人に到達するために必要な動きが1だった場合、すべてが正常です。しかし、次のセルへの移動がより高価な場合、すべてがより複雑になります。



均質なマップに沿って移動するとします。つまり、各セルに入るために5単位の移動が必要だとします。4つのステップの後、移動ストックから20ユニットを費やし、残りの4つが残っています。この段階で何をする必要がありますか?



この状況には2つのアプローチがあります。 1つ目は、十分な動きがない場合でも、ユニットが現在のターンで5番目のセルに入ることを許可することです。 2つ目は、現在の移動中の移動を禁止することです。つまり、残りの移動ポイントは使用できず、失われます。



オプションの選択はゲームによって異なります。一般的な場合、最初のアプローチは、たとえば文明シリーズのゲームのように、ユニットが1ターンあたり数ステップしか移動できないゲームとより整合性があります。これにより、ユニットは常に1ターンあたり少なくとも1つのセルを移動できます。 Age of WondersやBattle for Wesnothのように、ユニットが1ターンあたり多くのセルを移動できる場合、2番目のオプションの方が優れています。



速度24を使用するため、2番目のアプローチを選択しましょう。作業を開始するには、現在の距離に追加する前に、次のセルに入るコストを分離する必要があります。



 // int distance = current.Distance; int moveCost; if (current.HasRoadThroughEdge(d)) { moveCost = 1; } else if (current.Walled != neighbor.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int distance = current.Distance + moveCost; int turn = distance / speed;
      
      





その結果、移動の境界を越える場合、まず現在の移動のすべての移動ポイントを使用します。これを行うには、単純に動きに速度を掛けます。その後、移動のコストを追加します。



  int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; }
      
      





この結果、4つの未使用の移動ポイントで4番目のセルの最初の移動を完了します。これらの失われたポイントは、5番目のセルのコストに追加されるため、その距離は25ではなく29になります。その結果、距離は以前よりも大きくなります。たとえば、10番目のセルの距離は50でした。しかし、ここに入るには、2つの動きの境界を越えて、8つの移動ポイントを失う必要があります。つまり、距離は58になります。









予想より長い



未使用の移動ポイントはセルまでの距離に追加されるため、最短経路を決定する際に考慮されます。最も効果的な方法は、できるだけ少ないポイントを無駄にすることです。したがって、異なる速度で、異なるパスを取得できます。



距離ではなく動きを表示する



ゲームをプレイするとき、最短パスを見つけるために使用される距離値にはあまり関心がありません。エンドポイントに到達するために必要な動きの数に興味があります。したがって、距離の代わりに、動きを表示しましょう。



まず、でUpdateDistanceLabel



彼の呼び出しを取り除きHexCell



ます。



  public int Distance { get { return distance; } set { distance = value; // UpdateDistanceLabel(); } } … // void UpdateDistanceLabel () { // UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); // label.text = distance == int.MaxValue ? "" : distance.ToString(); // }
      
      





代わりに、任意の文字列を受け取るHexCell



一般的なメソッドに追加しSetLabel



ます。



  public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; }
      
      





HexGrid.Search



セルのクリーニングにこの新しい方法を使用しますセルを非表示にするには、単にそれらを割り当てますnull







  for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); }
      
      





次に、隣人のマークに彼の移動の値を割り当てます。その後、最後まで移動するために必要な追加の移動数を確認できます。



  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); }
      
      











unitypackage パスに沿って移動するために必要な移動の数







インスタントパス



また、ゲームをプレイするとき、パス検索アルゴリズムがどのように道を見つけるかは気にしません。要求されたパスをすぐに見たいです。現時点では、アルゴリズムが機能していることを確認できますので、検索の視覚化を削除しましょう。



コルチンなし



アルゴリズムをゆっくりと通過させるために、コルチンを使用しました。これを行う必要はもうないので、呼び出しStartCoroutine



StopAllCoroutines



c を取り除きますHexGrid



代わりに、単純にSearch



通常のメソッドとして呼び出します。



  public void Load (BinaryReader reader, int header) { // StopAllCoroutines(); … } public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // StopAllCoroutines(); // StartCoroutine(Search(fromCell, toCell, speed)); Search(fromCell, toCell, speed); }
      
      





Search



コルーチンとして使用しなくなったため、yieldを必要としないため、この演算子を取り除きます。これは、宣言も削除しWaitForSeconds



、メソッドの戻り値の型をに変更することを意味void



ます。



  void Search (HexCell fromCell, HexCell toCell, int speed) { … // WaitForSeconds delay = new WaitForSeconds(1 / 60f); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { // yield return delay; HexCell current = searchFrontier.Dequeue(); … } }
      
      







インスタント結果



検索時間の定義



これですぐにパスを取得できますが、どのくらいの速さで計算されますか?短いパスはほとんどすぐに表示されますが、大きなマップで長いパスは少し遅く見えるかもしれません。



パスを見つけて表示するのにかかる時間を測定しましょう。プロファイラーを使用して検索時間を決定できますが、これは少し多すぎて追加コストが発生します。代わりStopwatch



に、namespaceにあるを使用しましょうSystem.Diagnostics



。一時的にのみ使用するためusing



、スクリプトの先頭に構造追加しません



検索の直前に、新しいストップウォッチを作成して開始します。検索が完了したら、ストップウォッチを停止し、コンソールに経過時間を表示します。



  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
      
      





アルゴリズムの最悪のケースを選択しましょう-大きな地図の左下から右上隅への検索。アルゴリズムは4,800個のマップセルすべてを処理する必要があるため、最悪の場合は均一なマップです。









最悪の場合の



検索Unityエディターがマシンで実行されている唯一のプロセスではないため、検索にかかる時間は異なる場合があります。したがって、それを数回テストして、平均期間を理解してください。私の場合、検索には約45ミリ秒かかります。これはそれほど多くはなく、1秒あたり22.22パスに相当します。これを22 pps(パス/秒)として示します。つまり、このパスを計算すると、ゲームのフレームレートもそのフレームで最大22 fps低下します。そして、これは、フレーム自体のレンダリングなど、他のすべての作業を考慮に入れていません。つまり、フレームレートがかなり大きく低下し、20 fpsに低下します。



このようなパフォーマンステストを実行する場合、Unityエディターのパフォーマンスは、完成したアプリケーションのパフォーマンスほど高くないことを考慮する必要があります。アセンブリで同じテストを実行すると、平均でわずか15ミリ秒かかります。これは66 ppsで、はるかに優れています。それでも、これはフレームごとに割り当てられるリソースの大部分であるため、フレームレートは60 fps未満になります。



アセンブリのデバッグログはどこで確認できますか?
Unityアプリケーションは、システムに保存されているログファイルに書き込みます。その場所はプラットフォームによって異なります。システムでログファイルを見つける方法については、Unity ログファイルのドキュメントを参照してください


必要な場合にのみ検索



単純な最適化を行うことができます-必要なときにのみ検索を実行します。マウスボタンが押されている各フレームで新しい検索を開始します。したがって、フレームレートは、ドラッグアンドドロップ時に常に過小評価されます。HexMapEditor.HandleInput



新しいエンドポイントを実際に処理している場合のみ、新しい検索を開始することでこれを回避できます。そうでない場合、現在の表示パスはまだ有効です。



  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } }
      
      





パスのラベルのみを表示



旅行マークの表示は、特に最適化されていないアプローチを使用しているため、かなり費用のかかる操作です。すべてのセルに対してこの操作を実行すると、間違いなく実行速度が低下します。のラベル付けをスキップしましょうHexGrid.Search







  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); }
      
      





この情報は、見つかったパスについてのみ表示する必要があります。したがって、終点に到達した後、コースを計算し、途中のセルにのみラベルを設定します。



  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
      
      











パスセルのみのラベルの表示



ここで、開始と終了の間にセルのラベルのみを含めます。しかし、終点は最も重要なものであり、それにラベルを設定する必要もあります。これを行うには、宛先セルのパスサイクルを開始し、その前のセルからではありません。この場合、エンドポイントの赤から白への照明が変化するため、サイクルの下でバックライトを削除します。



  fromCell.EnableHighlight(Color.blue); // toCell.EnableHighlight(Color.red); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current == toCell) { // current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } toCell.EnableHighlight(Color.red); break; } … }
      
      











最も重要なエンドポイントに関する情報



これらの変更後、最悪の場合の時間は、エディタで、完成したアセンブリで6ミリ秒までの23ミリ秒に減少しました。これらは43 ppsと166 ppsです。



ユニティパッケージ



最も賢い検索



前のパートでは、A *アルゴリズムを実装することにより、検索手順をよりスマートにしましたただし、実際にはまだ最適な方法で検索を実行していません。各反復で、現在のセルからそのすべての隣接セルまでの距離を計算します。これは、まだ検索枠に含まれていないか、現在検索枠の一部になっているセルに当てはまります。しかし、すでに境界から削除されたセルは、これらのセルへの最短経路をすでに見つけているため、考慮する必要はありません。A *の正しい実装はこれらのセルをスキップするので、同じことができます。



セル検索フェーズ



セルがすでに境界線を離れているかどうかをどのようにして知るのでしょうか?これを判断することはできませんが。そのため、セルが検索のどの段階にあるかを追跡する必要があります。彼女はまだ国境にいなかったか、今国境にいているか、海外にいます。これを追跡するには、HexCell



単純な整数プロパティに追加します。



  public int SearchPhase { get; set; }
      
      





たとえば、0はセルがまだ到達していないこと、1-セルは現在境界内にあること、2-既に境界から削除されていることを意味します。



ボーダーを打つ



ここでHexGrid.Search



は、すべてのセルを0にリセットし、境界に常に1を使用できます。または、新しい検索ごとに境界線の数を増やすことができます。このおかげで、境界線の数を毎回2ずつ増やしても、セルのダンプを処理する必要はありません。



  int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … }
      
      





次に、境界にセルを追加するときにセル検索のフェーズを設定する必要があります。プロセスは、境界に追加される初期セルから始まります。



  fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell);
      
      





また、国境に隣人を追加するたびに。



  if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); }
      
      





ボーダーチェック



これまで、セルがまだ境界線に追加されていないことを確認するために、に等しい距離を使用しましたint.MaxValue



これで、セル検索のフェーズと現在の境界線を比較できます。



 // if (neighbor.Distance == int.MaxValue) { if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); }
      
      





これは、検索する前にセルの距離をリセットする必要がなくなったことを意味します。つまり、作業が少なくてすみます。これは良いことです。



  for (int i = 0; i < cells.Length; i++) { // cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); }
      
      





国境を離れる



境界からセルが削除されると、検索フェーズの増加によってこれを示します。これにより、彼女は現在の境界を越えて次の境界の前に配置されます。



  while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … }
      
      





これで、境界から削除されたセルをスキップして、無意味な計算と距離の比較を回避できます。



  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … }
      
      





この時点で、アルゴリズムは同じ結果を生成しますが、より効率的です。私のマシンでは、最悪の場合の検索にはエディターで20ミリ秒、アセンブリで5ミリ秒かかります。



アルゴリズムによってセルが処理された回数を計算することもできます。これにより、セルまでの距離を計算するときにカウンターが増えます。以前は、最悪の場合のアルゴリズムは28,239の距離を計算していました。既製のA *アルゴリズムでは、14,120の距離を計算します。量は50%減少しました。これらの指標が生産性に与える影響の程度は、移動コストを計算するコストに依存します。私たちの場合、ここでの作業はあまりないので、アセンブリの改善はそれほど大きくありませんが、エディターでは非常に顕著です。



ユニティパッケージ



道を開く



新しい検索を開始するとき、最初に以前のパスの視覚化をクリアする必要があります。この間、選択をオフにして、各グリッドセルからラベルを削除します。これは非常に難しいアプローチです。理想的には、前のパスの一部であったセルのみをリセットする必要があります。



検索のみ



から視覚化コードを完全に削除することから始めましょうSearch



彼はパス検索を実行するだけでよく、この情報で何をするかを知る必要はありません。



  void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } // for (int i = 0; i < cells.Length; i++) { // cells[i].SetLabel(null); // cells[i].DisableHighlight(); // } // fromCell.EnableHighlight(Color.blue); fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { // while (current != fromCell) { // int turn = current.Distance / speed; // current.SetLabel(turn.ToString()); // current.EnableHighlight(Color.white); // current = current.PathFrom; // } // toCell.EnableHighlight(Color.red); // break; } … } }
      
      





Search



方法が見つかったことを報告するには、ブール値を返します。



  bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; }
      
      





方法を覚えている



パスが見つかったら、それを覚えておく必要があります。これにより、将来的にはクリーニングできるようになります。したがって、エンドポイントと、それらの間にパスがあるかどうかを追跡します。



  HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
      
      





パスをもう一度表示する



記録した検索データを使用して、パスを再度視覚化できます。このための新しいメソッドを作成しましょうShowPath



パスの終わりから始まりまでのサイクルで行われ、セルを強調表示し、ラベルにストローク値を割り当てます。これを行うには、速度を知る必要があるため、速度をパラメータにします。パスがない場合、メソッドは単にエンドポイントを選択します。



  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); }
      
      





FindPath



検索後にこのメソッドを呼び出します。



  currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed);
      
      





スイープ



私たちは再び道を見るが、今ではそれは離れていない。クリアするには、メソッドを作成しますClearPath



実際、これはコピーですShowPath



。ただし、選択とラベルは無効になりますが、それらは含まれません。これを行った後、彼は無効になった記録されたパスデータを消去する必要があります。



  void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; }
      
      





この方法を使用すると、必要なセルのみにアクセスすることで古いパスの視覚化をクリアできます。マップのサイズは重要ではなくなりました。FindPath



新しい検索を開始する前に呼び出してください。



  sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop();
      
      





さらに、新しいマップを作成するときにパスをクリアします。



  public bool CreateMap (int x, int z) { … ClearPath(); if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
      
      





また、別のカードをロードする前に。



  public void Load (BinaryReader reader, int header) { ClearPath(); … }
      
      





この変更前と同様に、パスの視覚化は再びクリアされます。しかし、今ではより効率的なアプローチを使用しており、最悪の検索の場合、時間は14ミリ秒に短縮されています。よりインテリジェントなクリーニングのみによる十分な深刻な改善。アセンブリ時間は3ミリ秒に減少しました。これは333 ppsです。これにより、パスの検索はリアルタイムで正確に適用できます。



パスをすばやく検索したので、一時的なデバッグコードを削除できます。



  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); // sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); // sw.Stop(); // Debug.Log(sw.ElapsedMilliseconds); }
      
      





ユニティパッケージ



パート18:ユニット





パスを検索する方法がわかったので、マップにチームを配置しましょう。









増援が到着しました



チームを作成する



ここまでは、セルとその固定オブジェクトのみを扱ってきました。ユニットはモバイルであるという点でそれらと異なります。分離とは、1人の人間や車両から軍全体に至るまで、あらゆる規模のあらゆるものを意味します。このチュートリアルでは、単純な汎用タイプのユニットに限定します。その後、いくつかのタイプのユニットの組み合わせのサポートに進みます。



プレハブ分隊



分隊で作業するには、新しいタイプのコンポーネントを作成しますHexUnit



とりあえず、空のものから始めて、MonoBehaviour



後で機能を追加しましょう



 using UnityEngine; public class HexUnit : MonoBehaviour { }
      
      





このコンポーネントで空のゲームオブジェクトを作成します。これはプレハブになります。これがチームのルートオブジェクトになります。









プレハブ部隊。



分離を象徴する3Dモデルを子オブジェクトとして追加します。青色のマテリアルを作成したシンプルなスケールキューブを使用しました。ルートオブジェクトがデタッチメントのグラウンドレベルを決定するため、それに応じて子要素を移動します。















子要素キューブ



将来的に選択しやすくするために、チームにコライダーを追加します。標準キューブのコライダーは私たちに非常に適しています。コライダーを1つのセルに収めるだけです。



分隊インスタンスの作成



まだゲームプレイがないため、ユニットの作成は編集モードで行われます。したがって、これに対処する必要がありますHexMapEditor



これを行うには、プレハブが必要なので、フィールドHexUnit unitPrefab



追加して接続します。



  public HexUnit unitPrefab;
      
      











プレハブの接続



ユニットを作成するとき、それらをカーソルの下のセルに配置します。HandleInput



地形を編集するときにこのセルを見つけるためのコードがあります。今度はチームにも必要なので、対応するコードを別のメソッドに移動します。



  HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; }
      
      





これで、このメソッドを使用してHandleInput



単純化できます



  void HandleInput () { // Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); // RaycastHit hit; // if (Physics.Raycast(inputRay, out hit)) { // HexCell currentCell = hexGrid.GetCell(hit.point); HexCell currentCell = GetCellUnderCursor(); if (currentCell) { … } else { previousCell = null; } }
      
      





次に、を使用する新しいメソッドCreateUnit



追加しますGetCellUnderCursor



セルがあれば、新しいチームを作成します。



  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } }
      
      





階層をきれいに保つために、グリッドをチームのすべてのゲームオブジェクトの親として使用してみましょう。



  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } }
      
      





HexMapEditor



ユニット作成のサポートを追加する最も簡単な方法は、キーを押すことです。Uキーが押されたときUpdate



に呼び出さCreateUnit



れるようにメソッドを変更しますc HandleInput



同様に、これはカーソルがGUI要素の上にない場合に発生します。最初に、マップを編集する必要があるかどうかを確認し、編集しない場合は、チームを追加する必要があるかどうかを確認します。その場合は、を呼び出しますCreateUnit







  void Update () { // if ( // Input.GetMouseButton(0) && // !EventSystem.current.IsPointerOverGameObject() // ) { // HandleInput(); // } // else { // previousCell = null; // } if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButton(0)) { HandleInput(); return; } if (Input.GetKeyDown(KeyCode.U)) { CreateUnit(); return; } } previousCell = null; }
      
      











分隊のインスタンスを作成しました



部隊配置



これでユニットを作成できますが、それらはマップの原点に表示されます。適切な場所に配置する必要があります。このためには、軍隊が自分の立場を認識する必要があります。したがって、それらが占めるセルを示すHexUnit



プロパティに追加しますLocation



プロパティを設定するとき、セルの位置に一致するように分隊の位置を変更します。



  public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location;
      
      





次にHexMapEditor.CreateUnit



、カーソルの下の分隊セルの位置を割り当てる必要があります。その後、ユニットは必要な場所に配置されます。



  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } }
      
      











マップ上の分隊



ユニットの向き



これまでのところ、すべてのユニットの向きは同じで、かなり不自然に見えます。それらを復活させるには、HexUnit



プロパティに追加しますOrientation



これは、Y軸に沿った分隊の回転を度単位で示すフロート値です。それを設定するとき、それに応じてゲームオブジェクト自体の回転を変更します。



  public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation;
      
      





HexMapEditor.CreateUnit



0〜360度から割り当てランダム回転。



  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
      
      











異なるユニットの向き



セルごとに1つのチーム



ユニットは、1つのセルで作成されていない場合に表示されます。この場合、奇妙に見えるキューブのクラスターを取得します。









オーバーレイされたユニット



ゲームによっては、複数のユニットを1か所に配置できるものとそうでないものがあります。セルごとに1つのチームで作業する方が簡単なので、このオプションを選択します。これは、現在のセルが占有されていない場合にのみ新しいチームを作成する必要があることを意味します。見つけられるように、HexCell



標準プロパティに追加しますUnit







  public HexUnit Unit { get; set; }
      
      





このプロパティを使用してHexUnit.Location



、ユニットがその上にあるかどうかをセルに知らせます。



  public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } }
      
      





現在HexMapEditor.CreateUnit



、現在のセルが空いているかどうかを確認できます。



  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
      
      





ビジーセルの編集



最初は、ユニットは正しく配置されていますが、将来セルが編集される場合、すべてが変更される可能性があります。セルの高さが変わると、セルを占有しているユニットがセルの上にぶら下がるか、セルに突入します。









ぶら下げてandれた分隊



解決策は、変更を行った後、分隊の位置を確認することです。これを行うには、メソッドをに追加しHexUnit



ます。これまでのところ、デタッチメントの位置にのみ関心があるため、単純に再設定します。



  public void ValidateLocation () { transform.localPosition = location.Position; }
      
      





セルを更新するとき、メソッドRefresh



またはRefreshSelfOnly



オブジェクトがHexCell



呼び出されたときに何が起こるか、デタッチメントの位置を調整する必要がありますもちろん、これはセルに実際に分離がある場合にのみ必要です。



  void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } }
      
      





チームを削除する



ユニットを作成することに加えて、ユニットを破壊することは有用です。したがって、HexMapEditor



メソッドに追加しDestroyUnit



ます。彼は、カーソルの下のセルにデタッチメントがあるかどうかを確認する必要があります。ある場合は、デタッチメントのゲームオブジェクトを破壊します。



  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } }
      
      





分隊に着くために、私たちはセルを通過することに注意してください。分隊とやり取りするには、マウスをそのセルの上に移動するだけです。したがって、これが機能するためには、チームにはコライダーが必要ありません。ただし、コライダーを追加すると、分隊の背後のセルに衝突する光線をブロックするため、それらを簡単に分離できます。左Shift + Uの組み合わせを使用



して、分隊を破壊しましょうUpdate







  if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; }
      
      





複数のユニットを作成および破棄する場合、ユニットを削除する際に注意してプロパティをクリアしましょう。つまり、分隊へのセルリンクを明示的にクリアします。これに対処するHexUnit



メソッドに加えて、Die



独自のゲームオブジェクトを破壊します。



  public void Die () { location.Unit = null; Destroy(gameObject); }
      
      





このメソッドはで呼び出しHexMapEditor.DestroyUnit



、分隊を直接破壊しません。



  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // Destroy(cell.Unit.gameObject); cell.Unit.Die(); } }
      
      





ユニティパッケージ



チームの保存と読み込み



マップ上にユニットを配置できるようになったので、保存および読み込みプロセスにユニットを含める必要があります。このタスクには2つの方法でアプローチできます。1つは、セルと分隊データが混合されるように、セルを記録するときに分隊データを記録することです。2番目の方法は、セルと分隊のデータを別々に保存することです。最初のアプローチの方が実装が簡単に思えるかもしれませんが、2番目のアプローチはより構造化されたデータを提供します。データを共有すれば、将来それらとの作業が容易になります。



ユニット追跡



すべてのユニットをまとめるために、それらを追跡する必要があります。これを行うにはHexGrid



、ユニットリストに追加します。このリストには、マップ上のすべてのユニットが含まれている必要があります。



  List<HexUnit> units = new List<HexUnit>();
      
      





新しいマップを作成またはロードするとき、マップ上のすべてのユニットを取り除く必要があります。このプロセスを簡素化するにClearUnits



は、リスト内の全員を殺してクリアするメソッド作成します



  void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); }
      
      





このメソッドをCreateMap



とで呼び出しますLoad



道を掃除してからやろう。



  public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … }
      
      





グリッドへのチームの追加



ここで、新しいユニットを作成するときに、それらをリストに追加する必要があります。これのメソッドを設定してみましょう。これAddUnit



は、分隊の位置とその親オブジェクトのパラメーターも処理します。



  public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; }
      
      





これで、デタッチメントの新しいインスタンス、その場所、ランダムな方向でHexMapEditor.CreatUnit



呼び出すだけで十分AddUnit



です。



  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { // HexUnit unit = Instantiate(unitPrefab); // unit.transform.SetParent(hexGrid.transform, false); // unit.Location = cell; // unit.Orientation = Random.Range(0f, 360f); hexGrid.AddUnit( Instantiate(unitPrefab), cell, Random.Range(0f, 360f) ); } }
      
      





グリッドからチームを削除する



分隊とcを削除するメソッドを追加しますHexGrid



リストからチームを削除して、死ぬように命じてください。



  public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); }
      
      





HexMapEditor.DestroyUnit



チームを直接破壊する代わりこのメソッドを呼び出します



  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // cell.Unit.Die(); hexGrid.RemoveUnit(cell.Unit); } }
      
      





ユニットの保存



すべてのユニットをまとめておくので、ユニットが占めるセルを覚えておく必要があります。最も信頼できる方法は、その場所の座標を保存することです。これを可能にするために、フィールドXとZ それを書き込むHexCoordinates



メソッドに追加しSave



ます。



 using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } }
      
      





この方法Save



のためのHexUnit



缶は今ユニットの座標と向きを記録します。これは、現在持っているユニットのすべてのデータです。



 using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } }
      
      





HexGrid



ユニットを追跡するため、そのメソッドSave



はユニットのデータを記録します。最初に、ユニットの総数を書き留めてから、ループ内でそれらをすべて回ってください。



  public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } writer.Write(units.Count); for (int i = 0; i < units.Count; i++) { units[i].Save(writer); } }
      
      





保存されているデータを変更したため、バージョン番号SaveLoadMenu.Save



を2に増やします。古いブートコードは、分隊データを読み取れないため、引き続き機能します。ただし、ファイルにユニットデータがあることを示すには、バージョン番号を増やす必要があります。



  void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } }
      
      





部隊の読み込み



これHexCoordinates



は構造体であるため、通常のメソッドを追加することはあまり意味がありませんLoad



保存された座標を読み込んで返す静的メソッドにします。



  public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; }
      
      





ユニットの数は可変であるため、データをロードできる既存のユニットはありません。データを読み込む前にユニットの新しいインスタンスを作成できますが、これにはHexGrid



ブート時に新しいユニットのインスタンス作成する必要があります。そのままにしておく方が良いHexUnit



です。また、静的メソッドも使用しますHexUnit.Load



これらのチームを単に読むことから始めましょう。方向フロートの値を読み取るには、メソッドを使用しBinaryReader.ReadSingle



ます。



なぜ独身?
float



, . , double



, . Unity .


  public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); }
      
      





次のステップは、新しいチームのインスタンスを作成することです。ただし、このためには、ユニットのプレハブへのリンクが必要です。まだ複雑にならないように、これにHexUnit



静的メソッドを追加しましょう



  public static HexUnit unitPrefab;
      
      





このリンクを設定HexGrid



するには、ノイズテクスチャで行ったように再度使用します。多くの種類のユニットをサポートする必要がある場合、より良いソリューションに進みます。



  public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } }
      
      











ユニットのプレハブを渡します。



フィールドを接続した後、に直接リンクする必要はなくなりましたHexMapEditor



代わりに、彼はを使用できますHexUnit.unitPrefab







 // public HexUnit unitPrefab; … void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { hexGrid.AddUnit( Instantiate(HexUnit.unitPrefab), cell, Random.Range(0f, 360f) ); } }
      
      





これで、新しいチームのインスタンスを作成できますHexUnit.Load



返す代わりに、ロードされた座標と方向を使用してグリッドに追加できます。これを可能にするには、パラメーターを追加しますHexGrid







  public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); }
      
      





最後に、HexGrid.Load



ユニットの数をカウントし、それを使用して、格納されているすべてのユニットをロードし、追加の引数として自分自身を渡します。



  public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
      
      





もちろん、これは、バージョンが2以上の保存ファイルに対してのみ機能します。若いバージョンでは、ロードするユニットがありません。



  if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
      
      





バージョン2のファイルを正しくアップロードできるようにSaveLoadMenu.Load



なったため、サポートされるバージョンの数を2 増やします



  void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 2) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
      
      





ユニティパッケージ



軍隊の動き



分隊はモバイルであるため、マップ上で移動できる必要があります。既にパス検索コードがありますが、これまでのところ、任意の場所についてのみテストしました。次に、古いテストUIを削除し、チーム管理用の新しいUIを作成する必要があります。



マップエディターのクリーンアップ



パスに沿ってユニットを移動することはゲームプレイの一部であり、マップエディターには適用されません。したがって、HexMapEditor



パスの検索に関連するすべてのコードを取り除きます。



 // HexCell previousCell, searchFromCell, searchToCell; HexCell previousCell; … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } // else if ( // Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell // ) { // if (searchFromCell != currentCell) { // if (searchFromCell) { // searchFromCell.DisableHighlight(); // } // searchFromCell = currentCell; // searchFromCell.EnableHighlight(Color.blue); // if (searchToCell) { // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } // } // else if (searchFromCell && searchFromCell != currentCell) { // if (searchToCell != currentCell) { // searchToCell = currentCell; // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } previousCell = currentCell; } else { previousCell = null; } }
      
      





このコードを削除した後、編集モードではないときにエディターをアクティブのままにしておくことは意味がありません。したがって、モード追跡フィールドの代わりに、コンポーネントを有効または無効にすることができますHexMapEditor



さらに、エディターはUIラベルを処理する必要がなくなりました。



 // bool editMode; … public void SetEditMode (bool toggle) { // editMode = toggle; // hexGrid.ShowUI(!toggle); enabled = toggle; } … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } // if (editMode) { EditCells(currentCell); // } previousCell = currentCell; } else { previousCell = null; } }
      
      





デフォルトではマップ編集モードではないため、アウェイクではエディターを無効にします。



  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); SetEditMode(false); }
      
      





レイキャストを使用して、マップを編集するときにカーソルの下の現在のセルを検索し、ユニットを制御する必要があります。おそらく将来的には、他の何かのために私たちに役立つでしょう。レイキャスティングロジックを、ビームパラメーターを使用HexGrid



した新しいメソッドに移動しましょうGetCell







  public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; }
      
      





HexMapEditor.GetCellUniderCursor



カーソルビームでこのメソッドを呼び出すだけです。



  HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
      
      





ゲームUI



ゲームモードUIを制御するには、新しいコンポーネントを使用します。彼はユニットの選択と移動のみを扱います。新しいコンポーネントタイプを作成しますHexGameUI



彼の仕事をするには、グリッドへのリンクで十分です。



 using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; }
      
      





このコンポーネントをUI階層の新しいゲームオブジェクトに追加します。彼は自分のオブジェクトを持っている必要はありませんが、ゲームには別のUIがあることは明らかです。















ゲームUIオブジェクトのよう



HexGameUI



メソッドを追加SetEditMode



HexMapEditor



ます。編集モードでないときは、ゲームUIをオンにする必要があります。また、ゲームUIはパスで機能するため、ここにラベルを含める必要があります。



  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); }
      
      





編集モードスイッチのイベントリストにゲームUIメソッドを追加します。これは、プレーヤーがモードを変更すると、両方のメソッドが呼び出されることを意味します。









いくつかのイベントメソッド。



現在のセルを追跡



状況に応じて、HexGameUI



現在カーソルの下にあるセルを知る必要があります。したがって、フィールドを追加しますcurrentCell







  HexCell currentCell;
      
      





カーソルビームをUpdateCurrentCell



使用してHexGrid.GetCell



このフィールドを更新するメソッド作成します



  void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
      
      





現在のセルを更新するとき、変更されているかどうかを確認する必要がある場合があります。UpdateCurrentCell



この情報を強制的に返します。



  bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; }
      
      





ユニット選択



分隊を移動する前に、それを選択して追跡する必要があります。したがって、フィールドを追加しますselectedUnit







  HexUnit selectedUnit;
      
      





選択を試みるとき、現在のセルを更新することから始める必要があります。現在のセルがこのセルを占めるユニットである場合、選択されたユニットになります。セルにユニットがない場合、ユニットは選択されません。このためのメソッドを作成しましょうDoSelection







  void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
      
      





ユニットの選択は、マウスを1回クリックするだけで実現します。したがって、Update



マウスボタンがアクティブになったときに選択を実行するメソッドを追加しますが、もちろん、カーソルがGUI要素の上にないときにのみ実行する必要があります。



  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } }
      
      





この段階で、マウスをクリックして一度に1つのユニットを選択することを学びました。空のセルをクリックすると、ユニットの選択が削除されます。しかし、これについては視覚的な確認は受けていません。



分隊で道を探す



ユニットを選択すると、その場所をパスを見つけるための開始点として使用できます。これをアクティブにするために、マウスボタンをもう一度クリックする必要はありません。代わりに、分隊の位置と現在のセルの間のパスを自動的に見つけて表示します。Update



選択が行われた場合を除き、これは常にで行います。これを行うには、デタッチメントがあるときに、メソッドを呼び出しますDoPathfinding







  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } }
      
      





DoPathfinding



現在のセルを更新しHexGrid.FindPath



、エンドポイントがある場合に呼び出します。再び24の一定速度を使用します。



  void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); }
      
      





更新ごとに新しいパスを見つける必要はありませんが、現在のセルが変更されたときにのみ見つける必要があることに注意してください。



  void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } }
      
      











デタッチメントのパスを見つけるこれで、デタッチメント



を選択した後にカーソルが移動したときに表示されるパスが表示されます。これにより、どのユニットが選択されているかが明らかです。ただし、パスは常に正しくクリアされるとは限りません。まず、カーソルがマップ外にある場合、古いパスをクリアしましょう。



  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
      
      





もちろん、これにはHexGrid.ClearPath



共通性が必要なので、このような変更を行います。



  public void ClearPath () { … }
      
      





次に、デタッチメントを選択するときに古いパスをクリアします。



  void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
      
      





最後に、編集モードを変更するときにパスをクリアします。



  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); }
      
      





有効なエンドポイントのみを検索



最終的なセルに到達できない場合があるため、常に道を見つけることはできません。 これは正常です。しかし、最終的なセル自体が受け入れられない場合があります。たとえば、パスには水中セルを含めることはできないと判断しました。ただし、ユニットに依存する場合があります。HexUnit



セルが有効なエンドポイントであるかどうかを通知するメソッドに追加しましょう水中の細胞はそうではありません。



  public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater; }
      
      





さらに、セル内に立つことができるユニットは1つだけです。したがって、最終セルはビジーの場合は無効になります。



  public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater && !cell.Unit; }
      
      





このメソッドを使用して、HexGameUI.DoPathfinding



無効なエンドポイントを無視します。



  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
      
      





終点に移動



有効なパスがあれば、分隊を終点に移動できます。HexGrid



これがいつできるかを知っています。この情報を新しい読み取り専用プロパティに渡しますHasPath







  public bool HasPath { get { return currentPathExists; } }
      
      





分隊を移動するには、HexGameUI



メソッドに追加しDoMove



ます。このメソッドは、コマンドが発行されたとき、およびユニットが選択されたときに呼び出されます。したがって、彼は方法があるかどうかを確認する必要があります。ある場合は、切り離しの場所を変更します。すぐにチームを終点にテレポートします。次のチュートリアルのいずれかで、チームを実際に最後まで進めます。



  void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } }
      
      





マウスボタン1(右クリック)を使用してコマンドを送信しましょう。デタッチメントが選択されている場合、これをチェックします。ボタンが押されていない場合、パスを検索します。



  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } }
      
      





これでユニットを移動できます!しかし、時には彼らはいくつかの細胞への道を見つけることを拒否します。特に、剥離がかつてあった細胞。これはHexUnit



、新しい場所を設定するときに古い場所を更新しないために発生します。これを修正するために、古い場所のチームへのリンクをクリアします。



  public HexCell Location { get { return location; } set { if (location) { location.Unit = null; } location = value; value.Unit = this; transform.localPosition = value.Position; } }
      
      





分隊を避ける



道を見つけることが正しく機能するようになり、ユニットはマップ上でテレポートできます。すでに分隊を持っているセルに移動することはできませんが、邪魔になっている分遣隊は無視されます。









途中で軍無視



1つの派閥の分隊は、通常、互いに間を移動することができますが、これまでのところ、我々は派閥を持っていません。したがって、すべてのユニットが互いに切断され、パスをブロックしていると考えてみましょう。これは、のビジーセルをスキップすることで実現できますHexGrid.Search







  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; }
      
      











避け分遣隊の



unitypackage



パート19:モーションアニメーション





このパートでは、テレポーテーションの代わりにユニットをトラックに沿って移動させます。









途中の分隊



道に沿った動き



前のパートでは、ユニットとそれらを移動する機能を追加しました。有効なエンドポイントを決定するためにパスの検索を使用しましたが、コマンドを与えた後、軍隊は最終的なセルにテレポートしました。彼らが実際に見つかった経路をたどるように、この経路を追跡し、分隊をセルからセルに強制的に移動させるアニメーションプロセスを作成する必要があります。アニメーションを見ると、分隊がどのように動いたかに気付くのが難しいため、ギズモを使用して移動したパスも視覚化します。しかし、先に進む前に、エラーを修正する必要があります。



ターンエラー



監視のため、セルに到達するコースを誤って計算します。ここで、合計距離を分隊速度で割ることにより、コースを決定しますで、残りを破棄します。このエラーは、セルに移動するときに、移動ごとに残りのすべての移動ポイントを正確に費やす必要があるときに発生します。たとえば、各ステップのコストが1で、速度が3の場合、1ターンあたり3つのセルを移動できます。ただし、既存の計算では、最初の移動では2つのステップしか実行できません。t=d/s



t=d/s=3/3=1









誤って定義された移動、速度3で移動する総コスト。移動を正しく



計算するには、境界を最初のセルから1ステップ移動する必要があります。これを行うには、移動を計算する前に距離を1減らすことにより、3番目のステップの移動は次のようになります。t=2/3=0









正しい動き



計算式をt=(d1)/sこの変更をに行いHexGrid.Search



ます。



  bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; }
      
      





また、動きのマークを変更します。



  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … }
      
      





このアプローチでは、初期セルパスが-1であることに注意してください。これは表示されないため正常であり、検索アルゴリズムは引き続き動作します。



方法を取得



パスに沿って移動することはチームのタスクです。彼がこれを行うためには、彼は方法を知る必要があります。この情報HexGrid



があるので、セルのリストの形式で現在のパスを取得するメソッドを追加しましょう。彼はリストのプールからそれを取得し、本当にパスがあれば戻ります。



  public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; }
      
      





リストは、パスを視覚化するときに行われるように、最後のセルから最初のセルへのリンクパスをたどることで埋められます。



  List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path;
      
      





この場合、初期セルを含むパス全体が必要です。



  for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path;
      
      





これで、パスは逆の順序になりました。彼と仕事をすることはできますが、それはあまり直感的ではありません。リストを反転させて、最初から最後まで行きましょう。



  path.Add(currentPathFrom); path.Reverse(); return path;
      
      





モーションリクエスト



これでHexUnit



メソッドに追加して、パスに従うように彼に命令することができます。最初は、彼に最後のセルにテレポートさせるだけです。リストはしばらく役立つので、すぐにリストをプールに返しません。



 using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … }
      
      





移動を要求するにHexGameUI.DoMove



は、ユニットの位置を設定するだけでなく、現在のパスで新しいメソッドを呼び出すように変更します。



  void DoMove () { if (grid.HasPath) { // selectedUnit.Location = currentCell; selectedUnit.Travel(grid.GetPath()); grid.ClearPath(); } }
      
      





パスの可視化



分隊のアニメーションを開始する前に、パスが正しいことを確認しましょう。HexUnit



ギズモを使用して視覚化できるように、移動する必要があるパス記憶するように命令することでこれを行います



  List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; }
      
      





OnDrawGizmos



最後のパスを表示するメソッド追加します(存在する場合)。ユニットがまだ移動していない場合、パスは等しくなりnull



ます。ただし、プレイモードでの再コンパイル後の編集中のUnityのシリアル化により、空のリストになることもあります。



  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } }
      
      





パスを表示する最も簡単な方法は、パスの各セルにギズモ球を描くことです。半径が2単位の球体が適しています。



  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } }
      
      





デタッチメントのパスを表示するので、最後のパスをすべて同時に見ることができます。









ギズモは、最後に移動したパスを表示します



セルの接続をよりよく表示するために、前のセルと現在のセルの間の線にループでいくつかの球体を描画します。これを行うには、2番目のセルからプロセスを開始する必要があります。球は、0.1単位の増分で線形補間を使用して配置できるため、セグメントごとに10個の球が得られます。



  for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
      
      











より明白な方法



道に沿って滑る



同じ方法を使用してユニットを移動できます。このためのコルーチンを作成しましょう。ギズモを描画する代わりに、分隊の位置を設定します。増分する代わりに、0.1時間のデルタを使用し、各反復でyieldを実行します。この場合、分隊は1秒で1つのセルから次のセルに移動します。



 using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … }
      
      





メソッドの最後にコルーチンを開始しTravel



ます。ただし、最初に、既存のコルーチンをすべて停止します。そのため、2つのコルーチンが同時に開始しないことを保証します。そうしないと、非常に奇妙な結果になります。



  public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); }
      
      





1秒間に1つのセルを移動するのはかなり遅いです。ゲーム中のプレイヤーはそんなに長く待ちたくないでしょう。分隊の移動速度を構成オプションにすることもできますが、ここでは定数を使用します。私は彼女に毎秒4セルの値を割り当てました。それは非常に高速ですが、何が起こっているかを確認できます。



  const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } }
      
      





複数の経路を同時に視覚化できるように、複数のユニットを同時に移動させることができます。ゲームの状態の観点から見ると、動きは依然としてテレポーテーションであり、アニメーションは視覚的にのみです。ユニットは即座に最終セルを占有します。方法を見つけて、到着する前に新しい動きを開始することもできます。この場合、それらは視覚的に新しいパスの始まりにテレポートされます。これは、移動中にユニットまたはUI全体をブロックすることで回避できますが、このような迅速な反応は、移動を設計およびテストするときに非常に便利です。





移動ユニット。



高さの違いはどうですか?
, . , . , . , . , Endless Legend, , . , .


コンパイル後の位置



コルチンの欠点の1つは、プレイモードで再コンパイルしても「生き残らない」ことです。ゲームの状態は常に真ですが、これは、彼らがまだ動いている間に再コンパイルが開始されると、分隊が最後のパスのどこかで立ち往生することにつながる可能性があります。結果を緩和するために、再コンパイル後、ユニットが常に正しい位置にあることを確認しましょう。これは、の位置を更新することで実行できますOnEnable







  void OnEnable () { if (location) { transform.localPosition = location.Position; } }
      
      





ユニティパッケージ



スムーズな動き



セルの中心から中心への動きは機械的すぎるように見え、方向が急激に変化します。多くのゲームでは、これは正常ですが、少なくとも少し現実的な動きが必要な場合は受け入れられません。それでは、ムーブメントを変更して、少し有機的に見えるようにします。



端から端へ移動します



チームはセルの中心から旅を始めます。セルの端の中央に移動し、その後次のセルに入ります。彼は中央に向かって移動する代わりに、彼が交差しなければならない次のエッジに向かってまっすぐ進むことができます。実際、ユニットは方向を変える必要があるときにパスをカットします。これは、パスの終点を除くすべてのセルで可能です。









端から端へ移動する3つの方法 この方法で生成されたパスの表示に



適応OnDrawGizmos



ましょうセルのエッジ間を補間する必要があります。これは、隣接するセルの位置を平均化することで見つけることができます。反復ごとに1つのエッジを計算し、前の反復からの値を再利用するだけで十分です。したがって、メソッドを初期セルに対して機能させることができますが、エッジの代わりにその位置を取ります。



  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } }
      
      





終了セルの中心に到達するには、セルの位置をエッジではなく最後のポイントとして使用する必要があります。このケースのチェックをループに追加できますが、単純なコードなので、コードを複製して少し変更するだけの方が明らかになります。



  void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
      
      











リブベースの



パス結果として生じるパスはジグザグのように見えず、最大回転角度は120°から90°に減少します。これは改善と見なすことができるため、同じ変更をコルーチンTravelPath



適用して、アニメーションでどのように見えるかを確認します。



  IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } }
      
      







変化する速度での移動



角度をカットした後、パスセグメントの長さは方向の変化に依存するようになりました。ただし、1秒あたりのセル数で速度を設定します。その結果、分隊の速度はランダムに変化します。



次の曲線



セルの境界を越えるときの方向と速度の瞬間的な変化は見苦しい。方向の段階的な変更を使用することをお勧めします。これをサポートするには、部隊を強制的に直線ではなく曲線に沿って追従させます。これにはベジェ曲線を使用できます。特に、セルの中心が中央の制御点になる2次ベジエ曲線を使用できます。この場合、隣接する曲線の接線は互いに鏡像になります。つまり、パス全体が連続した滑らかな曲線になります。









エッジからエッジへの 曲線二次ベジェ曲線上の点を取得する方法で



補助クラスBezier



作成します曲線とスプラインのチュートリアル説明されているように、式はこのために使用されます、ここで(1t)2A+2(1t)tB+t2CAB そして は制御点で、tは補間器です。C



 using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } }
      
      





GetPointは0-1に制限されるべきではありませんか?
0-1, . . , GetPointClamped



, t



. , GetPointUnclamped



.


で曲線のパスを表示するにはOnDrawGizmos



、2つではなく3つのポイントを追跡する必要があります。追加のポイントは、現在の反復で作業しているセルの中心ですi - 1



。サイクルは1から始まるため、インデックスがあります。すべてのポイントを受け取ったらVector3.Lerp



、に置き換えることができBezier.GetPoint



ます。



開始点と終了点では、終了点と中間点の代わりに、単純にセルの中心を使用できます。



  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } }
      
      











ベジェで作成したパスの



曲線パスが非常に良く見えます。同じ変更を適用し、TravelPath



このアプローチでユニットがどのようにアニメーション化されるかを確認します。



  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
      
      







カーブに沿って移動する



と、デタッチの速度が不安定な場合でも、アニメーションもスムーズになりました。隣接するセグメントの曲線の接線が一致するため、速度は連続的です。速度の変化は徐々に起こり、剥離がセルを通過するときに起こり、方向が変わると減速します。彼が直進した場合、速度は一定のままです。さらに、分隊はゼロ速度で旅を開始および終了します。これは自然な動きを模倣しているので、そのままにしておきます。



時間の追跡



この時点まで、各セグメントの0から反復を開始し、1に達するまで継続しました。これは一定の値で増加する場合は正常に機能しますが、反復はデルタ時間に依存します。 1つのセグメントでの反復が完了すると、時間差に応じて、1をある程度超える可能性があります。これは、高フレームレートでは見えませんが、低フレームレートではぎくしゃくすることがあります。



時間のロスを避けるため、残りの時間をあるセグメントから次のセグメントに転送する必要があります。これはt



、各セグメントだけでなく、パス全体に沿って追跡することで実行できます。次に、各セグメントの終わりに、1を減算します。



  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
      
      





すでにこれを行っている場合は、パスの開始時に時間の差分が考慮されるようにします。つまり、私たちはすぐに動き始め、1つのフレームの間アイドル状態になりません。



  float t = Time.deltaTime * travelSpeed;
      
      





さらに、パスが終了する時点で正確に終了するのではなく、少し前に終了します。ここで、違いはフレームレートにも依存します。したがって、分隊に正確に終点でパスを完成させましょう。



  IEnumerator TravelPath () { … transform.localPosition = location.Position; }
      
      





ユニティパッケージ



オリエンテーションアニメーション



ユニットは滑らかな曲線に沿って動き始めましたが、動きの方向に応じて方向を変えませんでした。その結果、彼らは滑るように見えます。動きを実際の動きのように見せるために、それらを回転させる必要があります。



楽しみにして



曲線とスプラインのチュートリアルのように、曲線の導関数を使用してユニットの方向を決定できます。二次ベジェ曲線の導関数の式:2((1t)(BA)+t(CB))Bezier



メソッドに追加して計算します。



  public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); }
      
      





微分ベクトルは、動きの方向と1本の直線上にあります。このメソッドQuaternion.LookRotation



を使用して、分隊ターンに変換できますのすべてのステップで実行しHexUnit.TravelPath



ます。



  transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null;
      
      





パスの始めに間違いはありませんか?
, . A そして B , . , t=0 , , Quaternion.LookRotation



. , , t=0 . . , t>0 .

, t<1


デタッチメントの位置とは対照的に、パスの終点での方向の非理想性は重要ではありません。ただし、その向きが最終回転に対応することを確認する必要があります。これを行うには、完了後、その向きをYの回転と同等にします。



  transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y;
      
      





現在、ユニットは水平方向と垂直方向の両方で移動方向を正確に見ています。これは、彼らが前後に傾き、斜面から降りて登ることを意味します。それらが常に真っ直ぐになるように、方向ベクトルの成分Yを強制的にゼロにしてから、ユニットの回転を決定します。



  Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d);
      
      







移動しながら楽しみ



ポイントを見る



パス全体を通して、ユニットは前方を見ますが、移動を開始する前に、ユニットは他の方向を見ることができます。この場合、彼らは即座に向きを変えます。移動を開始する前にパスの方向に曲がる方が良いでしょう。



他の状況では正しい方向を見るのが便利な場合があるのでLookAt



、特定のポイントを見るためにチームの向きを強制的に変更するメソッド作成しましょう。メソッドを使用して、必要な回転を設定するにはTransform.LookAt



、まず、デタッチメントと同じ垂直位置にポイントを作成します。その後、チームの方向を抽出できます。



  void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
      
      





デタッチメントを実際に回転させるために、メソッドを別のコルチンに変え、一定の速度で回転させます。回転速度も調整できますが、ここでも定数を使用します。回転は毎秒約180°の速さでなければなりません。



  const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … }
      
      





感知できないので、ターンの加速をいじる必要はありません。 2つの方向の間を単純に補間するだけで十分です。残念ながら、角度は円形であるため、これは2つの数値の場合ほど簡単ではありません。たとえば、350°から10°への移行では、時計回りに20°回転しますが、単純な補間では、反時計回りに340°回転します。



正しい回転を作成する最も簡単な方法は、球面補間を使用して2つのクォータニオン間を補間することです。これは最短のターンにつながります。これを行うには、最初と最後のクォータニオンを取得し、を使用してそれらの間のトランジションを作成しQuaternion.Slerp



ます。



  IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
      
      





これは機能しますが、回転角度に関係なく、補間は常に0から1になります。角速度を均一にするには、回転角が大きくなるにつれて補間を遅くする必要があります。



  Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; }
      
      





角度がわかっているので、ターンがゼロになった場合、ターンを完全にスキップできます。



  float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } }
      
      





これで、ユニットの回転をに追加できます。2番目のセルの位置でTravelPath



歩留まりLookAt



移動する前に実行するだけです。Unityは自動的にkorutinu実行されLookAt



、そしてTravelPath



その完了を待ちます。



  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … }
      
      





コードを確認すると、分隊は最終セルにテレポートし、そこでターンし、次にパスの先頭にテレポートし、そこから移動を開始します。これはLocation



、coroutineの開始前にプロパティ値を割り当てるために発生しますTravelPath



テレポーテーションを取り除くTravelPath



ために、最初にデタッチメントの位置を初期セルに戻すことができます。



  Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position);
      
      







移動する前に回す



スイープ



必要な動きを受け取ったら、メソッドを取り除くことができますOnDrawGizmos



将来パスを確認する必要がある場合に備えて、削除するかコメントアウトしてください。



 // void OnDrawGizmos () { // … // }
      
      





移動した方向を覚える必要がなくなったため、最終的TravelPath



にセルのリストを解放できます。



  IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; }
      
      





本物のチームのアニメーションはどうですか?
, . 3D- . . , . Mecanim, TravelPath



.


ユニティパッケージ



All Articles