Unityの六角形マップ:保存と読み込み、テクスチャ、距離

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



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



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



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



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



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



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



パート12:保存と読み込み





かなり興味深いマップを作成する方法はすでに知っています。 今、あなたはそれらを保存する方法を学ぶ必要があります。









test.mapファイルからロードされます。



地形タイプ



地図を保存するとき、アプリケーション中に追跡するすべてのデータを保存する必要はありません。 たとえば、セルの高さレベルを覚えるだけです。 その垂直位置自体はこのデータから取得されるため、保存する必要はありません。 実際、これらの計算された指標を保存しない方が良いです。 したがって、後で高さオフセットを変更することにした場合でも、マップデータは正しいままです。 データはプレゼンテーションとは別のものです。



同様に、セルの正確な色を保存する必要はありません。 セルが緑色であることを書くことができます。 しかし、視覚スタイルが変わると、正確な緑の色合いが変わる可能性があります。 これを行うために、色そのものではなく、色のインデックスを保存できます。 実際、実行時にセルに実際の色の代わりにこのインデックスを保存すれば十分かもしれません。 これにより、後でレリーフのより複雑な視覚化に進むことができます。



色の配列を移動する



セルにカラーデータがなくなった場合は、別の場所に保存する必要があります。 HexMetrics



に保存するのが最も便利HexMetrics



。 色の配列を追加してみましょう。



  public static Color[] colors;
      
      





ノイズなどの他のすべてのグローバルデータと同様に、これらの色をHexGrid



初期化できます。



  public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; … } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; } }
      
      





また、セルに直接色を割り当てないので、デフォルトの色を取り除きます。



 // public Color defaultColor = Color.white; … void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); // cell.Color = defaultColor; … }
      
      





六角マップエディターの一般的な配列に一致するように新しい色を設定します。









グリッドに追加された色。



セルリファクタリング



HexCell



からカラーフィールドを削除します。 代わりに、インデックスを保存します。 カラーインデックスの代わりに、より一般的なレリーフタイプインデックスを使用します。



 // Color color; int terrainTypeIndex;
      
      





colorプロパティは、対応する色を取得するためにのみこのインデックスを使用できます。 現在は直接設定されていないため、この部分を削除します。 この場合、コンパイルエラーが発生しますが、すぐに修正します。



  public Color Color { get { return HexMetrics.colors[terrainTypeIndex]; } // set { // … // } }
      
      





新しいプロパティを追加して、新しい標高タイプのインデックスを取得および設定します。



  public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; Refresh(); } } }
      
      





エディターのリファクタリング



HexMapEditor



内で、色に関するすべてのコードHexMapEditor



削除します。 これにより、コンパイルエラーが修正されます。



 // public Color[] colors; … // Color activeColor; … // bool applyColor; … // public void SelectColor (int index) { // applyColor = index >= 0; // if (applyColor) { // activeColor = colors[index]; // } // } … // void Awake () { // SelectColor(0); // } … void EditCell (HexCell cell) { if (cell) { // if (applyColor) { // cell.Color = activeColor; // } … } }
      
      





次に、アクティブな標高タイプインデックスを制御するフィールドとメソッドを追加します。



  int activeTerrainTypeIndex; … public void SetTerrainTypeIndex (int index) { activeTerrainTypeIndex = index; }
      
      





このメソッドは、現在欠落しているSelectColor



メソッドの代わりとして使用します。 UIのカラーウィジェットをSetTerrainTypeIndex



で接続し、他のすべてを変更しないままにします。 これは、負のインデックスがまだ使用中であり、色が変わらないことを意味します。



編集セルを変更して、編集中のセルに標高タイプのインデックスを割り当てます。



  void EditCell (HexCell cell) { if (cell) { if (activeTerrainTypeIndex >= 0) { cell.TerrainTypeIndex = activeTerrainTypeIndex; } … } }
      
      





セルから色データを削除しましたが、マップは以前と同じように機能するはずです。 唯一の違いは、デフォルトの色が配列の最初になったことです。 私の場合は黄色です。









黄色が新しいデフォルトの色です。



ユニティパッケージ



データをファイルに保存する



マップの保存と読み込みを制御するには、 HexMapEditor



を使用しHexMapEditor



。 これを行う2つのメソッドを作成し、今は空のままにします。



  public void Save () { } public void Load () { }
      
      





UIに2つのボタンを追加します( GameObject / UI / Button )。 それらをボタンに接続し、適切なラベルを付けます。 それらを右パネルの下部に配置しました。









[保存]ボタンと[読み込み]ボタン。



ファイルの場所



カードを保存するには、どこかに保存する必要があります。 ほとんどのゲームで行われているように、ファイルにデータを保存します。 しかし、このファイルをファイルシステムのどこに置くのでしょうか? 答えは、ゲームが実行されているオペレーティングシステムによって異なります。 各OSには、アプリケーションに関連するファイルを保存するための独自の標準があります。



これらの標準を知る必要はありません。 Unityは、 Application.persistentDataPath



取得できる正しいパスを知っていApplication.persistentDataPath



Save



メソッドで、コンソールに表示し、Playモードでボタンを押すと、どのようになるかを確認できます。



  public void Save () { Debug.Log(Application.persistentDataPath); }
      
      





デスクトップシステムでは、パスには会社と製品の名前が含まれます。 エディターとアセンブリの両方がこのパスを使用します。 名前は、 編集/プロジェクト設定/プレーヤーで設定できます。









会社と製品の名前。



Macでライブラリフォルダーが見つからないのはなぜですか?
多くの場合、 ライブラリフォルダーは非表示です。 表示方法はOS Xのバージョンによって異なります。古いバージョンがない場合は、Finderでホームフォルダーを選択し、[ 表示オプション表示]に進みますライブラリフォルダのチェックボックスがあります。


WebGLはどうですか?
WebGLゲームはユーザーのファイルシステムにアクセスできません。 代わりに、すべてのファイル操作がメモリ内にあるファイルシステムにリダイレクトされます。 彼女は私たちに透明です。 ただし、データを保存するには、Webページを手動で注文して、ブラウザストレージにデータをダンプする必要があります。


ファイル作成



ファイルを作成するには、 System.IO



名前空間のクラスを使用する必要があります。 したがって、 HexMapEditor



クラスの上にusing



ステートメントを追加します。



 using UnityEngine; using UnityEngine.EventSystems; using System.IO; public class HexMapEditor : MonoBehaviour { … }
      
      





まず、ファイルへのフルパスを作成する必要があります。 ファイルとしてtest.mapを使用します。 保存されたデータのパスに追加する必要があります。 スラッシュまたはバックスラッシュ(スラッシュまたはバックスラッシュ)を挿入する必要があるかどうかは、プラットフォームによって異なります。 Path.Combine



メソッドがPath.Combine



Path.Combine







  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); }
      
      





次に、この場所にあるファイルにアクセスする必要があります。 これはFile.Open



メソッドを使用してFile.Open



ます。 このファイルにデータを書き込むため、作成モードを使用する必要があります。 この場合、指定したパスに新しいファイルが作成されるか、既存のファイルが置き換えられます。



  string path = Path.Combine(Application.persistentDataPath, "test.map"); File.Open(path, FileMode.Create);
      
      





このメソッドを呼び出した結果は、このファイルに関連付けられたオープンデータストリームになります。 これを使用して、データをファイルに書き込むことができます。 そして、ストリームが不要になったときに閉じることを忘れてはなりません。



  string path = Path.Combine(Application.persistentDataPath, "test.map"); Stream fileStream = File.Open(path, FileMode.Create); fileStream.Close();
      
      





この段階で[ 保存 ]ボタンをクリックすると、 保存されたデータへのパスとして指定されたフォルダーにtest.mapファイルが作成されます。 このファイルを調べると、空になり、サイズが0バイトになります。これまでのところ何も書き込んでいません。



ファイルに書き込む



ファイルにデータを書き込むには、データをストリーミングする方法が必要です。 これを行う最も簡単な方法は、 BinaryWriter



です。 これらのオブジェクトを使用すると、プリミティブデータを任意のストリームに書き込むことができます。



新しいBinaryWriter



オブジェクトを作成すると、ファイルストリームが引数になります。 ライターを閉じると、使用するスレッドが閉じられます。 したがって、ストリームへの直接リンクを保存する必要がなくなりました。



  string path = Path.Combine(Application.persistentDataPath, "test.map"); BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Close();
      
      





データをストリームに転送するには、 BinaryWriter.Write



メソッドを使用できます。 整数や浮動小数点数など、すべてのプリミティブ型に対してWrite



メソッドのバリアントがあります。 行を記録することもできます。 整数123を書きましょう。



  BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Write(123); writer.Close();
      
      





[ 保存 ]ボタンをクリックして、 test.mapをもう一度調べます 。 整数サイズが4バイトであるため、サイズは4バイトになりました。



ファイルマネージャーが、ファイルがより多くのスペースを占有することを示すのはなぜですか?
ファイルシステムはスペースをバイトブロックに分割するためです。 個々のバイトを追跡しません。 test.mapはこれまで4バイトしか使用しないため、1ブロックのストレージスペースが必要です。


人間が読めるテキストではなく、バイナリデータを格納することに注意してください。 したがって、テキストエディターでファイルを開くと、不明瞭な文字のセットが表示されます。 おそらくシンボル{に続いて、何もないプレースホルダーがいくつか表示されます。



16進エディターでファイルを開くことができます。 この場合、 7b 00 00 00が表示されます。 これらは16進表記でマッピングされた4バイトの整数です。 通常の10進数では、これは123 0 0 0です。 バイナリでは、最初のバイトは01111011のようになります。



{のASCIIコードは123であるため、この文字はテキストエディターで表示できます。 ASCII 0は、表示される文字と一致しないヌル文字です。



残りの3バイトは、256未満の数字を書き込んだため、ゼロに等しくなります。256を書き込んだ場合、16進エディターに00 01 00 00が表示されます。



123は00 00 00 7bとして保存されるべきではありませんか?
BinaryWriter



は、リトルエンディアン形式を使用して数値を保存します。 つまり、最下位バイトが最初に書き込まれます。 この形式は、Microsoftが.Netフレームワークの開発に使用したものです。 Intel CPUがリトルエンディアン形式を使用しているため、おそらく選択されました。



それに代わるものはビッグエンディアンで、最上位バイトが最初に保存されます。 これは、数字の通常の数字の順序に対応します。 123はビッグエンディアンレコードを意味するため、123です。 リトルエンディアンの場合、123は312を意味します。


リソースを解放する



作家を閉じることが重要です。 ファイルシステムが開いている間、ファイルシステムはファイルをロックし、他のプロセスがファイルに書き込みできないようにします。 それを閉じるのを忘れると、自分自身もブロックします。 保存ボタンを2回押すと、2回目はストリームを開くことができません。



ライターを手動で閉じる代わりに、このためにusing



ブロックを作成できます。 ライターが有効な範囲を定義します。 実行可能コードがこの範囲を超えると、ライターが削除され、スレッドが閉じられます。



  using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(123); } // writer.Close();
      
      





これは、ライタークラスとファイルストリームクラスがIDisposable



インターフェイスを実装しているため機能します。 これらのオブジェクトにはDispose



メソッドがあります。このメソッドは、 using



の範囲外になると間接的に呼び出されます。



使用の大きな利点は、プログラムがどのようにスコープ外で実行されても動作することです。 早期の返品、例外、エラーは彼を悩ませません。 さらに、彼は非常に簡潔です。



データ検索



以前に書き込まれたデータを読み取るには、 Load



メソッドにコードを挿入する必要があります。 保存の場合と同様に、パスを作成してファイルストリームを開く必要があります。 違いは、ファイルを書き込み用ではなく読み取り用に開くことです。 そして、ライターの代わりにBinaryReader



が必要です。



  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)) ) { } }
      
      





この場合、 File.OpenRead



メソッドを使用して、読み取り用にファイルを開くことができます。



  using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { }
      
      





書き込み時にFile.OpenWriteを使用できないのはなぜですか?
このメソッドは、既存のファイルを置き換えるのではなく、既存のファイルにデータを追加するストリームを作成します。


読み取り時には、受信したデータのタイプを明示的に示す必要があります。 ストリームから整数を読み取るには、 BinaryReader.ReadInt32



を使用する必要がBinaryReader.ReadInt32



ます。 このメソッドは、32ビット整数、つまり4バイトを読み取ります。



  using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { Debug.Log(reader.ReadInt32()); }
      
      





123を受信すると 1バイトを読み取るだけで十分であることに注意してください。 ただし、同時に、この整数に属する3バイトがストリームに残ります。 また、これは、0〜255の範囲外の数値では機能しません。 したがって、そうしないでください。



ユニティパッケージ



地図データの書き込みと読み取り



データを保存する際の重要な質問は、人間が読める形式を使用するかどうかです。 通常、人間が読める形式では、JSON、XML、および何らかの構造のプレーンASCIIを使用します。 このようなファイルは、テキストエディターで開いたり、解釈したり、編集したりできます。 さらに、異なるアプリケーション間のデータ交換を簡素化します。



ただし、このような形式には独自の要件があります。 ファイルは、バイナリデータを使用するよりも多くのスペース(場合によってはそれ以上)を占有します。 また、ランタイムとメモリフットプリントの両方の点で、データのエンコードとデコードのコストを大幅に増加させる可能性があります。



対照的に、バイナリデータはコンパクトで高速です。 これは、大量のデータを記録する場合に重要です。 たとえば、ゲームの各ターンで大きなマップを自動保存する場合。 だから

バイナリ形式を使用します。 これを処理できれば、より詳細な形式で作業できます。



自動シリアル化はどうですか?
Unityデータをシリアル化するプロセスの直後に、シリアル化されたクラスをストリームに直接書き込むことができます。 個々のフィールドの記録の詳細は非表示になります。 ただし、セルを直接シリアル化することはできません。 これらは保存する必要のないデータを含むMonoBehaviour



クラスです。 したがって、オブジェクトの別の階層を使用する必要がありますが、これは自動シリアル化の単純さを破壊します。 さらに、将来のコード変更をサポートすることはより困難になります。 したがって、手動のシリアル化で完全な制御を維持します。 さらに、何が起こっているのかを本当に理解させてくれます。


マップをシリアル化するには、各セルのデータを保存する必要があります。 単一のセルを保存およびロードするには、 Save



およびLoad



メソッドをHexCell



追加しSave



。 ライターまたはリーダーが必要なため、パラメーターとして追加します。



 using UnityEngine; using System.IO; public class HexCell : MonoBehaviour { … public void Save (BinaryWriter writer) { } public void Load (BinaryReader reader) { } }
      
      





Save



およびLoad



メソッドをHexGrid



追加しSave



。 これらのメソッドは、 Load



メソッドとSave



メソッドを呼び出すことにより、すべてのセルを単純にバイパスしSave







 using UnityEngine; using UnityEngine.UI; using System.IO; public class HexGrid : MonoBehaviour { … public void Save (BinaryWriter writer) { for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } } public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } } }
      
      





マップをダウンロードする場合、セルデータが変更された後にマップを更新する必要があります。 これを行うには、すべてのフラグメントを更新するだけです。



  public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
      
      





最後に、 HexMapEditor



テストコードを、グリッドのSave



メソッドとLoad



メソッドの呼び出しに置き換え、ライターまたはリーダーを渡します。



  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { hexGrid.Save(writer); } } public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { hexGrid.Load(reader); } }
      
      





地形タイプの保存



現在の段階では、再保存は空のファイルを作成し、ダウンロードは何もしません。 HexCell



エレベーションタイプインデックスのみを記述およびロードすることから始めましょう。



値をterrainTypeIndexフィールドに直接割り当てます。 プロパティは使用しません。 すべてのフラグメントを明示的に更新するため、 Refresh



プロパティの呼び出しは必要ありません。 さらに、正しいマップのみを保存するため、ダウンロードしたすべてのマップも正しいと想定します。 したがって、たとえば、川や道路が許可されているかどうかはチェックしません。



  public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); }
      
      





このファイルに保存すると、すべてのセルの救済タイプのインデックスが次々と書き込まれます。 インデックスは整数なので、サイズは4バイトです。 私のカードには300個のセルが含まれています。つまり、ファイルサイズは1200バイトになります。



ロードは、インデックスが書き込まれるのと同じ順序でインデックスを読み取ります。 保存後にセルの色を変更した場合、マップをロードすると色が保存時の状態に戻ります。 もう何も保存しないため、残りのセルデータは同じままです。 つまり、ロードによって地形のタイプは変わりますが、その高さ、水位、地形の特徴などは変わりません。



すべての整数を保存する



レリーフタイプのインデックスを保存するだけでは十分ではありません。 他のすべてのデータを保存する必要があります。 すべての整数フィールドから始めましょう。 これは、起伏のタイプ、セルの高さ、水位、都市レベル、農場レベル、植生レベル、および特別なオブジェクトのインデックスのインデックスです。 それらは、記録されたのと同じ順序で読む必要があります。



  public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); }
      
      





これらの操作の間に変更を加えて、マップを保存してロードしてみてください。 セルの高さを除いて、保存されたデータに含まれるすべてのものが、可能な限り復元されました。 これは、高さレベルを変更するときに、セルの垂直位置を更新する必要があるために発生しました。 これは、フィールドではなく、ロードされた高さの値にプロパティに割り当てることで実行できます。 ただし、このプロパティは必要のない追加の作業を行います。 したがって、セルの位置を更新するコードをElevation



セッターから抽出し、別のRefreshPosition



メソッドに貼り付けましょう。 ここでの唯一の変更は、 value



elevation



フィールドへの参照に置き換えることvalue







  void RefreshPosition () { Vector3 position = transform.localPosition; position.y = elevation * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; }
      
      





これで、プロパティを設定するとき、および高さデータを読み込んだ後にメソッドを呼び出すことができます。



  public int Elevation { … set { if (elevation == value) { return; } elevation = value; RefreshPosition(); ValidateRivers(); … } } … public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); … }
      
      





この変更後、セルはロード時に見かけの高さを正しく変更します。



すべてのデータを保存する



セル内の壁と流入/流出河川の存在は、ブールフィールドに格納されます。 単純に整数として書くことができます。 さらに、道路データは、ループで書き込むことができる6つのブール値の配列です。



  public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write(hasOutgoingRiver); for (int i = 0; i < roads.Length; i++) { writer.Write(roads[i]); } }
      
      





流入河川と流出河川の方向は、 HexDirection



フィールドに保存されます。 HexDirection



型は、複数の整数値として内部的に保存される列挙です。 したがって、明示的な変換を使用して整数としてシリアル化することもできます。



  writer.Write(hasIncomingRiver); writer.Write((int)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((int)outgoingRiver);
      
      





ブール値はBinaryReader.ReadBoolean



メソッドを使用して読み取られます。 川の方向は整数です。これをHexDirection



変換し直す必要があります。



  public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadInt32(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadInt32(); for (int i = 0; i < roads.Length; i++) { roads[i] = reader.ReadBoolean(); } }
      
      





次に、マップの完全な保存と復元に必要なすべてのセルデータを保存します。これには、セルごとに9つの整数と9つのブール値が必要です。各ブール値は1バイトを使用するため、セルごとに合計45バイトを使用します。つまり、300セルのカードには合計13,500バイトが必要です。



ユニティパッケージ



ファイルサイズを小さくする



300セルの場合、13,500バイトはそれほど多くないように見えますが、おそらくもっと少ない量で実行できます。最終的に、データのシリアル化方法を完全に制御できます。それらを保存するよりコンパクトな方法があるかどうかを見てみましょう。



数値間隔の削減



異なるセルレベルとインデックスは整数として保存されます。ただし、使用する値の範囲はごくわずかです。それぞれが0〜255の範囲に確実に留まります。これは、各整数の最初のバイトのみが使用されることを意味します。他の3つは常にゼロになります。これらの空のバイトを保存することは意味がありません。ストリームに書き込む前に整数をバイトに書き込むことにより、それらを破棄できます。



  writer.Write((byte)terrainTypeIndex); writer.Write((byte)elevation); writer.Write((byte)waterLevel); writer.Write((byte)urbanLevel); writer.Write((byte)farmLevel); writer.Write((byte)plantLevel); writer.Write((byte)specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write((byte)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((byte)outgoingRiver);
      
      





さて、これらの数値を返すには、を使用する必要がありますBinaryReader.ReadByte



バイトから整数への変換は暗黙的に行われるため、明示的な変換を追加する必要はありません。



  terrainTypeIndex = reader.ReadByte(); elevation = reader.ReadByte(); RefreshPosition(); waterLevel = reader.ReadByte(); urbanLevel = reader.ReadByte(); farmLevel = reader.ReadByte(); plantLevel = reader.ReadByte(); specialIndex = reader.ReadByte(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadByte(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadByte();
      
      





したがって、整数ごとに3バイトを取り除き、セルごとに27バイトを節約します。セルあたり18バイト、300セルあたりわずか5,400バイトを使用します。



この段階で古いカードのデータが無意味になることは注目に値します。古いセーブをロードすると、データが混在していることがわかり、セルが混乱します。これは、現在読み取り中のデータが少ないためです。以前よりも多くのデータを読み取ると、ファイルの末尾を超えて読み取ろうとするとエラーが発生します。



古いデータを処理できないことは、私たちに適しています。なぜなら、私たちはフォーマットを決定する過程にあるからです。ただし、保存形式を決定するときは、将来のコードで常に保存形式を読み取れるようにする必要があります。形式を変更しても、理想的には古い形式を読み取ることができるはずです。



River Byte Union



この段階では、4バイトを使用して川のデータを保存します(方向ごとに2バイト)。方向ごとに、川の存在と流れる



方向を保存しますが、川の方向を保存する必要がないのは明らかです。これは、川のないセルに必要なバイト数が2バイト少ないことを意味します。実際、その存在に関係なく、川の方向に1バイトあれば十分です。



可能な方向は6つあり、0〜5の間隔で数値として保存されます。これには3ビットで十分です。これは、バイナリ形式では0〜5の数値が000、001、010、011、100、101、110のように見えるためです。つまり、1バイトでさらに5ビットが使用されません。それらの1つを使用して、川が存在するかどうかを示すことができます。たとえば、数値128に対応する8番目のビットを使用でき



ます。これを行うには、方向をバイトに変換する前に128を追加します。つまり、北西に流れる川がある場合、133を書き込みます。川がない場合は、ゼロバイトを書き込むだけです。



同時に、さらに4ビットが未使用のままですが、これは正常です。川の両方向を1バイトにまとめることができますが、これは既に混乱しすぎます。



 // writer.Write(hasIncomingRiver); // writer.Write((byte)incomingRiver); if (hasIncomingRiver) { writer.Write((byte)(incomingRiver + 128)); } else { writer.Write((byte)0); } // writer.Write(hasOutgoingRiver); // writer.Write((byte)outgoingRiver); if (hasOutgoingRiver) { writer.Write((byte)(outgoingRiver + 128)); } else { writer.Write((byte)0); }
      
      





riverデータをデコードするには、最初にバイトを読み戻す必要があります。値が128以上の場合、これは川があることを意味します。方向を取得するには、128を減算してからに変換しHexDirection



ます。



 // hasIncomingRiver = reader.ReadBoolean(); // incomingRiver = (HexDirection)reader.ReadByte(); byte riverData = reader.ReadByte(); if (riverData >= 128) { hasIncomingRiver = true; incomingRiver = (HexDirection)(riverData - 128); } else { hasIncomingRiver = false; } // hasOutgoingRiver = reader.ReadBoolean(); // outgoingRiver = (HexDirection)reader.ReadByte(); riverData = reader.ReadByte(); if (riverData >= 128) { hasOutgoingRiver = true; outgoingRiver = (HexDirection)(riverData - 128); } else { hasOutgoingRiver = false; }
      
      





その結果、セルごとに16バイトを得ました。改善はそれほど大きくないように見えますが、これはバイナリデータのサイズを小さくするために使用されるトリックの1つです。



道路を1バイトで保存する



同様の手法を使用して、道路データを圧縮できます。バイトの最初の6ビットに格納できる6つのブール値があります。つまり、道路の各方向は2の累乗の数値で表されます。これらは、1、2、4、8、16、32、またはバイナリ形式の1、10、100、1000、10000、100000です。



完成したバイトを作成するには、道路の使用方向に対応するビットを設定する必要があります。方向の正しい方向を取得するには、演算子を使用できます<<



次に、ビットごとのOR演算子を使用してそれらを結合します。たとえば、1番目、2番目、3番目、6番目の道路が使用されている場合、完成したバイトは100111になります。



  int roadFlags = 0; for (int i = 0; i < roads.Length; i++) { // writer.Write(roads[i]); if (roads[i]) { roadFlags |= 1 << i; } } writer.Write((byte)roadFlags);
      
      





<<はどのように機能しますか?
これはビット単位の左シフト演算子です。左側に整数を取り、合計ビットを左側にシフトします。オーバーフローは破棄されました。シフトステップの数は、右側の整数によって決まります。数値は2進数であるため、すべてのビットを1ステップ左にシフトすると、数値の値が2倍になります。つまり、1 << n



2 nが得られ、これが必要です。


道路のブール値を取得するには、ビットが設定されているかどうかを確認する必要があります。その場合、ビット単位のAND演算子を適切な数で使用して、他のすべてのビットをマスクします。結果がゼロに等しくない場合、ビットが設定され、道路が存在します。



  int roadFlags = reader.ReadByte(); for (int i = 0; i < roads.Length; i++) { roads[i] = (roadFlags & (1 << i)) != 0; }
      
      





6バイトを1つに圧縮すると、セルごとに11バイトを受け取りました。300セルの場合、これはわずか3,300バイトです。つまり、バイトを少し使用して、ファイルサイズを75%削減しました。



未来への準備



保存形式の完了を宣言する前に、もう1つの詳細を追加します。マップデータを保存する前にHexMapEditor



、整数のゼロを強制的に書き込みます。



  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(0); hexGrid.Save(writer); } }
      
      





これにより、データの先頭に4つの空のバイトが追加されます。つまり、カードをロードする前に、これらの4バイトを読み取る必要があります。



  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { reader.ReadInt32(); hexGrid.Load(reader); } }
      
      





これらのバイトはこれまでのところ役に立たないが、将来的に後方互換性を提供するヘッダーとして使用されます。これらのヌルバイトを追加していない場合、最初の数バイトの内容はマップの最初のセルに依存していました。したがって、将来、どのバージョンの保存形式を扱っているかを把握することはより困難になります。これで、最初の4バイトを確認できます。それらが空の場合、フォーマット0のバージョンを扱っています。将来のバージョンでは、そこに何か他のものを追加することが可能になります。



つまり、タイトルがゼロ以外の場合、不明なバージョンを処理しています。どのデータがあるのか​​わからないため、マップのダウンロードを拒否する必要があります。



  using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader); } else { Debug.LogWarning("Unknown map format " + header); } }
      
      







ユニティパッケージ



パート13:カード管理





このパートでは、さまざまなサイズのカードのサポートを追加し、さまざまなファイルを保存します。



このパートから始めて、チュートリアルはUnity 5.5.0で作成されます。









マップライブラリの始まり



新しいマップを作成する



ここまでで、六角形のグリッドを一度だけ作成しました-シーンをロードするとき。これで、いつでも新しいマップを開始できるようになります。新しいカードは、単に現在のカードを置き換えます。



Awake HexGrid



では、いくつかのメトリックが初期化され、次にセルの数が決定され、必要なフラグメントとセルが作成されます。フラグメントとセルの新しいセットを作成して、新しいマップを作成します。HexGrid.Awake



初期化ソースコードと一般的なメソッドの2つの部分に分けましょうCreateMap







  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(); } public void CreateMap () { cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
      
      





UIにボタンを追加して、新しいマップを作成します。大きくして、保存ボタンと読み込みボタンの下に配置しました。









新しいマップボタン。 このボタンのOn Click



イベントをobjectのメソッドに接続しましょうつまり、Hex Map Editorを使用するのではなくHex Gridオブジェクトメソッドを直接呼び出しますCreateMap



HexGrid













クリックして地図を作成します。



古いデータを消去する



新しいマップボタンをクリックすると、新しいフラグメントとセルのセットが作成されます。ただし、古いものは自動的に削除されません。したがって、結果として、複数のマップメッシュが相互に重ねられます。これを回避するには、最初に古いオブジェクトを削除する必要があります。これは、最初にすべての現在のフラグメントを破棄することで実行できますCreateMap







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





既存のオブジェクトを再利用できますか?
可能ですが、新しいフラグメントとセルから始める最も簡単な方法です。これは、さまざまなサイズのカードのサポートを追加する場合に特に当てはまります。さらに、新しいマップの作成は比較的まれなアクションであり、最適化はここではあまり重要ではありません。


このような子要素をループで破棄することは可能ですか?
もちろん。 .


フラグメントではなくセルでサイズを指定します



フィールドchunkCountX



chunkCountZ



オブジェクトを使用してマップのサイズを設定ますHexGrid



ただし、セルのマップのサイズを示す方がはるかに便利です。同時に、カードのサイズを変更せずに、フラグメントのサイズを将来変更することもできます。したがって、セル数とフラグメントフィールド数の役割を交換しましょう。



 // public int chunkCountX = 4, chunkCountZ = 3; public int cellCountX = 20, cellCountZ = 15; … // int cellCountX, cellCountZ; int chunkCountX, chunkCountZ; … public void CreateMap () { … // cellCountX = chunkCountX * HexMetrics.chunkSizeX; // cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
      
      





これは、HexMapCamera



フラグメントサイズ使用して位置を制限するため、コンパイルエラーが発生しますHexMapCamera.ClampPosition



彼がまだ必要な数のセルを直接使用するように変更します。



  Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
      
      





フラグメントのサイズは5 x 5セルで、デフォルトのマップのサイズは4 x 3フラグメントです。したがって、カードを同じままにするには、20 x 15セルのサイズを使用する必要があります。また、コード内でデフォルト値を割り当てましたが、フィールドはすでに存在し、デフォルトで0に設定されているため、グリッドオブジェクトはまだそれらを自動的に使用しません。









デフォルトでは、カードのサイズは20 x 15です。



カスタムカードサイズ



次のステップは、デフォルトのサイズだけでなく、あらゆるサイズのカードの作成をサポートすることです。これを行うには、HexGrid.CreateMap



パラメーターにXとZ を追加すると、既存のセル数が置き換えられます。内部でAwake



は、現在のセル数で呼び出します。



  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } public void CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
      
      





ただし、これは、フラグメントサイズの倍数であるセルの数でのみ正しく機能します。そうしないと、整数除算で作成されるフラグメントが少なすぎます。セルで部分的に満たされたフラグメントのサポートを追加できますが、フラグメントに対応しないサイズの使用を禁止してみましょう。



演算子%



使用して、セルの数をフラグメントの数で割った余りを計算できますゼロに等しくない場合、矛盾があり、新しいマップを作成しません。そして、これを行っている間に、ゼロおよび負のサイズに対する保護を追加しましょう。



  public void CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return; } … }
      
      





新しいカードメニュー



現在の段階では、メソッドには2つのパラメーターがあるため、[ 新しいマップ ]ボタンは機能しなくなりましたHexGrid.CreateMap



。 Unityイベントをそのようなメソッドに直接接続することはできません。さらに、さまざまなサイズのカードをサポートするには、いくつかのボタンが必要です。これらすべてのボタンをメインUIに追加する代わりに、個別のポップアップメニューを作成しましょう。



シーンに新しいキャンバスを追加します(GameObject / UI / Canvas)。既存のキャンバスと同じ設定を使用しますが、並べ替え順序は1にする必要があります。これにより、メインエディターのUIの上部に表示されます。新しいUIオブジェクトのキャンバスとイベントシステムの両方の子を作成して、シーンの階層がきれいに保たれるようにしました。















キャンバスメニューの新しいマップ。 画面全体を閉じるパネル



新しいマップメニューに追加します。背景を暗くして、メニューが開いているときにカーソルが他のすべてと対話できないようにする必要があります。Source Imageをクリアして均一な色を与えColorとして(0、0、0、200)を設定しました









背景画像の設定。 16進マップエディターの



パネルと同様に、キャンバスの中央にメニューバーを追加します彼女の小、中、大のカード用の明確なラベルとボタンを作成しましょう。プレイヤーが気が変わった場合に備えて、キャンセルボタンも追加します。デザインの作成が完了したら、新しいマップメニュー全体を無効にします















新しいマップメニュー。



メニューを管理するには、コンポーネントNewMapMenu



作成し、キャンバスの新しいマップメニューオブジェクトに追加します新しいマップを作成するには、Hex Gridオブジェクトにアクセスする必要がありますしたがって、共通フィールドを追加して接続します。



 using UnityEngine; public class NewMapMenu : MonoBehaviour { public HexGrid hexGrid; }
      
      











新しいマップメニューのコンポーネント。



開閉



キャンバスオブジェクトをアクティブ化および非アクティブ化するだけで、ポップアップメニューを開いたり閉じたりできます。NewMapMenu



これを行うための2つの一般的なメソッドを追加しましょう



  public void Open () { gameObject.SetActive(true); } public void Close () { gameObject.SetActive(false); }
      
      





次に、エディターの[ 新しいマップ UI ]ボタンを[新しいマップメニュー]Open



オブジェクトのメソッド接続します









を押してメニューを開きます。



また、[ キャンセル ]ボタンをメソッドに接続しますClose



これにより、ポップアップメニューを開いたり閉じたりできます。



新しいマップを作成する



新しいマップを作成するには、Hex Gridオブジェクトのメソッドを呼び出す必要がありますCreateMap



さらに、その後、ポップアップメニューを閉じる必要があります。NewMapMenu



任意のサイズを考慮して、これに対処するメソッドに追加します。



  void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); Close(); }
      
      





このメソッドは、ボタンイベントに直接接続できないため、一般的ではありません。代わりに、CreateMap



指定されたサイズで呼び出すボタンごとに1つのメソッドを作成します。小さなマップの場合、マップのデフォルトサイズに対応する20 x 15のサイズを使用しました。中央のカードについては、このサイズを2倍にして40 x 30にし、大きなカードの場合は再び2倍にすることにしました。適切な方法でボタンを接続します。



  public void CreateSmallMap () { CreateMap(20, 15); } public void CreateMediumMap () { CreateMap(40, 30); } public void CreateLargeMap () { CreateMap(80, 60); }
      
      





カメラロック



これで、ポップアップメニューを使用して、3つの異なるサイズの新しいカードを作成できます。すべてうまくいきますが、少し詳細に注意する必要があります。ときに新規マップメニューがアクティブになって、私たちはUIエディタおよび編集細胞と、もはや対話することができます。ただし、カメラを制御することはできます。理想的には、メニューを開いた状態でカメラをロックする必要があります。



カメラは1台しかないため、簡単で実用的な解決策は、静的プロパティを追加することLocked



です。広範囲に使用する場合、このソリューションはあまり適していませんが、シンプルなインターフェイスでは十分です。これには、内部の静的インスタンスを追跡する必要がありますHexMapCamera



。これは、Awakeカメラが設定されたときに設定されます。



  static HexMapCamera instance; … void Awake () { instance = this; swivel = transform.GetChild(0); stick = swivel.GetChild(0); }
      
      





プロパティLocked



は、セッターのみを使用した単純な静的ブールプロパティにすることができます。HexMapCamera



ロックされるとインスタンスオフになり、ロックが解除されるとオンになります。



  public static bool Locked { set { instance.enabled = !value; } }
      
      





これでNewMapMenu.Open



カメラをブロックでき、NewMapMenu.Close



ロックを解除できます。



  public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; }
      
      





正しいカメラ位置を維持する



カメラに別の問題がある可能性があります。現在のマップよりも小さい新しいマップを作成すると、カメラがマップの境界の外側に表示される場合があります。プレーヤーがカメラを動かそうとするまで、彼女はそこに残ります。そして、新しいマップの制限によって制限されます。



この問題を解決するために、HexMapCamera



静的メソッドに追加できValidatePosition



ます。AdjustPosition



ゼロオフセットでインスタンスメソッド呼び出すと、カメラがマップの境界に強制的に移動します。カメラが既に新しいマップの境界内にある場合、カメラは所定の位置に残ります。



  public static void ValidatePosition () { instance.AdjustPosition(0f, 0f); }
      
      





NewMapMenu.CreateMap



新しいマップを作成した後、内部でメソッドを呼び出します。



  void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); HexMapCamera.ValidatePosition(); Close(); }
      
      





ユニティパッケージ



マップサイズの保存



さまざまなサイズのカードを作成できますが、保存や読み込みの際には考慮されません。これは、現在のマップのサイズがロードされたマップのサイズと一致しない場合、マップをロードするとエラーまたは誤ったマップが発生することを意味します。



この問題を解決するには、セルデータを読み込む前に、適切なサイズの新しいマップを作成する必要があります。小さな地図が保存されているとします。この場合、最初にHexGrid.Load



20 x 15のマップを作成すれば、すべてうまくいきます



  public void Load (BinaryReader reader) { CreateMap(20, 15); for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
      
      





カードサイズの保管



もちろん、どんなサイズのカードでも保存できます。したがって、一般的な解決策は、これらのセルの前にマップのサイズを保存することです。



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





次に、実際のサイズを取得し、それを使用して正しいサイズのマップを作成できます。



  public void Load (BinaryReader reader) { CreateMap(reader.ReadInt32(), reader.ReadInt32()); … }
      
      





さまざまなサイズのマップをロードできるようになったため、カメラの位置の問題に再び直面しています。HexMapEditor.Load



地図を読み込んだ後、その位置をチェックインすることで解決します。



  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
      
      





新しいファイル形式



このアプローチは、今後保持するカードで機能しますが、古いカードでは機能しません。またその逆-チュートリアルの前の部分のコードは、新しいマップファイルを正しくロードできません。古い形式と新しい形式を区別するために、ヘッダーの整数値を増やします。マップサイズのない古い保存形式はバージョン0でした。マップサイズの新しい形式はバージョン1になります。したがって、記録するときHexMapEditor.Save



、0ではなく1を書き込む必要があります。



  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } }
      
      





これ以降、マップはバージョン1として保存されます。前のチュートリアルのアセンブリでマップを開こうとすると、不明なマップ形式のロードとレポートが拒否されます。実際、このようなカードを既にロードしようとすると、これが発生します。HexMapEditor.Load



新しいバージョンを受け入れるようにメソッドを変更する必要があります。



  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 1) { hexGrid.Load(reader); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
      
      





下位互換性



実際、必要に応じて、バージョン0のマップをダウンロードできます。マップのサイズはすべて20 x 15であると仮定します。つまり、タイトルは1である必要はなく、ゼロでもかまいません。各バージョンには独自のアプローチが必要であるためHexMapEditor.Load



、ヘッダーをmethodに渡す必要がありHexGrid.Load



ます。



  if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); }
      
      





HexGrid.Load



パラメータにタイトルを追加し、それを使用して、さらにアクションを決定します。ヘッダーが1以上の場合、カードサイズデータを読み取る必要があります。それ以外の場合、20 x 15の古い固定カードサイズを使用し、サイズデータの読み取りをスキップします。



  public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } CreateMap(x, z); … }
      
      





マップファイルバージョン0



カードサイズの確認



新しいマップの作成と同様に、フラグメントのサイズと互換性のないマップをロードする必要がある可能性が理論的にあります。この場合、カードのダウンロードを中断する必要があります。HexGrid.CreateMap



すでにマップの作成を拒否し、コンソールにエラーを表示します。これをメソッドの呼び出し元に伝えるために、マップが作成されたかどうかを伝えるブール値を返しましょう。



  public bool CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return false; } … return true; }
      
      





さてHexGrid.Load



、あまりにも、マップを作成するには、失敗した場合に実行を停止することができます。



  public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } if (!CreateMap(x, z)) { return; } … }
      
      





ロードすると既存のセルのすべてのデータが上書きされるため、同じサイズのマップがロードされる場合、新しいマップを作成する必要はありません。したがって、この手順はスキップできます。



  if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z)) { return; } }
      
      





ユニティパッケージ



ファイル管理



サイズの異なるカードを保存およびロードできますが、常にtest.mapを読み書きします次に、さまざまなファイルのサポートを追加します。



マップを直接保存またはロードする代わりに、高度なファイル管理を提供する別のポップアップメニューを使用します。New Map Menuのように別のキャンバスを作成しますが、今回はSave Load Menuと呼びますこのメニューは、マップを開いて押すボタンに応じて、マップを保存およびロードします。Save Load Menu



デザインを作成します保存メニューのように。後でブートメニューに動的に変更します。別のメニューと同様に、背景とメニューバー、メニューラベル、キャンセルボタンが必要です。次に、メニューにスクロールビュー(GameObject / UI / Scroll View)を追加して、ファイルのリストを表示します。以下に入力フィールド(GameObject / UI / Input Fieldを挿入して、新しいカードの名前を示します。マップを保存するためのアクションボタンも必要です。そして最後に。削除ボタン追加して、不要なカードを削除します。















[保存メニューの保存]を設計します。



デフォルトでは、スクロールビューでは水平スクロールと垂直スクロールの両方が可能ですが、必要なのは垂直スクロールのリストのみです。そのため、無効にスクロール水平および水平スクロールバー抜いてください。また、Movement Typeをclampedに設定しInertia無効にしてリストの制限を強化します。









ファイルリストオプション。 File ListオブジェクトからScrollbar Horizo​​ntal



子を削除します。これは必要ないためです。次に、Scrollbar Verticalのサイズを変更して、リストの下部に到達します。Name Inputオブジェクトのプレースホルダーテキストは、その子Placeholderで変更できます説明テキストを使用しましたが、空白のままにしてプレースホルダーを削除できます。













メニューのデザインを変更しました。



デザインが完了し、デフォルトでメニューが非表示になるようにメニューを無効にします。



メニュー管理



メニューを機能させるには、別のスクリプト、この場合-が必要SaveLoadMenu



です。のようNewMapMenu



に、グリッドへのリンク、メソッドOpen



、およびが必要Close



です。



 using UnityEngine; public class SaveLoadMenu : MonoBehaviour { public HexGrid hexGrid; public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; } }
      
      





このコンポーネントをSaveLoadMenuに追加し、グリッドオブジェクトへのリンクを与えます。









コンポーネントSaveLoadMenu。



保存またはロードするメニューが開きます。作業を簡素化するには、Open



ブールパラメータをmethodに追加します。メニューを保存モードにするかどうかを決定します。このモードをフィールドで追跡して、後で実行するアクションを確認します。



  bool saveMode; public void Open (bool saveMode) { this.saveMode = saveMode; gameObject.SetActive(true); HexMapCamera.Locked = true; }
      
      





今すぐボタン組み合わせる保存読み込みオブジェクトヘックスマップエディタをメソッドでOpen



オブジェクトの保存は、メニューをロードします[ 保存 ]ボタンのみのブールパラメータを確認します









保存モードでメニューを開きます。



まだ行っていない場合は、キャンセルボタンイベントをメソッドに接続しますClose



セーブロードメニューを開閉することができます。



外観の変化



メニューを保存メニューとして作成しましたが、モードはボタンを押して開くことで決定されます。モードに応じてメニューの外観を変更する必要があります。特に、メニューラベルとアクションボタンラベルを変更する必要があります。これは、これらのタグへのリンクが必要になることを意味します。



 using UnityEngine; using UnityEngine.UI; public class SaveLoadMenu : MonoBehaviour { public Text menuLabel, actionButtonLabel; … }
      
      











タグとの接続。



メニューが保存モードで開くと、既存のラベル、つまり、メニューのマップの保存とアクションボタンの保存が使用されます。それ以外の場合は、ロードモードになります。つまり、Load MapLoadを使用します



  public void Open (bool saveMode) { this.saveMode = saveMode; if (saveMode) { menuLabel.text = "Save Map"; actionButtonLabel.text = "Save"; } else { menuLabel.text = "Load Map"; actionButtonLabel.text = "Load"; } gameObject.SetActive(true); HexMapCamera.Locked = true; }
      
      





カード名を入力してください



ファイルのリストは今のところ残しておきましょう。ユーザーは、入力フィールドにカードの名前を入力して、保存またはダウンロードしたファイルを指定できます。このデータを取得するにInputField



は、Name Inputオブジェクトのコンポーネントへの参照が必要です。



  public InputField nameInput;
      
      











入力フィールドへの接続。



ユーザーは、マップファイルへのフルパスを入力する必要はありません。.map拡張子のないマップ名のみで十分ですユーザー入力を受け取り、その正しいパスを作成するメソッドを追加しましょう。入力が空の場合、これは不可能なので、この場合はを返しnull



ます。



 using UnityEngine; using UnityEngine.UI; using System.IO; public class SaveLoadMenu : MonoBehaviour { … string GetSelectedPath () { string mapName = nameInput.text; if (mapName.Length == 0) { return null; } return Path.Combine(Application.persistentDataPath, mapName + ".map"); } }
      
      





ユーザーが無効な文字を入力するとどうなりますか?
, . , , .



Content Type . , - , . , , .


保存と読み込み



これで、保存と読み込みが行われSaveLoadMenu



ます。したがって、我々は、メソッドを移動Save



してLoad



HexMapEditor



の中でSaveLoadMenu



それらはもはや共有される必要がなく、固定パスの代わりにパスパラメータで動作します。



  void Save (string path) { // string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } } void Load (string path) { // string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
      
      





現在、任意のファイルをアップロードしているので、ファイルが実際に存在することを確認してから、それを読み取ろうとするとよいでしょう。そうでない場合は、エラーをスローして操作を終了します。



  void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } … }
      
      





次に、一般的なメソッドを追加しAction



ます。ユーザーが選択したパスを取得することから始まります。パスがある場合は、保存またはロードします。次にメニューを閉じます。



  public void Action () { string path = GetSelectedPath(); if (path == null) { return; } if (saveMode) { Save(path); } else { Load(path); } Close(); }
      
      





このメソッドにAction Buttonイベントをアタッチすることにより、任意のマップ名を使用して保存およびロードできます。入力フィールドをリセットしないため、選択した名前は次の保存またはロードまで残ります。これは、1つのファイルから連続して複数回保存またはロードするのに便利なので、何も変更しません。



マップリストアイテム



次に、ファイルのリストに、データストレージパス上にあるすべてのカードを入力します。リスト内のいずれかのアイテムをクリックすると、名前入力のテキストとして使用されますSaveLoadMenu



このための一般的な方法を追加します。



  public void SelectItem (string name) { nameInput.text = name; }
      
      





リストアイテムが必要です。通常のボタンでできます。作成し、高さを20単位に減らして、垂直方向に多くのスペースを占有しないようにします。ボタンのように見えるべきではないのでImageコンポーネントのSource Imageリンクをクリアしますこの場合、完全に白になります。さらに、ラベルが左に揃えられ、テキストとボタンの左側の間にスペースがあることを確認します。ボタンの設計が完了したら、それをプレハブに変えます。















ボタンはリスト項目です。



ボタンイベントを新しいマップメニュー直接接続することはできません。これは、プレハブであり、まだシーンに存在しないためです。したがって、クリックされSelectItem



ときにメソッドを呼び出すことができるように、メニュー項目にはメニューへのリンクが必要です。彼はまた、彼が代表するカードの名前を追跡し、テキストを設定する必要があります。このための小さなコンポーネントを作成しましょうSaveLoadItem







 using UnityEngine; using UnityEngine.UI; public class SaveLoadItem : MonoBehaviour { public SaveLoadMenu menu; public string MapName { get { return mapName; } set { mapName = value; transform.GetChild(0).GetComponent<Text>().text = value; } } string mapName; public void Select () { menu.SelectItem(mapName); } }
      
      





コンポーネントをメニュー項目に追加し、ボタンがそのメソッドを呼び出すようにしますSelect













アイテムコンポーネント。



リスト塗りつぶし



リストを作成SaveLoadMenu



するには、ファイルリストオブジェクトのビューポート内のコンテンツへのリンク必要です。また、アイテムのプレハブへのリンクも必要です。



  public RectTransform listContent; public SaveLoadItem itemPrefab;
      
      











リストのコンテンツとプレハブをミックスします。



新しいメソッドを使用して、このリストを作成します。最初のステップは、既存のマップファイルを識別することです。ディレクトリ内のすべてのファイルパスの配列を取得するには、メソッドを使用できますDirectory.GetFiles



このメソッドには、ファイルをフィルターできる2番目のパラメーターがあります。この場合、* .mapマスクに一致するファイルのみが必要です



  void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); }
      
      





残念ながら、ファイルの順序は保証されていません。それらをアルファベット順に表示するには、配列をでソートする必要がありますSystem.Array.Sort







 using UnityEngine; using UnityEngine.UI; using System; using System.IO; public class SaveLoadMenu : MonoBehaviour { … void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); Array.Sort(paths); } … }
      
      





次に、配列の各要素に対してプレハブインスタンスを作成します。アイテムをメニューにバインドし、マップ名を設定して、リストのコンテンツの子にします。



  Array.Sort(paths); for (int i = 0; i < paths.Length; i++) { SaveLoadItem item = Instantiate(itemPrefab); item.menu = this; item.MapName = paths[i]; item.transform.SetParent(listContent, false); }
      
      





以来Directory.GetFiles



、ファイルへのフルパス戻り、私たちはそれらをきれいにする必要があります。幸いなことに、これはまさに便利なメソッドを作るものPath.GetFileNameWithoutExtension



です。



  item.MapName = Path.GetFileNameWithoutExtension(paths[i]);
      
      





メニューを表示する前に、リストに記入する必要があります。また、ファイルは変更される可能性が高いため、メニューを開くたびにこれを行う必要があります。



  public void Open (bool saveMode) { … FillList(); gameObject.SetActive(true); HexMapCamera.Locked = true; }
      
      





リストを再入力する場合、新しいアイテムを追加する前に古いものをすべて削除する必要があります。



  void FillList () { for (int i = 0; i < listContent.childCount; i++) { Destroy(listContent.GetChild(i).gameObject); } … }
      
      











アレンジなしのアイテム。



ポイントの配置



これでリストに項目が表示されますが、それらは重なって不適切な位置にあります。それらを垂直リストに変換するには、垂直レイアウトグループコンポーネントコンポーネント/レイアウト/垂直レイアウトグループリストのコンテンツオブジェクトに追加します。配置が正しく機能するようにするには、「子コントロールサイズ」と「子強制拡張」の両方の有効にします。両方の高さオプションを無効にする必要があります。



















垂直レイアウトグループを使用します。



美しいアイテムのリストを入手しました。ただし、リストのコンテンツのサイズはアイテムの実際の数に調整されません。したがって、スクロールバーのサイズは変更されません。私たちは、作ることができるコンテンツが自動的にコンポーネント加えることにより、サイズ変更、コンテンツのサイズフィッターコンポーネント/レイアウト/コンテンツサイズフィッタを)。その垂直フィットモードPreferred Sizeに設定する必要があります















コンテンツサイズフィッターを使用します。



少数のポイントで、スクロールバーが消えます。また、リスト内のビューポートに収まらないアイテムが多すぎる場合、スクロールバーが表示され、適切なサイズになります。









スクロールバーが表示されます。



カードの削除



これで、多くのマップファイルを簡単に操作できます。ただし、場合によっては一部のカードを取り除く必要があります。これを行うには、削除ボタンを使用できますこのためのメソッドを作成し、ボタンから呼び出してみましょう。選択したパスがある場合は、単にで削除しFile.Delete



ます。



  public void Delete () { string path = GetSelectedPath(); if (path == null) { return; } File.Delete(path); }
      
      





ここでは、実際に存在するファイルで作業していることも確認する必要があります。そうでない場合は、削除しようとするべきではありませんが、エラーにはなりません。



  if (File.Exists(path)) { File.Delete(path); }
      
      





カードを取り外した後、メニューを閉じる必要はありません。これにより、一度に複数のファイルを簡単に削除できます。ただし、削除後は、名前の入力をクリアするとともに、ファイルのリストを更新する必要があります。



  if (File.Exists(path)) { File.Delete(path); } nameInput.text = ""; FillList();
      
      





ユニティパッケージ



パート14:レリーフテクスチャ





この瞬間まで、私たちはカードを着色するために単色を使用していました。次に、テクスチャを適用します。









テクスチャの描画。



3種類の混合物



均一な色は明らかに識別可能で、タスクに完全に対応していますが、あまり面白く見えません。テクスチャを使用すると、マップの魅力が大幅に向上します。もちろん、このためには色だけでなくテクスチャを混ぜる必要があります。Rendering 3チュートリアル、テクスチャの結合で、スプラットマップを使用して複数のテクスチャを混合する方法について説明しました。六角形のマップでは、同様のアプローチを使用できます。Rendering 3



チュートリアル4つのテクスチャのみが混合され、1つのスプラットマップで最大5つのテクスチャをサポートできます。現時点では5つの異なる色を使用しているため、これは非常に適しています。ただし、後で他のタイプを追加できます。したがって、任意の数の救済タイプのサポートが必要です。テクスチャプロパティを明示的に設定して使用する場合、これは不可能であるため、テクスチャの配列を使用する必要があります。後で作成します。



テクスチャ配列を使用する場合、シェーダーに混合するテクスチャを何らかの方法で伝える必要があります。最も難しいミキシングは、三角形の三角形に必要です。三角形は、独自のタイプの地形を持つ3つのセルの間にある場合があります。したがって、三角形ごとに3つのタイプの混合サポートが必要です。



スプラットマップとして頂点カラーを使用する



どのテクスチャを混合するかを伝えることができると仮定すると、頂点カラーを使用して各三角形のスプラットマップを作成できます。いずれの場合も最大3つのテクスチャが使用されるため、必要なカラーチャネルは3つだけです。赤は最初のテクスチャ、緑は2番目、青は3番目のテクスチャを表します。









三角形のスプラットマップ。



三角形のスプラットマップの合計は常に1に等しいですか?
はい . . , (1, 0, 0) , (½, ½, 0) (&frac13;, &frac13;, &frac13;) .


三角形に必要なテクスチャが1つだけの場合、最初のチャネルのみを使用します。つまり、その色は完全に赤になります。2つの異なるタイプを混合する場合、最初と2番目のチャネルを使用します。つまり、三角形の色は赤と緑の混合になります。そして、3つのタイプすべてが出会うと、赤、緑、青の混合物になります。









3つのスプラットマップ構成。



どのテクスチャが実際にミックスされるかに関係なく、これらのスプラットマップ構成を使用します。つまり、スプラットマップは常に同じになります。テクスチャのみが変更されます。これを行う方法は、後で確認します。セルの色を使用するのではなく、これらのスプラットマップを作成



するHexGridChunk



ように変更する必要があります。しばしば3色を使用するため、それらの静的フィールドを作成します。



  static Color color1 = new Color(1f, 0f, 0f); static Color color2 = new Color(0f, 1f, 0f); static Color color3 = new Color(0f, 0f, 1f);
      
      





セルセンター



デフォルトでセルの中心の色を置き換えることから始めましょう。ここではブレンドは行われないため、最初の色、つまり赤を使用します。



  void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, color1); … }
      
      











セルの赤い中心。



セルの中心が赤に変わります。これらはすべて、テクスチャが何であるかに関係なく、3つのテクスチャの最初のものを使用します。スプラットマップは、セルを色付けする色に関係なく同じです。



川の近所



セルが流れる川のないセル内でのみセグメントを変更しました。川に隣接するセグメントについても同様にする必要があります。私たちの場合、これはリブストリップとリブの三角形の扇の両方です。ここでも、赤だけで十分です。



  void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
      
      











川に隣接する赤いセグメント。





次に、セル内の川の形状に注意する必要があります。それらもすべて赤に変わります。まず、川の始まりと終わりを見てみましょう。



  void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
      
      





そして、土手と川底を構成するジオメトリ。コードを読みやすくするために、カラーメソッド呼び出しをグループ化しました。



  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); terrain.AddTriangle(centerL, m.v1, m.v2); // terrain.AddTriangleColor(cell.Color); terrain.AddQuad(centerL, center, m.v2, m.v3); // terrain.AddQuadColor(cell.Color); terrain.AddQuad(center, centerR, m.v3, m.v4); // terrain.AddQuadColor(cell.Color); terrain.AddTriangle(centerR, m.v4, m.v5); // terrain.AddTriangleColor(cell.Color); terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); … }
      
      











セルに沿ったレッドリバー。



リブ



異なるタイプの地形を持つことができるセルの間にあるため、すべてのエッジは異なります。現在のセルタイプには最初の色を使用し、隣接セルタイプには2番目の色を使用します。その結果、両方のセルが同じタイプであっても、スプラットマップは赤緑のグラデーションになります。両方のセルが同じテクスチャを使用する場合、それは両側の同じテクスチャの混合になります。



  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); } … }
      
      











棚を除く赤緑のリブ。



赤と緑の急激な移行は問題を引き起こしませんか?
, , . . splat map, . .



, .


出っ張りのあるエッジは、頂点が追加されているため、もう少し複雑です。幸いなことに、既存の補間コードは、スプラットマップの色に最適です。最初と最後のセルの色ではなく、最初と2番目の色を使用してください。



  void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); TriangulateEdgeStrip(begin, color1, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, color2, hasRoad); }
      
      











green骨の赤緑の棚。



角度



セル角度は、3つの異なるテクスチャを混合する必要があるため、最も困難です。下のピークに赤、左に緑、右に青を使用します。1つの三角形の角から始めましょう。



  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
      
      











棚を除く赤、緑、青の角。



ここでも、出っ張りのあるコーナーに既存のカラー補間コードを使用できます。補間は2色ではなく3色の間で行われます。最初に、崖の近くにない棚を考えてください。



  void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); }
      
      











崖に沿った棚を除く、赤緑青のコーナー棚。



崖に関しては、メソッドを使用する必要がありますTriangulateBoundaryTriangle



このメソッドは、開始セルと左セルをパラメーターとして受け取りました。ただし、適切なスプラットカラーが必要になりました。これは、トポロジによって異なる場合があります。したがって、これらのパラメーターを色に置き換えます。



  void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); }
      
      





TriangulateCornerTerracesCliff



正しい色を使用するように変更してください



  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color3, b); TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
      
      





そして、同じことを行いTriangulateCornerCliffTerraces



ます。



  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color2, b); TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
      
      











完全なスプラットレリーフマップ。



ユニティパッケージ



テクスチャ配列



地形にスプラットマップがあるので、テクスチャコレクションをシェーダーに渡すことができます。配列は単一のエンティティとしてGPUメモリに存在する必要があるため、シェーダーをC#テクスチャの配列に割り当てることはできません。Texture2DArray



バージョン5.4以降、Unityでサポートされている特別なオブジェクトを使用する必要があります



すべてのGPUはテクスチャ配列をサポートしていますか?
GPU , . Unity .

  • Direct3D 11/12 (Windows, Xbox One)
  • OpenGL Core (Mac OS X, Linux)
  • Metal (iOS, Mac OS X)
  • OpenGL ES 3.0 (Android, iOS, WebGL 2.0)
  • PlayStation 4




マスター



残念ながら、バージョン5.5でのテクスチャ配列のUnityのサポートは最小限です。テクスチャ配列アセットを作成してテクスチャを割り当てるだけではできません。手動で行う必要があります。プレイモードでテクスチャの配列を作成するか、エディターでアセットを作成できます。アセットを作成しましょう。



資産を作成する理由
, Play . , .



, . Unity . , . , .


テクスチャの配列を作成するには、独自のマスターを組み立てます。スクリプトTextureArrayWizard



作成しEditorフォルダー内に配置します代わりに、名前空間からMonoBehaviour



型を拡張する必要がScriptableWizard



ありますUnityEditor







 using UnityEditor; using UnityEngine; public class TextureArrayWizard : ScriptableWizard { }
      
      





一般的な静的メソッドを使用してウィザードを開くことができますScriptableWizard.DisplayWizard



そのパラメーターは、ウィザードウィンドウとその作成ボタンの名前です。このメソッドを静的メソッドで呼び出しますCreateWizard







  static void CreateWizard () { ScriptableWizard.DisplayWizard<TextureArrayWizard>( "Create Texture Array", "Create" ); }
      
      





エディターからウィザードにアクセスするには、このメソッドをUnityメニューに追加する必要があります。これは、メソッドに属性を追加することで実行できますMenuItem



それをAssetsメニューに、具体的にはAssets / Create / Texture Arrayに追加しましょう



  [MenuItem("Assets/Create/Texture Array")] static void CreateWizard () { … }
      
      











カスタムウィザード。



新しいメニュー項目を使用して、カスタムウィザードのポップアップメニューを開くことができます。あまり美しくありませんが、問題の解決に適しています。ただし、まだ空です。テクスチャの配列を作成するには、テクスチャの配列が必要です。マスターの一般フィールドを追加します。ウィザードの標準GUIには、標準インスペクターのように表示されます。



  public Texture2D[] textures;
      
      











テクスチャのマスター。



何かを作ろう



ウィザードの[ 作成 ]ボタンをクリックすると、ウィザードが消えます。さらに、Unityはメソッドがないと文句を言いOnWizardCreate



ます。これは、作成ボタンがクリックされたときに呼び出されるメソッドなので、ウィザードに追加する必要があります。



  void OnWizardCreate () { }
      
      





ここで、テクスチャの配列を作成します。少なくとも、ユーザーがマスターにテクスチャを追加した場合。そうでない場合、作成するものは何もないので、作業を停止する必要があります。



  void OnWizardCreate () { if (textures.Length == 0) { return; } }
      
      





次のステップは、テクスチャ配列アセットを保存する場所をリクエストすることです。メソッドを使用して、ファイル保存パネルを開くことができますEditorUtility.SaveFilePanelInProject



そのパラメーターは、パネル名、デフォルトのファイル名、ファイル拡張子、および説明を定義します。テクスチャ配列は、一般的なアセットファイル拡張子を使用します



  if (textures.Length == 0) { return; } EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" );
      
      





SaveFilePanelInProject



ユーザーが選択したファイルパスを返します。ユーザーがこのパネルでキャンセルをクリックした場合、パスは空の文字列になります。したがって、この場合、作業を中断する必要があります。



  string path = EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" ); if (path.Length == 0) { return; }
      
      





テクスチャの配列を作成する



正しいパスがあれば、次に進み、新しいオブジェクトを作成できますTexture2DArray



彼のコンストラクターメソッドでは、テクスチャの幅と高さ、配列の長さ、テクスチャの形式、ミップテクスチャリングの必要性を指定する必要があります。これらのパラメーターは、配列内のすべてのテクスチャで同じでなければなりません。オブジェクトを設定するには、最初のテクスチャを使用します。ユーザーは、すべてのテクスチャが同じ形式であることを確認する必要があります。



  if (path.Length == 0) { return; } Texture2D t = textures[0]; Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 );
      
      





テクスチャ配列は単一のGPUリソ​​ースであるため、すべてのテクスチャに同じフィルタリングモードと折りたたみモードを使用します。ここでも、最初のテクスチャを使用してすべてを設定します。



  Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 ); textureArray.anisoLevel = t.anisoLevel; textureArray.filterMode = t.filterMode; textureArray.wrapMode = t.wrapMode;
      
      





メソッドを使用して、テクスチャを配列にコピーできますGraphics.CopyTexture



このメソッドは、一度に1ミップレベルの生のテクスチャデータをコピーします。したがって、すべてのテクスチャとそれらのミップレベルをループする必要があります。メソッドパラメータは、テクスチャリソース、インデックス、およびミップレベルで構成される2つのセットです。ソーステクスチャは配列ではないため、インデックスは常にゼロです。



  textureArray.wrapMode = t.wrapMode; for (int i = 0; i < textures.Length; i++) { for (int m = 0; m < t.mipmapCount; m++) { Graphics.CopyTexture(textures[i], 0, m, textureArray, i, m); } }
      
      





この段階で、メモリには正しいテクスチャの配列がありますが、まだ資産ではありません。最後の手順はAssetDatabase.CreateAsset



、配列とそのパスを使用して呼び出すことです。この場合、データはプロジェクトのファイルに書き込まれ、プロジェクトウィンドウに表示されます。



  for (int i = 0; i < textures.Length; i++) { … } AssetDatabase.CreateAsset(textureArray, path);
      
      







テクスチャー



テクスチャの実際の配列を作成するには、元のテクスチャが必要です。以下は、これまで使用していた色に一致する5つのテクスチャです。黄色が砂になり、緑が草になり、青が土になり、オレンジが石になり、白が雪になります。

































砂、草、地球、石、雪のテクスチャ。



これらのテクスチャは、このレリーフの写真ではないことに注意してください。これらは、NumberFlowを使用して作成した軽量の擬似ランダムパターンです私は、抽象的な多角形のレリーフと矛盾しない、認識可能なレリーフのタイプと詳細を作成するよう努めました。フォトリアリズムはこれに適さないことが判明しました。さらに、パターンは変動性を追加しますが、繰り返しがすぐに目立つようにするパターンはほとんどありません。



これらのテクスチャをマスター配列に追加し、その順序が色と一致することを確認します。つまり、最初に砂、次に草、土、石、そして最後に雪です。















テクスチャの配列を作成します。



テクスチャ配列アセットを作成したら、それを選択してインスペクターで調べます。









テクスチャ配列インスペクター。



これは、テクスチャ配列のデータの最も単純な表示です。最初にオンになっいるIs Readableスイッチがあることに注意してください。配列からピクセルデータを読み取る必要がないため、オフにします。Texture2DArray



このパラメーターにアクセスするメソッドまたはプロパティないため、ウィザードでこれを行うことはできません



(ではユニティ5.6は、周りには切断をせずに、可能である。複数のプラットフォーム間でのアセンブリにおけるテクスチャのバグその腐敗のアレイを有する読み取り可能である。)



これは、フィールドがあることが注目にも価値があるカラースペースは、値1が割り当てられます。これは、テクスチャがガンマ空間にあると想定されることを意味します。それらが線形空間にあることになっている場合、フィールドに値0を割り当てる必要がありました。実際、デザイナーにTexture2DArray



は色空間を設定するための追加のパラメーターがありますTexture2D



が、線形空間にあるかどうかは表示されないため、いずれにしても、設定する必要があります手動で値。



シェーダー



テクスチャの配列ができたので、シェーダーにその操作方法を教える必要があります。ここでは、VertexColorsシェーダーを使用して地形をレンダリングします今から色の代わりにテクスチャを使用するので、名前をTerrainに変更します。次に、_MainTexパラメーターをテクスチャの配列に変換し、アセットに割り当てます。



 Shader "Custom/Terrain" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } … }
      
      











テクスチャの配列を含むレリーフ素材。



テクスチャアレイをサポートするすべてのプラットフォームでテクスチャアレイを有効にするには、シェーダーのターゲットレベルを3.0から3.5に上げる必要があります。



  #pragma target 3.5
      
      





変数_MainTex



はテクスチャの配列を参照するようになったため、タイプを変更する必要があります。タイプはターゲットプラットフォームに依存し、マクロがこれを処理しUNITY_DECLARE_TEX2DARRAY



ます。



 // sampler2D _MainTex; UNITY_DECLARE_TEX2DARRAY(_MainTex);
      
      





他のシェーダーと同様に、地形テクスチャをサンプリングするには、世界のXZ座標が必要です。したがって、サーフェスシェーダーの入力構造にワールド内の位置を追加します。また、デフォルトのUV座標も必要ないため、削除します。



  struct Input { // float2 uv_MainTex; float4 color : COLOR; float3 worldPos; };
      
      





テクスチャの配列をサンプリングするには、マクロを使用する必要がありますUNITY_SAMPLE_TEX2DARRAY



配列をサンプリングするには、3つの座標が必要です。最初の2つは通常のUV座標です。0.02にスケーリングされたXZワールド座標を使用します。そのため、フル倍率で良好なテクスチャ解像度が得られます。テクスチャは約4セルごとに繰り返されます。



3番目の座標は、通常の配列と同様に、テクスチャ配列のインデックスとして使用されます。座標は浮動小数点なので、GPU配列にインデックスを付ける前に座標を丸めます。どのテクスチャが必要かがわかるまで、常に最初のテクスチャを使用しましょう。また、頂点の色はスプラットマップであるため、最終結果に影響しません。



  void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz * 0.02; fixed4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, float3(uv, 0)); Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
      
      











すべてが砂になりました。



ユニティパッケージ



テクスチャ選択



三角形ごとに3つのタイプを混在させるレリーフスプラットマップが必要です。テクスチャの配列があり、地形のタイプごとにテクスチャがあります。テクスチャ配列をサンプリングするシェーダーがあります。ただし、現時点では、各三角形に選択するテクスチャをシェーダーに伝える方法はありません。



各三角形には最大3つのタイプが混在しているため、各三角形に3つのインデックスを関連付ける必要があります。三角形の情報を保存できないため、頂点のインデックスを保存する必要があります。三角形の3つの頂点はすべて、単色の場合と同じインデックスを単純に格納します。



メッシュデータ



インデックスを保存するために、UVメッシュのセットの1つを使用できます。各頂点に3つのインデックスが格納されているため、既存の2D UVセットでは十分ではありません。幸いなことに、UVセットには最大4つの座標を含めることができます。したがって、HexMesh



2番目のリストに追加し、Vector3



これをレリーフタイプと呼びます。



  public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; public bool useTerrainTypes; [NonSerialized] List<Vector3> vertices, terrainTypes;
      
      





プレハブのHex Grid ChunkTerrain子オブジェクトの地形タイプを有効にします









リリーフタイプを使用しています。



必要に応じて、Vector3



メッシュクリーニング中にレリーフタイプの別のリスト取得します。



  public void Clear () { … if (useTerrainTypes) { terrainTypes = ListPool<Vector3>.Get(); } triangles = ListPool<int>.Get(); }
      
      





メッシュデータを適用するプロセスでは、3番目のUVセットにレリーフタイプを保存します。このため、一緒に使用することに決めた場合、他の2つのセットと競合することはありません。



  public void Apply () { … if (useTerrainTypes) { hexMesh.SetUVs(2, terrainTypes); ListPool<Vector3>.Add(terrainTypes); } hexMesh.SetTriangles(triangles, 0); … }
      
      





三角形のレリーフタイプを設定するには、を使用しますVector3



これらは三角形全体で同じであるため、同じデータを3回追加するだけです。



  public void AddTriangleTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
      
      





クワッドでの混合も同じように機能します。4つの頂点はすべて同じタイプです。



  public void AddQuadTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
      
      





リブの三角形のファン



次に、のメッシュデータに型を追加する必要がありHexGridChunk



ます。から始めましょうTriangulateEdgeFan



まず、読みやすくするために、頂点メソッドとカラーメソッドの呼び出しを分離します。このメソッドを呼び出すたびにそれを彼color1



渡すので、この色を直接使用でき、パラメーターを適用しないことを思い出してください。



  void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { terrain.AddTriangle(center, edge.v1, edge.v2); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v2, edge.v3); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v3, edge.v4); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v4, edge.v5); // terrain.AddTriangleColor(color); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); }
      
      





色の後に、レリーフタイプを追加します。三角形のタイプは異なる場合があるため、これは色を置き換えるパラメーターである必要があります。この単純なタイプを使用してを作成しますVector3



この場合、スプラットマップは常に赤であるため、最初の4つのチャネルのみが重要です。ベクトルの3つのコンポーネントすべてを割り当てる必要があるため、1つのタイプを割り当てましょう。



  void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float type) { … Vector3 types; types.x = types.y = types.z = type; terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); }
      
      





次に、このメソッドのすべての呼び出しを変更して、色の引数をセルの地形のタイプのインデックスに置き換える必要があります。TriangulateWithoutRiver



TriangulateAdjacentToRiver



およびでこの変更を行いTriangulateWithRiverBeginOrEnd



ます。



 // TriangulateEdgeFan(center, e, color1); TriangulateEdgeFan(center, e, cell.TerrainTypeIndex);
      
      





この時点で、再生モードを開始すると、UVメッシュの3番目のセットが範囲外であることを通知するエラーが表示されます。これは、各三角形と四角形にまだレリーフタイプを追加していないために発生しました。それで、変更を続けましょうHexGridChunk







リブストライプ



ここで、エッジストリップを作成するとき、両側にどのような地形があるかを知る必要があります。したがって、それらをパラメーターとして追加し、2つのチャネルにこれらのタイプが割り当てられているタイプのベクトルを作成します。3番目のチャネルは重要ではありません。したがって、最初のチャネルと同一視してください。色を追加したら、クワッドにタイプを追加します。



  void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, float type1, EdgeVertices e2, Color c2, float type2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); Vector3 types; types.x = types.z = type1; types.y = type2; terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
      
      





今、私たちは課題を変える必要がありTriangulateEdgeStrip



ます。まずTriangulateAdjacentToRiver



TriangulateWithRiverBeginOrEnd



そしてTriangulateWithRiver



あなたは、フィンストリップの両側のための細胞型を使用する必要があります。



 // TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeStrip( m, color1, cell.TerrainTypeIndex, e, color1, cell.TerrainTypeIndex );
      
      





次に、エッジの最も単純なケースではTriangulateConnection



、最も近いエッジにはセルタイプを使用し、遠いエッジには隣接タイプを使用する必要があります。それらは同じでも異なっていてもかまいません。



  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { // TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); TriangulateEdgeStrip( e1, color1, cell.TerrainTypeIndex, e2, color2, neighbor.TerrainTypeIndex, hasRoad ); } … }
      
      





同じTriangulateEdgeTerraces



ことが3回トリガーされるものに当てはまりますTriangulateEdgeStrip



棚のタイプは同じです。



  void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); float t1 = beginCell.TerrainTypeIndex; float t2 = endCell.TerrainTypeIndex; TriangulateEdgeStrip(begin, color1, t1, e2, c2, t2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, t1, e2, c2, t2, hasRoad); } TriangulateEdgeStrip(e2, c2, t1, end, color2, t2, hasRoad); }
      
      





角度



角度の最も単純な場合は、単純な三角形です。下のセルは最初のタイプ、左は2番目、右は3番目のタイプを送信します。それらを使用して、タイプのベクトルを作成し、三角形に追加します。



  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); Vector3 types; types.x = bottomCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
      
      





で同じアプローチを使用しますが、TriangulateCornerTerraces



ここではクワッドのグループを作成します。



  void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); terrain.AddQuadTerrainTypes(types); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); terrain.AddQuadTerrainTypes(types); }
      
      





棚と崖を混ぜるときは、を使用する必要がありますTriangulateBoundaryTriangle



型ベクトルパラメーターを指定して、すべての三角形に追加するだけです。



  void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor, Vector3 types ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); terrain.AddTriangleTerrainTypes(types); }
      
      





TriangulateCornerTerracesCliff



透過細胞タイプに基づいてベクトルを作成します。次に、それを1つの三角形に追加して渡しTriangulateBoundaryTriangle



ます。



  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryColor = Color.Lerp(color1, color3, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
      
      





同じことが当てはまりTriangulateCornerCliffTerraces



ます。



  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryColor = Color.Lerp(color1, color2, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
      
      







変更する最後の方法はこれTriangulateWithRiver



です。ここではセルの中心にいるので、現在のセルのタイプのみを扱っています。したがって、そのためのベクトルを作成し、三角形と四角形に追加します。



  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); Vector3 types; types.x = types.y = types.z = cell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); … }
      
      





タイプミックス



この段階で、メッシュには必要な標高インデックスが含まれています。残っているのは、Terrainシェーダーに強制的にそれら使用させることだけです。フラグメントシェーダーでインデックスを取得するには、最初にインデックスを頂点シェーダーに渡す必要があります。河口シェーダーで行ったように、独自の頂点関数でこれを行うことができますこの場合、入力構造にフィールドを追加し、その構造float3 terrain



にコピーしますv.texcoord2.xyz







  #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 … struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); data.terrain = v.texcoord2.xyz; }
      
      





テクスチャ配列をフラグメントごとに3回サンプリングする必要があります。したがって、テクスチャ座標の作成、配列のサンプリング、1つのインデックスのスプラットマップを使用したサンプルの変調に便利な関数を作成しましょう。



  float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * IN.color[index]; } void surf (Input IN, inout SurfaceOutputStandard o) { … }
      
      





ベクトルを配列として使用できますか?
はい - color[0]



color.r



. color[1]



color.g



, .


この関数を使用すると、単純にテクスチャ配列を3回サンプリングし、結果を組み合わせることができます。



  void surf (Input IN, inout SurfaceOutputStandard o) { // float2 uv = IN.worldPos.xz * 0.02; fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); o.Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
      
      











テクスチャレリーフ。



これで、テクスチャでレリーフをペイントできます。単色のように混ざります。ワールド座標はUV座標として使用されるため、高さによって変化することはありません。その結果、鋭い崖に沿って、テクスチャが引き伸ばされます。テクスチャがかなりニュートラルで非常に変化しやすい場合、結果は受け入れられます。それ以外の場合、大きなbigいストレッチマークが表示されます。追加のジオメトリまたは崖のテクスチャで非表示にすることもできますが、チュートリアルではこれを行いません。



スイープ



ここで、色の代わりにテクスチャを使用する場合、エディターパネルを変更するのが論理的です。レリーフテクスチャを表示することもできる美しいインターフェイスを作成できますが、既存のスキームのスタイルに対応する略語に焦点を当てます。









救済オプション。



また、HexCell



colorプロパティは不要になったため、削除します。



 // public Color Color { // get { // return HexMetrics.colors[terrainTypeIndex]; // } // }
      
      





またのでHexGrid



、あなたは色の配列とそれに関連するコードを削除することができます。



 // public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } … … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; } }
      
      





最後に、色の配列も必要ありませんHexMetrics







 // public static Color[] colors;
      
      





ユニティパッケージ



パート15:距離





高品質のマップを作成したら、ナビゲーションを開始します。









最短経路は必ずしも直線ではありません。



グリッド表示



マップ上のナビゲーションは、セルからセルに移動することにより実行されます。どこかに到達するには、一連のセルを通過する必要があります。距離の推定を簡単にするために、マップのベースとなる六角形のグリッドを表示するオプションを追加しましょう。



メッシュテクスチャ



マップメッシュの不規則性にもかかわらず、基礎となるメッシュは完全に平らです。これを示すには、グリッドパターンを地図に投影します。これは、繰り返しメッシュテクスチャを使用して実現できます。









メッシュテクスチャを繰り返します。



上記のテクスチャには、2 x 2セルをカバーする六角形グリッドの小さな部分が含まれています。この領域は正方形ではなく長方形です。テクスチャ自体は正方形なので、パターンは引き伸ばされて見えます。サンプリング時には、これを補正する必要があります。



グリッド投影



メッシュパターンを投影するには、Terrainシェーダーにテクスチャプロパティを追加する必要があります。



  Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 }
      
      











メッシュテクスチャ付きのレリーフ素材。



ワールドのXZ座標を使用してテクスチャをサンプリングし、アルベドを掛けます。テクスチャのグリッド線は灰色であるため、これによりパターンが浮き彫りに織り込まれます。



  sampler2D _GridTex; … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); fixed4 grid = tex2D(_GridTex, IN.worldPos.xz); o.Albedo = c.rgb * grid * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
      
      











アルベドに細かいメッシュを掛けます。



マップ内のセルに一致するようにパターンをスケーリングする必要があります。隣接するセルの中心間の距離は15で、2つのセルを上に移動するには2倍にする必要があります。つまり、Vグリッドの座標を30で割る必要があります。セルの内部半径は5√3であり、2つのセルを右に移動するには、4倍必要です。したがって、グリッドのU座標を20√3で除算する必要があります。



  float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); fixed4 grid = tex2D(_GridTex, gridUV);
      
      











正しいメッシュサイズ。



これで、グリッド線はマップのセルに対応します。レリーフテクスチャと同様に、高さは無視されるため、崖に沿って線が引き伸ばされます。









高さのあるセルへの投影。



メッシュの変形は、特にマップを遠くから見た場合、特にそれほど悪くはありません。









遠くのメッシュ。



グリッド有効化



グリッドの表示は便利ですが、常に必要なわけではありません。たとえば、スクリーンショットを撮るときはオフにする必要があります。さらに、誰もが常にグリッドを見ることを好むわけではありません。それでは、オプションにしましょう。multi_compileディレクティブをシェーダーに追加して、グリッドの有無にかかわらずオプションを作成します。これを行うには、キーワードを使用しGRID_ON



ます。条件付きシェーダーのコンパイルについては、Rendering 5チュートリアル、Multiple Lightsで説明されています。



  #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 #pragma multi_compile _ GRID_ON
      
      





変数を宣言するとき、grid



最初に値1を割り当てます。その結果、グリッドは無効になります。次に、特定のキーワードを持つバリアントのグリッドテクスチャのみをサンプリングしGRID_ON



ます。



  fixed4 grid = 1; #if defined(GRID_ON) float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); grid = tex2D(_GridTex, gridUV); #endif o.Albedo = c.rgb * grid * _Color;
      
      





キーワードはGRID_ON



テレインシェーダーに含まれていないため、グリッドは消えます。再び有効にするには、マップエディターUIにスイッチを追加します。これを可能にするにHexMapEditor



は、テレインマテリアルへのリンクと、キーワードを有効または無効にするメソッドを取得する必要がありますGRID_ON







  public Material terrainMaterial; … public void ShowGrid (bool visible) { if (visible) { terrainMaterial.EnableKeyword("GRID_ON"); } else { terrainMaterial.DisableKeyword("GRID_ON"); } }
      
      











材料に関する3月の編集者の六角形。 グリッド



スイッチをUIに追加し、それをメソッドに接続します。ShowGrid













グリッドスイッチ。



状態を保存



これで、プレイモードで、グリッドの表示を切り替えることができます。最初のテストでは、グリッドは最初はオフになっており、スイッチをオンにすると表示されます。オフにすると、グリッドは再び表示されなくなります。ただし、グリッドが表示されているときに再生モードを終了すると、次に再生モードを開始すると、スイッチはオフになりますが、再びオンになります。



これは、一般的なテレインマテリアルのキーワードを変更しているためです。マテリアルアセットを編集しているため、変更はUnityエディターに保存されます。アセンブリには保存されません。



常にグリッドなしでゲームを開始するにはGRID_ON



、Awakeでキーワードを無効にしHexMapEditor



ます。



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





ユニティパッケージ



編集モード



マップ上の動きを制御したい場合、それと対話する必要があります。少なくとも、パスの開始点としてセルを選択する必要があります。しかし、セルをクリックすると、編集されます。すべての編集オプションを手動で無効にできますが、これは不便です。さらに、マップの編集中にディスプレイスメントの計算を実行したくありません。それでは、編集モードかどうかを決定するスイッチを追加しましょう。



編集スイッチ



HexMapEditor



ブール型フィールドeditMode



、およびそれを定義するメソッドに追加します。次に、UIに別のスイッチを追加して制御します。ナビゲーションモードから始めましょう。つまり、編集モードはデフォルトで無効になります。



  bool editMode; … public void SetEditMode (bool toggle) { editMode = toggle; }
      
      











編集モードの切り替え。



編集を実際に無効にするには、呼び出しをにEditCells



依存させeditMode



ます。



  void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } previousCell = currentCell; } else { previousCell = null; } }
      
      





ラベルのデバッグ



これまでのところ、マップ内を移動するユニットはありません。代わりに、移動距離を視覚化します。これを行うには、既存のセルラベルを使用できます。したがって、編集モードが無効になっているときにそれらを表示します。



  public void SetEditMode (bool toggle) { editMode = toggle; hexGrid.ShowUI(!toggle); }
      
      





ナビゲーションモードで開始するため、デフォルトのラベルを有効にする必要があります。現在HexGridChunk.Awake



、それらを無効にしますが、彼はこれを行うべきではありません。



  void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; // ShowUI(false); }
      
      











ラベルを調整します。



プレイモードを起動すると、セル座標がすぐに表示されるようになりました。ただし、座標は必要ありません。ラベルを使用して距離を表示します。これにはセルごとに1つの数値しか必要ないため、フォントサイズを大きくして読みやすくすることができます。Hex Cell Labelのプレハブを変更して、サイズ8の太字フォントを使用するようにします。









太字のフォントサイズ8の



タグ。再生モードを起動すると、大きなタグが表示されます。セルの最初の座標のみが表示され、残りはラベルに配置されません。









大きなタグ。



座標はもう必要ないので、HexGrid.CreateCell



割り当ての値を削除しますlabel.text







  void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); // label.text = cell.coordinates.ToStringOnSeparateLines(); cell.uiRect = label.rectTransform; … }
      
      





Labelsスイッチとその関連メソッドをUIから削除することもできHexMapEditor.ShowUI



ます。



 // public void ShowUI (bool visible) { // hexGrid.ShowUI(visible); // }
      
      











メソッドの切り替えはもうありません。



ユニティパッケージ



距離を見つける



タグ付きナビゲーションモードができたので、距離の表示を開始できます。セルを選択し、このセルからマップ上のすべてのセルまでの距離を表示します。



距離表示



セルまでの距離を追跡するには、HexCell



整数フィールドに追加しますdistance



このセルと選択したセルの間の距離を示します。したがって、選択されたセル自体の場合はゼロになり、すぐ隣のセルの場合は1になります。



  int distance;
      
      





距離が設定されたら、セルラベルを更新してその値を表示する必要があります。UIオブジェクトHexCell



への参照がRectTransform



あります。私たちは彼に電話しGetComponent<Text>



て、独房に着く必要があります。Text



名前空間にあるものを考慮してUnityEngine.UI



、スクリプトの最初で使用してください。



  void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance.ToString(); }
      
      





テキストコンポーネントへの直接リンクを保持する必要はありませんか?
, . , , , . , .


一般的なプロパティを設定して、セルまでの距離を受け取って設定し、ラベルを更新します。



  public int Distance { get { return distance; } set { distance = value; UpdateDistanceLabel(); } }
      
      





cellパラメーターを使用してHexGrid



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



ます。ここでは、各セルにゼロ距離を設定するだけです。



  public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = 0; } }
      
      





編集モードが有効になっていない場合HexMapEditor.HandleInput



、現在のセルで新しいメソッド呼び出します。



  if (editMode) { EditCells(currentCell); } else { hexGrid.FindDistancesTo(currentCell); }
      
      





座標間の距離



ナビゲーションモードでは、そのうちの1つに触れると、すべてのセルにゼロが表示されます。ただし、もちろん、セルまでの実際の距離を表示する必要があります。それらまでの距離を計算するには、セルの座標を使用できます。したがって、HexCoordinates



メソッドがあると仮定しDistanceTo



、で使用しHexGrid.FindDistancesTo



ます。



  public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
      
      





HexCoordinates



メソッドに追加しDistanceTo



ます。彼は自分の座標を別のセットの座標と比較しなければなりません。Xを測定することから始めましょう。X座標を互いに減算します。



  public int DistanceTo (HexCoordinates other) { return x - other.x; }
      
      





その結果、選択したセルに対してXに沿ったオフセットを取得します。ただし、距離を負にすることはできないため、座標差Xモジュロを返す必要があります。



  return x < other.x ? other.x - x : x - other.x;
      
      











Xに沿った距離。



したがって、1つの次元のみを考慮した場合にのみ正しい距離が得られます。しかし、六角形のグリッドには3つの次元があります。それでは、3つの次元すべての距離を合計して、それが何をもたらすかを見てみましょう。



  return (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z);
      
      











XYZによる距離の合計。



距離が2倍になることがわかりました。つまり、正しい距離を取得するには、この量を半分に分割する必要があります。



  return ((x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z)) / 2;
      
      











実際の距離。



合計が距離の2倍に等しいのはなぜですか?
, . , (1, −3, 2). . , . . , . .









.


ユニティパッケージ



障害物を扱う



私たちが計算した距離は、選択したセルから他の各セルまでの最短経路に対応しています。もっと短い方法を見つけることはできません。ただし、ルートが何もブロックしない場合、これらのパスは正しいことが保証されます。崖、水、その他の障害物があると、私たちは歩き回ることができます。おそらく、いくつかのセルにまったく到達できません。



障害物を回避する方法を見つけるには、単に座標間の距離を計算するのではなく、別のアプローチを使用する必要があります。各セルを個別に調べることはできなくなりました。到達可能なすべてのセルが見つかるまで、マップを検索する必要があります。



検索の視覚化



マップ検索は反復プロセスです。私たちが何をしているかを理解するには、検索の各段階を確認すると役立ちます。これを行うには、検索アルゴリズムをコルーチンに変換しますSystem.Collections



これには検索スペースが必要です。1秒間に60回の更新のリフレッシュレートは、何が起こっているのかを確認するには十分に小さく、小さなマップでの検索にはそれほど時間がかかりませんでした。



  public void FindDistancesTo (HexCell cell) { StartCoroutine(Search(cell)); } IEnumerator Search (HexCell cell) { WaitForSeconds delay = new WaitForSeconds(1 / 60f); for (int i = 0; i < cells.Length; i++) { yield return delay; cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
      
      





常に1つの検索のみがアクティブであることを確認する必要があります。したがって、新しい検索を開始する前に、すべてのコルーチンを停止します。



  public void FindDistancesTo (HexCell cell) { StopAllCoroutines(); StartCoroutine(Search(cell)); }
      
      





さらに、新しいマップを読み込むときに検索を完了する必要があります。



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





幅優先検索



検索を開始する前でも、選択したセルまでの距離はゼロであることがわかります。そして、もちろん、それらに到達できる場合、そのすべての近隣への距離は1です。次に、これらの隣人の1つを見てみましょう。このセルには、到達可能な独自の近傍があり、距離はまだ計算されていません。その場合、これらの隣人までの距離は2である必要があります。距離1のすべての隣人に対してこのプロセスを繰り返すことができます。その後、距離2のすべての隣人に対してこのプロセスを繰り返します。



つまり、最初に距離1にあるすべてのセルを見つけ、次に2の距離にあるすべてのセルを見つけ、次に3の距離にあるなど、終了するまで見つけます。これにより、到達可能な各セルまでの最短距離を見つけることができます。このアルゴリズムは、幅優先検索と呼ばれます。



それが機能するためには、セルまでの距離をすでに決定しているかどうかを知る必要があります。多くの場合、セルは既製セットまたは密閉セットと呼ばれるコレクションに配置されます。ただし、セルint.MaxValue



までの距離を設定して、まだアクセスしていないことを示すことができます。検索を実行する直前に、すべてのセルに対してこれを行う必要があります。



  IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } … }
      
      





これを使用して、を変更することにより、未訪問のセルをすべて非表示にすることもできHexCell.UpdateDistanceLabel



ます。その後、空白のマップで各検索を開始します。



  void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance == int.MaxValue ? "" : distance.ToString(); }
      
      





次に、訪問する必要があるセルと、訪問する順序を追跡する必要があります。このようなコレクションは、多くの場合、ボーダーまたはオープンセットと呼ばれます。セルは、出会ったのと同じ順序で処理する必要があります。これを行うにQueue



は、名前空間の一部であるqueueを使用できますSystem.Collections.Generic



選択したセルがこのキューに最初に配置され、距離は0になります。



  IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); Queue<HexCell> frontier = new Queue<HexCell>(); cell.Distance = 0; frontier.Enqueue(cell); // for (int i = 0; i < cells.Length; i++) { // yield return delay; // cells[i].Distance = // cell.coordinates.DistanceTo(cells[i].coordinates); // } }
      
      





この瞬間から、アルゴリズムはキューに何かがある間にループを実行します。各反復で、最前面のセルがキューから取得されます。



  frontier.Enqueue(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); }
      
      





これで現在のセルができました。これはどんな距離でも構いません。次に、選択したセルからさらに1ステップ先のすべての隣接ノードをキューに追加する必要があります。



  while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor != null) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } } }
      
      





ただし、まだ距離が与えられていないセルのみを追加する必要があります。



  if (neighbor != null && neighbor.Distance == int.MaxValue) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
      
      







広い検索。



水を避ける



幅優先探索が単調なマップ上で正しい距離を見つけることを確認した後、障害物の追加を開始できます。これは、特定の条件が満たされた場合にキューにセルを追加することを拒否することで実行できます。



実際、いくつかのセルがすでに欠落しています。存在しないセルと、距離をすでに示したセルです。この場合、明示的に隣人をスキップするようにコードを書き直しましょう。



  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
      
      





また、水中にあるすべての細胞をスキップしましょう。つまり、最短距離を検索するときは、地上での動きのみを考慮します。



  if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } if (neighbor.IsUnderwater) { continue; }
      
      







水の中を移動せずに距離。



アルゴリズムは依然として最短距離を見つけますが、すべての水を回避します。したがって、水中のセルは、孤立した土地のプロットのように距離を得ることができません。水中のセルは、選択されている場合にのみ距離を取得します。



崖を避ける



また、隣人を訪問する可能性を判断するために、rib骨のタイプを使用できます。たとえば、崖でパスをブロックすることができます。斜面での移動を許可した場合、崖の反対側のセルには、他のパスでのみ到達できます。したがって、それらは非常に異なる距離にある可能性があります。



  if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; }
      
      







崖を越えない距離。



ユニティパッケージ



旅費



セルとエッジは回避できますが、これらのオプションはバイナリです。他の方向よりもある方向にナビゲートする方が簡単だと想像できます。この場合、距離は労力または時間で測定されます。



高速道路



道路を移動する方が簡単で高速であることが論理的であるため、エッジと道路の交差点のコストを低くしましょう。整数値を使用して距離を設定するため、道路に沿って移動するコストを1のままにし、他のエッジを横断するコストを10に増やします。これは大きな違いであり、正しい結果が得られるかどうかをすぐに確認できます。



  int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } neighbor.Distance = distance;
      
      











距離が間違っている道路。



ボーダーソート



残念ながら、幅優先の検索は、変動する移動コストでは機能しません。彼は、距離の増加順にセルが境界に追加されると想定しており、私たちにとってこれはもはや関係ありません。優先キュー、つまりそれ自体をソートするキューが必要です。すべての状況に適合するような方法でそれらをプログラムすることはできないため、標準の優先度キューはありません。



独自の優先度キューを作成できますが、今後のチュートリアル用に最適化してみましょう。今のところ、キューをmethodを持つリストに置き換えるだけSort



です。



  List<HexCell> frontier = new List<HexCell>(); cell.Distance = 0; frontier.Add(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … neighbor.Distance = distance; frontier.Add(neighbor); } }
      
      





ListPool <HexCell>を使用できませんか?
, , . , , .


境界線を真にするには、セルを追加した後に境界線を並べ替える必要があります。実際、セルのすべての隣接セルが追加されるまでソートを延期できますが、最適化に関心がなくなるまで繰り返します。



距離でセルをソートします。これを行うには、この比較を実行するメソッドへのリンクを使用してリストの並べ替えメソッドを呼び出す必要があります。



  frontier.Add(neighbor); frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
      
      





このソート方法はどのように機能しますか?
. , . .



  frontier.Sort(CompareDistances); … static int CompareDistances (HexCell x, HexCell y) { return x.Distance.CompareTo(y.Distance); }
      
      











ソートされた境界線はまだ正しくありません。



国境更新



境界線の並べ替えを開始した後、より良い結果が得られ始めましたが、まだエラーがあります。これは、セルが境界に追加されたときに、このセルまでの最短距離を必ずしも見つけることができないためです。これは、すでに距離が割り当てられている隣人をスキップできなくなったことを意味します。代わりに、短いパスが見つかったかどうかを確認する必要があります。その場合、境界に追加するのではなく、近隣への距離を変更する必要があります。



  HexCell neighbor = current.GetNeighbor(d); if (neighbor == null) { continue; } if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; } frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
      
      







正しい距離。



正しい距離が得られたので、移動のコストを検討し始めます。一部のセルまでの距離は最初は大きすぎますが、境界から削除されると修正されます。このアプローチはダイクストラのアルゴリズムと呼ばれ、エドガー・ダイクストラが最初に発明したものにちなんで名付けられました。



坂道



私たちは、道路だけの異なるコストに制限されることを望みません。たとえば、道路のない平坦なエッジを横断するコストを5に削減し、道路のない斜面の値を10のままにすることができます。



  HexEdgeType edgeType = current.GetEdgeType(neighbor); if (edgeType == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; }
      
      











斜面を克服するには、より多くの作業を行う必要があり、道路は常に高速です。



救済オブジェクト



地形機能がある場合、コストを追加できます。たとえば、多くのゲームでは、フォレストをナビゲートすることはより困難です。この場合、オブジェクトのすべてのレベルを距離に追加するだけです。そして、ここでも道はすべてを加速します。



  if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; }
      
      











道路がない場合、オブジェクトは遅くなります。





最後に、壁を考慮しましょう。道路が通過しない場合、壁は動きをブロックする必要があります。



  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; }
      
      











壁は通過させないので、門を探す必要があります。



ユニティパッケージ



All Articles