Unityの六角形マップ:水循環、侵食、バイオーム、円筒形マップ

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



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



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



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



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



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



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



パート24:地域と侵食





前のパートでは、手続き型マップ生成の基礎を構築しました。 今回は、土地が発生する可能性のある場所を制限し、侵食を行います。



このチュートリアルはUnity 2017.1.0で作成されました。









土地を分離して滑らかにします。



地図の境界線



土地をランダムに上げるため、土地が地図の端に触れることがあります。 これは望ましくない場合があります。 水に制限のある地図には、プレイヤーが端に近づくのを防ぐ自然の障壁が含まれています。 したがって、地図の端近くの水位より上に土地を上昇させることを禁じるとよいでしょう。



ボーダーサイズ



土地は地図の端にどれくらい近いはずですか? この質問に対する正しい答えはないため、このパラメーターをカスタマイズ可能にします。 HexMapGenerator



コンポーネントに2つのスライダーを追加します。1つはX軸に沿ったエッジに沿った境界線、もう1つはZ軸に沿った境界線に使用します。 デフォルト値の5で0から10までの間隔を使用してみましょう。



  [Range(0, 10)] public int mapBorderX = 5; [Range(0, 10)] public int mapBorderZ = 5;
      
      











マップ境界線スライダー。



土地の中心を制限します



境界線がなければ、すべてのセルが有効です。 境界がある場合、最小許容座標は増加し、最大許容座標は減少します。 プロットを生成するには許容間隔を知る必要があるため、4つの整数フィールドを使用して追跡しましょう。



  int xMin, xMax, zMin, zMax;
      
      





寿司を作成する前に、 GenerateMap



制約を初期化します。 Random.Range



呼び出しのパラメーターとしてこれらの値を使用するため、実際には最高値は例外的です。 境界線がない場合、それらは測定セルの数に等しいため、マイナス1ではありません。



  public void GenerateMap (int x, int z) { … for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = waterLevel; } xMin = mapBorderX; xMax = x - mapBorderX; zMin = mapBorderZ; zMax = z - mapBorderZ; CreateLand(); … }
      
      





境界線を越えた土地の出現を厳密に禁止することはありません。鋭く切り取られたエッジが作成されるからです。 代わりに、プロットの生成を開始するために使用されるセルのみを制限します。 つまり、サイトのおおよその中心は制限されますが、サイトの一部は境界エリアを超えることができます。 これを行うには、 GetRandomCell



を変更して、許容されるオフセットの範囲内のセルを選択します。



  HexCell GetRandomCell () { // return grid.GetCell(Random.Range(0, cellCount)); return grid.GetCell(Random.Range(xMin, xMax), Random.Range(zMin, zMax)); }
      
      





























マップの境界は、0×0、5×5、10×10、および0×10です。



すべてのマップパラメーターがデフォルト値に設定されている場合、サイズ5の境界線により、マップの端が土地に触れないように確実に保護されます。 ただし、これは保証されません。 土地は端に近づくことがあり、いくつかの場所で土地に触れることがあります。



土地が国境全体を横切る可能性は、国境のサイズとサイトの最大サイズに依存します。 ためらうことなく、セクションは六角形のままです。 半径のある完全な六角形 r 含む 3r2+3r+1 セル。 境界のサイズに等しい半径を持つ六角形がある場合、それらはそれを横切ることができます。 半径5の完全な六角形には91個のセルが含まれます。 デフォルトでは、セクションごとに最大100セルなので、これは、特に振動がある場合に、土地が5セルを介して橋を架けることができることを意味します。 これを防ぐには、プロットの最大サイズを小さくするか、境界線のサイズを大きくします。



六角形領域のセル数の式はどのように導出されますか?
半径が0の場合、1つのセルを扱います。 中心から1の半径で、6つの追加のセルがあります。 6+1 。 これらの6つのセルは、中心に接する6つの三角形の端と考えることができます。 半径が2の場合、これらの三角形に2番目の行が追加されます。つまり、三角形でさらに2つのセルが取得され、合計で 6ドル(1 + 2)+ 1ドル 。 半径3の場合、3番目の行が追加されます。つまり、三角形ごとに3つのセルが追加され、合計で 6ドル(1 + 2 + 3)+ 1ドル 。 などなど。 つまり、一般的に、式は次のようになります 6sumi=1ri+1=6rr+1/2+1=3rr+1+1=3r2+3r+1


これをより明確に見るために、境界サイズを200に設定できます。半径8の完全な六角形には217個のセルが含まれるため、土地はマップの端に触れる可能性があります。 少なくともデフォルトの境界サイズ値(5)を使用する場合。 境界を10に増やすと、確率は大幅に低下します。















土地区画のサイズは一定で200で、地図の境界線は5と10です。



パンゲア



マップの境界線を増やして土地の割合を同じに保つと、土地が強制的に小さな領域を形成することに注意してください。 この結果、デフォルトで大きな地図は、単一の大きな土地-超大陸パンゲア-をいくつかの小さな島で作成する可能性が非常に高くなります。 境界のサイズが大きくなると、この可能性が高くなり、特定の値では、超大陸になることがほぼ保証されます。 ただし、土地の割合が大きすぎると、利用可能な領域のほとんどがいっぱいになり、その結果、ほぼ長方形の土地の塊ができます。 これを防ぐには、土地の割合を減らす必要があります。









カードの枠線が10の寿司40%。



パンゲアという名前はどこから来たのですか?
それは何年も前に地球上に存在していた最後の既知の超大陸の名前でした。 名前はギリシャ語のパンとガイアで構成され、「すべての自然」または「すべての土地」のようなものを意味します。




無理なカードから守る



目的の土地に到達するまで土地を持ち上げ続けるだけで、適切な量の土地を生成します。 これは、遅かれ早かれ、各セルを水位で上げるために機能します。 ただし、マップの境界線を使用する場合、すべてのセルに到達できるわけではありません。 高い割合の土地が必要な場合、これは発電機の無限の「試行と失敗」につながり、より多くの土地を調達し、無限のサイクルで行き詰まります。 この場合、アプリケーションはフリーズしますが、これは起こりません。



不可能な構成を前もって確実に見つけることはできませんが、無限のサイクルから身を守ることはできます。 CreateLand



実行されたサイクルの数を単純に追跡しCreateLand



。 繰り返しが多すぎる場合、スタックしている可能性が高く、停止する必要があります。



大きなマップの場合、1000回の反復は許容できるように見え、1万回の反復はすでに不合理に思えます。 そのため、この値を終了ポイントとして使用しましょう。



  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); // while (landBudget > 0) { for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); … } }
      
      





破損したマップを取得した場合、多くのセルがすぐに最大の高さに達するため、10,000回の反復を実行してもそれほど時間はかかりません。これにより、新しい領域の成長が妨げられます。



ループを中断した後でも、適切なマップを取得できます。 土地の量が足りず、あまりおもしろくないでしょう。 これに関する通知をコンソールに表示して、残りのどの土地を使用できなかったかをお知らせします。



  void CreateLand () { … if (landBudget > 0) { Debug.LogWarning("Failed to use up " + landBudget + " land budget."); } }
      
      











カード境界線が10の土地の95%は、全額を使うことができませんでした。



故障したカードにはまだばらつきがあるのはなぜですか?
海岸線にはばらつきがあります。これは、作成領域内の高さが高くなりすぎると、新しい領域では外側に成長できないためです。 同じ原則では、区画が最大の高さに達し、単に欠落していることが判明するまで、区画を土地の小さな領域に成長させることはできません。 さらに、プロットを低くすると、ばらつきが大きくなります。


ユニティパッケージ



カードを分割する



マップの境界線ができたので、マップを基本的に2つの別々の領域に分割しました。境界線領域とプロットが作成された領域です。 私たちにとって創造の地域のみが重要であるため、そのような場合は1つの地域の状況と考えることができます。 リージョンは、単にマップ全体をカバーしていません。 しかし、これが不可能な場合、地図を土地創造のいくつかの接続されていない領域に分割することを妨げるものはありません。 これにより、大陸が互いに独立して形成され、異なる大陸が指定されます。



地図地域



マップの1つの領域を構造体として説明することから始めましょう。 これにより、いくつかの地域での作業が簡素化されます。 これのためにMapRegion



構造を作成してみましょう。これには、リージョンの境界フィールドのみが含まれています。 HexMapGenerator



外部ではこの構造を使用しないため、このクラスの内部でプライベートな内部構造として定義できます。 次に、4つの整数フィールドを1つのMapRegion



フィールドに置き換えることができます。



 // int xMin, xMax, zMin, zMax; struct MapRegion { public int xMin, xMax, zMin, zMax; } MapRegion region;
      
      





すべてが機能するためには、 GenerateMap



最小値と最大値のフィールドにregion.



プレフィックスを追加する必要がありregion.







  region.xMin = mapBorderX; region.xMax = x - mapBorderX; region.zMin = mapBorderZ; region.zMax = z - mapBorderZ;
      
      





また、 GetRandomCell







  HexCell GetRandomCell () { return grid.GetCell( Random.Range(region.xMin, region.xMax), Random.Range(region.zMin, region.zMax) ); }
      
      





いくつかの地域



複数の地域をサポートするには、1つのMapRegion



フィールドMapRegion



地域のリストに置き換えます。



 // MapRegion region; List<MapRegion> regions;
      
      





この時点で、リージョンを作成するための別のメソッドを追加するとよいでしょう。 必要なリストを作成するか、既に存在する場合はクリアする必要があります。 その後、彼は以前に行ったように1つのリージョンを決定し、それをリストに追加します。



  void CreateRegions () { if (regions == null) { regions = new List<MapRegion>(); } else { regions.Clear(); } MapRegion region; region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); }
      
      





GenerateMap



でこのメソッドを呼び出しますが、リージョンを直接作成しません。



 // region.xMin = mapBorderX; // region.xMax = x - mapBorderX; // region.zMin = mapBorderZ; // region.zMax = z - mapBorderZ; CreateRegions(); CreateLand();
      
      





GetRandomCell



が任意の領域でGetRandomCell



できるように、 MapRegion



パラメーターを指定します。



  HexCell GetRandomCell (MapRegion region) { return grid.GetCell( Random.Range(region.xMin, region.xMax), Random.Range(region.zMin, region.zMax) ); }
      
      





これで、 RaiseTerraion



SinkTerrain



SinkTerrain



は、対応する領域をGetRandomCell



渡す必要があります。 これを行うには、それぞれにリージョンパラメーターも必要です。



  int RaiseTerrain (int chunkSize, int budget, MapRegion region) { searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(region); … } int SinkTerrain (int chunkSize, int budget, MapRegion region) { searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(region); … }
      
      





CreateLand



メソッドは、セクションを上下させる各領域を決定する必要があります。 リージョン間で土地のバランスをとるために、サイクル内のリージョンのリストを繰り返し繰り返します。



  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { for (int i = 0; i < regions.Count; i++) { MapRegion region = regions[i]; int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (Random.value < sinkProbability) { landBudget = SinkTerrain(chunkSize, landBudget, region); } else { landBudget = RaiseTerrain(chunkSize, landBudget, region); } } } if (landBudget > 0) { Debug.LogWarning("Failed to use up " + landBudget + " land budget."); } }
      
      





ただし、プロットの低下を均等に分散させる必要があります。 これは、すべての地域でそれらを省略するかどうかを決定しながら行うことができます。



  for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { bool sink = Random.value < sinkProbability; for (int i = 0; i < regions.Count; i++) { MapRegion region = regions[i]; int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); // if (Random.value < sinkProbability) { if (sink) { landBudget = SinkTerrain(chunkSize, landBudget, region); } else { landBudget = RaiseTerrain(chunkSize, landBudget, region); } } }
      
      





最後に、土地の全量を正確に使用するためには、量がゼロになったらすぐにプロセスを停止する必要があります。 これは、リージョンのサイクルのどの段階でも発生する可能性があります。 したがって、ゼロサムチェックを内側のループに移動します。 実際、このチェックは土地を上げた後にしか実行できません。なぜなら、金額を下げるときは決して使われないからです。 完了したら、すぐにCreateLand



メソッドを終了できます。



 // for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { for (int guard = 0; guard < 10000; guard++) { bool sink = Random.value < sinkProbability; for (int i = 0; i < regions.Count; i++) { MapRegion region = regions[i]; int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (sink) { landBudget = SinkTerrain(chunkSize, landBudget, region); } else { landBudget = RaiseTerrain(chunkSize, landBudget, region); if (landBudget == 0) { return; } } } }
      
      





2つの地域



現在、いくつかの地域のサポートがありますが、私たちはまだ1つだけを求めています。 CreateRegions



変更して、マップを垂直方向に半分に分割します。 これを行うには、追加された領域のxMax



値を半分にします。 次にxMin



同じ値を使用し、再びxMin



の元の値を使用して、それを2番目の領域として使用します。



  MapRegion region; region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region);
      
      





この段階でカードを生成しても違いはありません。 2つの領域を特定しましたが、それらは1つの古い領域と同じ領域を占めています。 それらを広げるには、それらの間に空きスペースを残す必要があります。 これは、マップの境界と同じ間隔とデフォルト値を使用して、リージョンの境界にスライダーを追加することで実行できます。



  [Range(0, 10)] public int regionBorder = 5;
      
      











リージョン境界スライダー。



土地は地域間のスペースの両側に形成できるため、地図の端に土地の橋を作る可能性が高くなります。 これを防ぐために、領域の境界を使用して、分割線とプロットを開始できる領域の間に土地なしゾーンを設定します。 これは、隣接する領域間の距離が領域の境界のサイズより2大きいことを意味します。



領域のこの境界を適用するにxMax



最初の領域のxMax



からそれを減算しxMin



2番目の領域のxMin



追加します。



  MapRegion region; region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region);
      
      











マップは垂直方向に2つの領域に分割されます。



デフォルト設定では、2つの顕著に分離された領域が作成されますが、1つの領域と大きな地図の境界の場合のように、正確に2つの陸地を受け取る保証はありません。 ほとんどの場合、2つの大きな大陸で、おそらくいくつかの島があります。 ただし、1つの地域に2つ以上の大きな島が作成される場合があります。 また、2つの大陸が地峡によって接続される場合もあります。



もちろん、XとZを測定する方法を変更することにより、マップを水平方向に分割できます。2つの可能な方向のいずれかをランダムに選択しましょう。



  MapRegion region; if (Random.value < 0.5f) { region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); } else { region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); }
      
      











水平方向に2つの領域に分割されたマップ。



幅の広いマップを使用しているため、幅が広くて薄い領域が水平方向に分離して作成されます。 その結果、これらの地域は、いくつかの分割された陸地を形成する可能性が高くなります。



4つの地域



リージョンの数をカスタマイズ可能にして、1〜4つのリージョンのサポートを作成しましょう。



  [Range(1, 4)] public int regionCount = 1;
      
      











リージョン数のスライダー。



switch



を使用して、対応するリージョンコードの実行を選択できます。 デフォルトで使用される1つの地域のコードを繰り返すことから始め、ケース2のために2つの地域のコードを残します。



  MapRegion region; switch (regionCount) { default: region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); break; case 2: if (Random.value < 0.5f) { region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); } else { region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); } break; }
      
      





switchステートメントとは何ですか?
これは、if-else-if-elseステートメントシーケンスを記述する代わりになります。 スイッチは変数に適用され、どのコードを実行する必要があるかを示すためにラベルが使用されます。 最後のelse



ブロックとして使用されるdefault



ラベルもありdefault



。 各オプションは、 break



ステートメントまたはreturn



終了する必要があります。



switch



ブロックを読みやすい状態に保つには、通常、すべてのケースを短く、理想的には単一のステートメントまたはメソッド呼び出しで保持するのが最善です。 リージョンコードの例としてこれは行いませんが、より興味深いリージョンを作成する場合は、別の方法を使用することをお勧めします。 例:



  switch (regionCount) { default: CreateOneRegion(); break; case 2: CreateTwoRegions(); break; case 3: CreateThreeRegions(); break; case 4: CreateFourRegions(); break; }
      
      





3つの領域は2つに似ていますが、半分ではなく3つだけが使用されます。 この場合、水平方向の分離は非常に狭い領域を作成するため、垂直方向の分離のみのサポートを作成しました。 その結果、リージョンの境界領域が2倍になっているため、新しいサイトを作成するためのスペースは、2つのリージョンの場合よりも少ないことに注意してください。



  switch (regionCount) { default: … break; case 2: … break; case 3: region.xMin = mapBorderX; region.xMax = grid.cellCountX / 3 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 3 + regionBorder; region.xMax = grid.cellCountX * 2 / 3 - regionBorder; regions.Add(region); region.xMin = grid.cellCountX * 2 / 3 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); break; }
      
      











3つの地域。



水平方向と垂直方向の分離を組み合わせて、マップの各コーナーに1つの領域を追加することにより、4つの領域を作成できます。



  switch (regionCount) { … case 4: region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; regions.Add(region); break; } }
      
      











4つの地域。



ここで使用されるアプローチは、マップを分割する最も簡単な方法です。 土地の質量によってほぼ同じ地域を生成し、その変動性はマップ生成の他のパラメーターによって制御されます。 ただし、マップが直線に分割されていることは常に明らかです。 コントロールが必要なほど、結果の外観は少なくなります。 したがって、ゲームプレイにほぼ等しい領域が必要な場合、これは正常です。 しかし、最も多様で無制限の土地が必要な場合は、1つの地域の助けを借りて造らなければなりません。



さらに、マップを分割する他の方法があります。 直線だけに限定することはできません。 同じサイズのリージョンを使用する必要はなく、マップ全体をカバーする必要もありません。 穴をあけることができます。 地域の交差を許可したり、地域間の土地の分布を変更したりすることもできます。 さらに、各地域に独自のジェネレーターパラメーターを設定することもできます(ただし、より複雑ですが)。たとえば、地図上に大きな大陸と列島を置くことができます。



ユニティパッケージ



侵食



これまでのところ、生成したすべてのカードはかなり失礼で壊れていました。 本当のレリーフはこのように見えるかもしれませんが、時間が経つにつれてますます滑らかになり、その鋭い部分は侵食により鈍くなってしまいます。マップを改善するために、この侵食プロセスを適用できます。これは、荒い土地を作成した後、別の方法で行います。



  public void GenerateMap (int x, int z) { … CreateRegions(); CreateLand(); ErodeLand(); SetTerrainType(); … } … void ErodeLand () {}
      
      





侵食率



時間が経過するほど、浸食が多くなります。したがって、侵食は永続的ではなく、カスタマイズ可能にする必要があります。少なくとも、侵食はゼロであり、これは以前に作成されたマップに対応します。最大の侵食は包括的です。つまり、侵食力をさらに加えても地形は変化しません。つまり、侵食パラメーターは0〜100の割合である必要があり、デフォルトでは50になります。



  [Range(0, 100)] public int erosionPercentage = 50;
      
      











侵食スライダー。



侵食破壊細胞の検索



侵食により、レリーフがより滑らかになります。私たちの場合、唯一の鋭い部分は崖です。したがって、それらは侵食プロセスのターゲットになります。崖が存在する場合、浸食は最終的に斜面に変わるまでそれを減らす必要があります。これは退屈な地形につながるため、斜面を滑らかにしません。これを行うには、崖の上にあるセルを特定し、高さを低くする必要があります。これらは侵食されやすい細胞です。



セルが侵食されやすいかどうかを判断するメソッドを作成しましょう。彼は高さの十分に大きな違いを見つけるまでセルの隣人をチェックすることによってこれを決定します。崖は少なくとも1つまたは2つのレベルの高さの差を必要とするため、1つまたは複数の隣接セルが少なくとも2ステップ下にある場合、セルは侵食の影響を受けます。そのような隣人がいない場合、セルは侵食を受けることができません。



  bool IsErodible (HexCell cell) { int erodibleElevation = cell.Elevation - 2; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (neighbor && neighbor.Elevation <= erodibleElevation) { return true; } } return false; }
      
      





このメソッドを使用しErodeLand



て、すべてのセルをループし、すべての侵食しやすいセルを一時リストに書き込むことができます。



  void ErodeLand () { List<HexCell> erodibleCells = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (IsErodible(cell)) { erodibleCells.Add(cell); } } ListPool<HexCell>.Add(erodibleCells); }
      
      





侵食しやすい細胞の総数がわかったら、侵食の割合を使用して、残りの侵食しやすい細胞の数を決定できます。たとえば、パーセンテージが50の場合、元の数量の半分が残るまでセルを侵食する必要があります。パーセンテージが100の場合、侵食を起こしやすい細胞をすべて破壊するまで停止しません。



  void ErodeLand () { List<HexCell> erodibleCells = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { … } int targetErodibleCount = (int)(erodibleCells.Count * (100 - erosionPercentage) * 0.01f); ListPool<HexCell>.Add(erodibleCells); }
      
      





侵食されやすい土地のセルだけを考慮すべきではありませんか?
. , , .


セル削減



素朴なアプローチから始めて、侵食によって破壊された細胞の高さを単純に減らすと、もはや侵食されにくくなると仮定します。これが当てはまる場合、リストからランダムなセルを取り出し、高さを減らしてからリストから削除することができます。侵食を受けやすい必要な細胞数に達するまで、この操作を繰り返します。



  int targetErodibleCount = (int)(erodibleCells.Count * (100 - erosionPercentage) * 0.01f); while (erodibleCells.Count > targetErodibleCount) { int index = Random.Range(0, erodibleCells.Count); HexCell cell = erodibleCells[index]; cell.Elevation -= 1; erodibleCells.Remove(cell); } ListPool<HexCell>.Add(erodibleCells);
      
      





必要な検索を防ぐためerodibleCells.Remove



に、リストの最後の現在のセルを上書きし、最後の要素を削除します。私たちはまだ彼らの順序を気にしません。



 // erodibleCells.Remove(cell); erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1);
      
      

















侵食しやすい細胞の0%および100%の素朴な減少、シードマップ1957632474。



侵食追跡



単純なアプローチにより、侵食を適用できますが、適切な程度には適用できません。これは、高さが1つ減少した後もセルが侵食を受け続ける可能性があるためです。したがって、侵食の傾向がなくなったときにのみリストからセルを削除します。



  if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); }
      
      











リスト内の侵食しやすいセルを維持しながら、100%侵食。



したがって、侵食ははるかに強くなりますが、100%を使用しても、すべての崖を取り除くことはできません。その理由は、セルの高さを低くすると、その隣のセルの1つが侵食を受けやすくなる可能性があるためです。したがって、結果として、侵食を起こしやすい細胞が元より多くなる可能性があります。



セルを下げた後、すべての隣接セルを確認する必要があります。侵食の傾向があるが、まだリストにない場合は、そこに追加する必要があります。



  if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if ( neighbor && IsErodible(neighbor) && !erodibleCells.Contains(neighbor) ) { erodibleCells.Add(neighbor); } }
      
      











侵食されたセルはすべて省略されます。



多くの土地を節約します



これで、すべての崖が消えるまで侵食プロセスを継続できます。これは土地に大きく影響します。土地の大部分は消滅し、必要な土地の割合よりもはるかに少なくなりました。これは、マップから土地を削除しているために発生しました。



真の侵食は物質を破壊しません。彼女はある場所からそれを取り出し、別の場所に置きます。同じことができます。 1つのセルが減少すると、その隣のセルを1つ上げる必要があります。実際、1レベルの高さが下のセルに転送されます。これにより、単純にスムージングしながら、マップの全高が節約されます。



これを実現するには、侵食製品をどこに移動するかを決定する必要があります。これが侵食のターゲットになります。侵食されるセルのターゲットポイントを決定するメソッドを作成しましょう。このセルにはブレークが含まれているため、このブレークの下にあるセルをターゲットとして選択するのが論理的です。しかし、侵食されやすいセルにはいくつかのブレークがある可能性があるため、すべての隣接セルをチェックし、すべての候補を一時リストに入れてから、ランダムに1つを選択します。



  HexCell GetErosionTarget (HexCell cell) { List<HexCell> candidates = ListPool<HexCell>.Get(); int erodibleElevation = cell.Elevation - 2; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (neighbor && neighbor.Elevation <= erodibleElevation) { candidates.Add(neighbor); } } HexCell target = candidates[Random.Range(0, candidates.Count)]; ListPool<HexCell>.Add(candidates); return target; }
      
      





ErodeLand



我々はすぐに侵食されたセルを選択した後、標的細胞を定義します。次に、セルの高さを次々に増減します。この場合、ターゲットセル自体が侵食を受けやすくなる可能性がありますが、新しく侵食されたセルの隣接セルをチェックすると、この状況は解決されます。



  HexCell cell = erodibleCells[index]; HexCell targetCell = GetErosionTarget(cell); cell.Elevation -= 1; targetCell.Elevation += 1; if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); }
      
      





ターゲットセルを上げたので、このセルの隣接部分の一部が侵食を受けなくなる可能性があります。それらを回って、侵食を受けやすいかどうかを確認する必要があります。そうでない場合でも、リストに含まれている場合は、リストから削除する必要があります。



  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); … } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = targetCell.GetNeighbor(d); if ( neighbor && !IsErodible(neighbor) && erodibleCells.Contains(neighbor) ) { erodibleCells.Remove(neighbor); } }
      
      











土地の質量を維持しながら100%の侵食。



侵食により、地形がより滑らかになり、一部のエリアが低くなり、他のエリアが高くなります。その結果、土地の質量は増加したり狭くなったりする可能性があります。これにより、土地の割合がいずれかの方向に数パーセント変化する可能性がありますが、重大な逸脱はほとんど発生しません。つまり、適用する侵食が大きいほど、結果として生じる土地の割合に対する制御が少なくなります。



加速侵食



侵食アルゴリズムの有効性を実際に気にする必要はありませんが、簡単な改善を行うことができます。最初に、侵食したセルが侵食可能かどうかを明示的にチェックします。そうでない場合は、基本的にリストから削除します。したがって、ターゲットセルの隣接セルを走査するときにこのセルのチェックをスキップできます。



  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = targetCell.GetNeighbor(d); if ( neighbor && neighbor != cell && !IsErodible(neighbor) && erodibleCells.Contains(neighbor) ) { erodibleCells.Remove(neighbor); } }
      
      





第二に、ターゲットセルの間にブレークがある場合にのみ、ターゲットセルのネイバーをチェックする必要がありましたが、今では必要ありません。これは、隣接セルがターゲットセルより1ステップ高い場合にのみ発生します。そうである場合、隣人はリストにあることが保証されるので、これをチェックする必要はありません。つまり、不必要な検索をスキップできます。



  HexCell neighbor = targetCell.GetNeighbor(d); if ( neighbor && neighbor != cell && neighbor.Elevation == targetCell.Elevation + 1 && !IsErodible(neighbor) // && erodibleCells.Contains(neighbor) ) { erodibleCells.Remove(neighbor); }
      
      





第三に、侵食しやすいセルの隣接をチェックするときに、同様のトリックを使用できます。それらの間に崖がある場合、隣人は侵食を受けやすいです。調べるために、を呼び出す必要はありませんIsErodible







  HexCell neighbor = cell.GetNeighbor(d); if ( neighbor && neighbor.Elevation == cell.Elevation + 2 && // IsErodible(neighbor) && !erodibleCells.Contains(neighbor) ) { erodibleCells.Add(neighbor); }
      
      





ただし、ターゲットセルが侵食の影響を受けやすいかどうかを確認する必要がありますが、上記のサイクルではこれが行われなくなりました。したがって、ターゲットセルに対してこれを明示的に実行します。



  if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } if (IsErodible(targetCell) && !erodibleCells.Contains(targetCell)) { erodibleCells.Add(targetCell); } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … }
      
      





これで、生成された崖の最初の数に対する必要な割合に、浸食を十分迅速に適用できます。ターゲットセルが侵食傾向リストに追加される場所をわずかに変更したという事実により、結果は最適化前の結果からわずかに変更されていることに注意してください。



























25%、50%、75%、および100%の侵食。



また、海岸の形状が変更されたにもかかわらず、トポロジが根本的に変更されていないことにも注意してください。陸地は通常、接続または分離されたままです。小さな島だけが完全にdrれることができます。レリーフの詳細は平滑化されていますが、一般的な形式は同じままです。狭い関節が消えたり、少し成長したりする場合があります。小さな隙間がわずかに埋められたり拡大したりすることがあります。したがって、侵食は分割された領域を強く結び付けません。









4つの完全に侵食された領域はまだ分離されたままです。



ユニティパッケージ



パート25:水循環





このパートでは、土地に湿度を追加します。



このチュートリアルはUnity 2017.3.0で作成されました。









水循環を使用してバイオームを決定します。





この時点まで、マップ生成アルゴリズムはセルの高さのみを変更していました。セル間の最大の違いは、セルが水の上か下かでした。さまざまな種類の地形を定義できますが、これは高さの単純な視覚化です。地域の気候を考慮して、地形のタイプを指定する方が良いでしょう。



地球の気候は非常に複雑なシステムです。幸いなことに、現実的な気候シミュレーションを作成する必要はありません。十分に自然に見えるものが必要です。気候の最も重要な側面は水循環です。なぜなら、動植物は生き残るために液体の水を必要とするからです。温度も非常に重要ですが、今のところ、私たちは本質的に地球の温度を一定に保ち、湿度のみを変化させ、水に焦点を当てています。



水循環は、環境内の水の動きを表します。簡単に言えば、池は蒸発し、雨が雲になり、再び雨が池に流れ込みます。システムにはさらに多くの側面がありますが、これらの手順をシミュレートするだけで、マップ上に自然な外観の水の分布を作成するのに十分な場合があります。



データの可視化



このシミュレーションに入る前に、関連するデータを直接確認すると便利です。これを行うには、テレインシェーダーを変更します。切り替え可能なプロパティを追加します。これは、通常のレリーフテクスチャの代わりに生のマップデータを表示するデータ視覚化モードに切り替えることができます。これは、キーワードを定義する切り替え可能な属性を持つfloatプロパティを使用して実装できます。このため、マテリアルインスペクターにキーワードの定義を制御するフラグとして表示されます。プロパティ自体の名前は重要ではありません。キーワードにのみ興味があります。SHOW_MAP_DATAを使用しています



  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 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [Toggle(SHOW_MAP_DATA)] _ShowMapData ("Show Map Data", Float) = 0 }
      
      











地図データの表示に切り替えます。



キーワードサポートを有効にするシェーダー関数を追加します。



  #pragma multi_compile _ GRID_ON #pragma multi_compile _ HEX_MAP_EDIT_MODE #pragma shader_feature SHOW_MAP_DATA
      
      





残りの救済データの場合と同様に、単一のフロートを表示します。これを実現するために、キーワードが定義されたときに構造Input



フィールドを追加しますmapData







  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; #if defined(SHOW_MAP_DATA) float mapData; #endif };
      
      





頂点プログラムでは、これらのセルのZチャネルを使用してmapData



、常にセル間で補間されるようにを埋めます。



  void vert (inout appdata_full v, out Input data) { … #if defined(SHOW_MAP_DATA) data.mapData = cell0.z * v.color.x + cell1.z * v.color.y + cell2.z * v.color.z; #endif }
      
      





セルデータを表示する必要がある場合は、通常の色ではなく、アルベドフラグメントとして直接使用します。データをレンダリングするときにグリッドがまだオンになるように、グリッドを乗算します。



  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … o.Albedo = c.rgb * grid * _Color * explored; #if defined(SHOW_MAP_DATA) o.Albedo = IN.mapData * grid; #endif … }
      
      





実際にデータをシェーダーに転送します。HexCellShaderData



テクスチャデータの青いチャネルに何かを書き込むメソッドに追加する必要があります。データは、0〜1に制限された単一の浮動小数点値です。



  public void SetMapData (HexCell cell, float data) { cellTextureData[cell.Index].b = data < 0f ? (byte)0 : (data < 1f ? (byte)(data * 255f) : (byte)255); enabled = true; }
      
      





ただし、この決定は研究システムに影響します。青色のチャネルデータ値255は、セルの可視性が移行中であることを示すために使用されます。このシステムが引き続き機能するためには、最大値としてバイト値254を使用する必要があります。デタッチメントの移動はすべてのカードデータを消去することに注意してください。



  cellTextureData[cell.Index].b = data < 0f ? (byte)0 : (data < 1f ? (byte)(data * 254f) : (byte)254);
      
      





同じ名前でにメソッドを追加しHexCell



ます。要求をシェーダーデータに転送します。



  public void SetMapData (float data) { ShaderData.SetMapData(this, data); }
      
      





コードの動作を確認するにHexMapGenerator.SetTerrainType



は、マップの各セルのデータを設定するように変更します。0〜1の間隔で整数から浮動小数点数に変換された高さを視覚化します。これは、セルの高さから最小の高さを引いて、最大の高さから最小の値を引いた値で除算することにより行われます。除算浮動小数点を作成しましょう。



  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … cell.SetMapData( (cell.Elevation - elevationMinimum) / (float)(elevationMaximum - elevationMinimum) ); } }
      
      





これで、テレインマテリアルアセットの[ マップデータを表示]チェックボックスを使用して、通常の地形とデータの視覚化を切り替えることができます















マップ1208905299、通常の地形、高さの視覚化。



気候の創造



気候をシミュレートするには、気候データを追跡する必要があります。マップは個別のセルで構成されているため、各セルには固有の気候があります。ClimateData



すべての関連データを保存する構造作成しますもちろん、セル自体にデータを追加できますが、マップを生成する場合にのみ使用します。したがって、それらを個別に保存します。これはHexMapGenerator



、のように内部この構造体を定義できることを意味しますMapRegion



雲の追跡から始めます。これは、単一のフロートフィールドを使用して実装できます。



  struct ClimateData { public float clouds; }
      
      





リストを追加して、すべてのセルの気候データを追跡します。



  List<ClimateData> climate = new List<ClimateData>();
      
      





次に、気候マップを作成する方法が必要です。まず、気候ゾーンのリストをクリアしてから、各セルに1つの要素を追加する必要があります。初期の気候データは単純にゼロです。これは標準のコンストラクタを使用して実現できますClimateData





  void CreateClimate () { climate.Clear(); ClimateData initialData = new ClimateData(); for (int i = 0; i < cellCount; i++) { climate.Add(initialData); } }
      
      





気候は、地形侵食にさらされた後、救援の種類を設定する前に作成する必要があります。実際には、侵食は主に気候の一部である空気と水の動きによって引き起こされますが、これをシミュレートしません。



  public void GenerateMap (int x, int z) { … CreateRegions(); CreateLand(); ErodeLand(); CreateClimate(); SetTerrainType(); … }
      
      





SetTerrainType



セルの高さの代わりにクラウドデータを表示できるように変更します。最初は、黒いカードのように見えます。



  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … cell.SetMapData(climate[i].clouds); } }
      
      





気候変動



気候シミュレーションの最初のステップは蒸発です。どのくらいの水分を蒸発させるべきですか?スライダーを使用してこの値を制御しましょう。値0は蒸発なし、1-最大蒸発を意味します。デフォルトでは、0.5を使用します。



  [Range(0f, 1f)] public float evaporation = 0.5f;
      
      











蒸発スライダー。



1つのセルの気候を形成するための具体的な方法をもう1つ作成してみましょう。パラメータとしてセルインデックスを指定し、それを使用して、対応するセルとその気候データを取得します。セルが水面下にある場合、私たちは蒸発しなければならない貯水池を扱っています。蒸気をすぐに雲に変え(露点と結露を無視)、セル雲の値に蒸発を直接追加します。これが完了したら、気候データをリストにコピーします。



  void EvolveClimate (int cellIndex) { HexCell cell = grid.GetCell(cellIndex); ClimateData cellClimate = climate[cellIndex]; if (cell.IsUnderwater) { cellClimate.clouds += evaporation; } climate[cellIndex] = cellClimate; }
      
      





の各セルに対してこのメ​​ソッドを呼び出しCreateClimate



ます。



  void CreateClimate () { … for (int i = 0; i < cellCount; i++) { EvolveClimate(i); } }
      
      





しかし、これでは十分ではありません。複雑なシミュレーションを作成するには、細胞の気候を数回形成する必要があります。これを頻繁に行うほど、結果は良くなります。定数値を選択してみましょう。40サイクル使用します。



  for (int cycle = 0; cycle < 40; cycle++) { for (int i = 0; i < cellCount; i++) { EvolveClimate(i); } }
      
      





なぜなら、水で満たされた細胞の上の雲の値を増やすだけであるので、その結果、黒い土地と白い貯水池を手に入れるからです。









水上での蒸発。



雲の散乱



特にますます多くの水が蒸発するとき、雲は常に一箇所にあるわけではありません。圧力差は空気を動かし、それが風の形で現れ、雲も動きます。



支配的な風向がない場合、平均して細胞の雲はすべての方向に均等に分散し、隣接する細胞に現れます。次のサイクルで新しい雲を生成するとき、セル内のすべての雲をその隣に分散させましょう。つまり、各ネイバーはセルクラウドから6分の1を受信し、その後ゼロまで局所的に減少します。



  if (cell.IsUnderwater) { cellClimate.clouds += evaporation; } float cloudDispersal = cellClimate.clouds * (1f / 6f); cellClimate.clouds = 0f; climate[cellIndex] = cellClimate;
      
      





隣人に実際に雲を追加するには、輪になって周りを回り、気候データを取得し、雲の値を増やして、リストにコピーし直す必要があります。



  float cloudDispersal = cellClimate.clouds * (1f / 6f); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } ClimateData neighborClimate = climate[neighbor.Index]; neighborClimate.clouds += cloudDispersal; climate[neighbor.Index] = neighborClimate; } cellClimate.clouds = 0f;
      
      











散乱雲。



これにより、各サイクルで水中のセルがますます多くの雲を地球規模の気候に追加するため、ほぼ白いマップが作成されます。最初のサイクルの後、水の近くの陸地のセルにも分散する必要がある雲があります。このプロセスは、マップの大部分が雲で覆われるまで続きます。デフォルトパラメータを使用したマップ1208905299の場合、北東部の大規模な土地の内部のみが完全に覆われたままでした。



池は無限の数の雲を生成できることに注意してください。水位は気候シミュレーションの一部ではありません。実際には、貯水池は、ほぼ蒸発速度で水が逆流するためにのみ保存されます。つまり、部分的な水循環のみをシミュレートします。これは正常ですが、シミュレーションが長く行われるほど、気候により多くの水が追加されることを理解する必要があります。これまでのところ、水の損失はマップの端でのみ発生し、隣接するエリアがないために散在する雲が失われます。



地図の上部、特に右上のセルで水の損失を確認できます。最後のセルには雲がまったくありません。なぜなら、雲は気候が形成される最後のセルだからです。彼女はまだ隣人から雲を受け取っていません。



すべての細胞の気候が並行して形成されるべきではないでしょうか?
, . - , . 40 . - , .


降水量



水は永遠に冷たいままではありません。ある時点で、彼女は再び地面に落ちるはずです。これは通常、雨の形で起こりますが、時には雪、ail、湿った雪になることもあります。これはすべて、一般に降水量と呼ばれます。雲の消失の大きさと速度は大きく異なりますが、カスタムの全球降雨速度を使用します。値0は降水がないことを意味し、値1はすべての雲が即座に消えることを意味します。デフォルト値は0.25です。これは、各サイクルで雲の4分の1が消えることを意味します。



  [Range(0f, 1f)] public float precipitationFactor = 0.25f;
      
      











降水係数スライダー。



蒸発後、雲の散乱前の降水量をシミュレートします。これは、貯水池から蒸発した水の一部がすぐに沈殿することを意味するため、散乱雲の数は減少します。陸地では、降水は雲の消失につながります。



  if (cell.IsUnderwater) { cellClimate.clouds += evaporation; } float precipitation = cellClimate.clouds * precipitationFactor; cellClimate.clouds -= precipitation; float cloudDispersal = cellClimate.clouds * (1f / 6f);
      
      











消える雲。



今、各サイクルで雲の25%を破壊すると、土地は再びほぼ黒になります。雲はわずか数ステップ内陸に移動し、その後は見えなくなります。



ユニティパッケージ



湿度



降雨は雲を破壊しますが、気候から水を除去すべきではありません。地面に落ちた後、水は別の状態でのみ保存されます。それは多くの形で存在する可能性があり、それらをまとめて湿度を考慮します。



湿度追跡



雲と湿度の2つの水の状態を追跡することにより、気候モデルを改善する予定です。これを実装するには、ClimateData



フィールドに追加しますmoisture







  struct ClimateData { public float clouds, moisture; }
      
      





最も一般化された形式では、蒸発は、少なくとも単純な気候モデルでは、水分を雲に変換するプロセスです。これは、蒸発が一定の値ではなく、別の要因であるべきであることを意味します。したがって、リファクタリングと名前変更を実行evaporation



evaporationFactor



ます。



  [Range(0f, 1f)] public float evaporationFactor = 0.5f;
      
      





セルが水中にある場合、湿度レベルが1であることを通知します。これは、蒸発が蒸発係数に等しいことを意味します。しかし今では、寿司の細胞から蒸発することもできます。この場合、蒸発を計算し、湿度からそれを差し引き、結果を雲に追加する必要があります。その後、湿度に降水量が追加されます。



  if (cell.IsUnderwater) { cellClimate.moisture = 1f; cellClimate.clouds += evaporationFactor; } else { float evaporation = cellClimate.moisture * evaporationFactor; cellClimate.moisture -= evaporation; cellClimate.clouds += evaporation; } float precipitation = cellClimate.clouds * precipitationFactor; cellClimate.clouds -= precipitation; cellClimate.moisture += precipitation;
      
      





雲は陸地からの蒸発によって支えられているため、雲をさらに内陸に移動できます。現在、大部分の土地は灰色になっています。









湿度が蒸発する雲。 雲の代わりに湿度を表示



するSetTerrainType



ように変更してみましょう。これは、レリーフのタイプを決定するために使用するためです。



  cell.SetMapData(climate[i].moisture);
      
      











湿度表示。



この時点で、湿度は雲に非常に似ています(ただし、すべての水中のセルが白であることを除いて)が、すぐに変化します。



降雨流出



水分が細胞から出る唯一の方法は蒸発だけではありません。水循環は、土地に追加された水分の大部分が何らかの形で水になってしまうことを示しています。最も顕著なプロセスは、重力の影響下での土地上の水の流れです。実際の河川をシミュレートするのではなく、カスタム降雨流出係数を使用します。それは、より低いエリアに排水する水の割合を示します。デフォルトでは25%になります。



  [Range(0f, 1f)] public float runoffFactor = 0.25f;
      
      











ドレインスライダー。



川を生成しませんか?
.


水の流出は雲の散乱のように機能しますが、3つの違いがあります。まず、すべての水分がセルから除去されるわけではありません。第二に、雲ではなく湿気を運びます。第三に、それは下降します。つまり、身長が低い隣人だけになります。排水係数は、隣接するすべてが低い場合にセルから流出する水分の量を表しますが、多くの場合、水分は少なくなります。これは、下に隣人が見つかった場合にのみセルの湿度を下げることを意味します。



  float cloudDispersal = cellClimate.clouds * (1f / 6f); float runoff = cellClimate.moisture * runoffFactor * (1f / 6f); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } ClimateData neighborClimate = climate[neighbor.Index]; neighborClimate.clouds += cloudDispersal; int elevationDelta = neighbor.Elevation - cell.Elevation; if (elevationDelta < 0) { cellClimate.moisture -= runoff; neighborClimate.moisture += runoff; } climate[neighbor.Index] = neighborClimate; }
      
      











より低い高さまで排水する。



結果として、高いセルは水分を低い方に伝達するため、湿度の分布はより多様になります。また、沿岸のセルは水分を水中のセルに排出するため、沿岸のセルの水分ははるかに少なくなります。この効果を弱めるには、セルが低いかどうか、つまり見かけの高さを判断するときに水位を使用する必要もあります。



  int elevationDelta = neighbor.ViewElevation - cell.ViewElevation;
      
      











目に見える高さを使用します。



浸透



水は流れ落ちるだけでなく、広がり、平らな地形を浸透し、水域に隣接する土地に吸収されます。この効果はほとんど効果がないかもしれませんが、湿度の分布を滑らかにするのに役立ちますので、シミュレーションに追加しましょう。デフォルトで0.125に等しいカスタム係数を作成してみましょう。



  [Range(0f, 1f)] public float seepageFactor = 0.125f;
      
      











漏れスライダー。



浸透はドレインに似ていますが、隣接セルの高さがセル自体と同じ場合に使用されます。



  float runoff = cellClimate.moisture * runoffFactor * (1f / 6f); float seepage = cellClimate.moisture * seepageFactor * (1f / 6f); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int elevationDelta = neighbor.ViewElevation - cell.ViewElevation; if (elevationDelta < 0) { cellClimate.moisture -= runoff; neighborClimate.moisture += runoff; } else if (elevationDelta == 0) { cellClimate.moisture -= seepage; neighborClimate.moisture += seepage; } climate[neighbor.Index] = neighborClimate; }
      
      











少し漏れを追加しました。



ユニティパッケージ



雨の影



私たちはすでに水循環の価値あるシミュレーションを作成しましたが、気候の違いを最も明確に示す雨の陰がないため、あまり面白くありません。雨の影は、近隣の地域と比べて降雨量が著しく少ない地域です。山が雲がそれらに到達するのを妨げるため、そのような領域が存在します。それらの作成には、高い山と支配的な風向が必要です。





支配的な風向をシミュレーションに追加することから始めましょう。支配的な風の方向は地球の表面で大きく異なりますが、カスタマイズ可能なグローバルな風の方向で管理します。デフォルトで北西を使用しましょう。さらに、風力を1〜10の範囲でデフォルト値の4に調整可能にします。



  public HexDirection windDirection = HexDirection.NW; [Range(1f, 10f)] public float windStrength = 4f;
      
      











風の方向と強さ。



支配的な風の強さは、雲の全体的な分散に対して表されます。風力が1の場合、散乱はすべての方向で同じです。2の場合、散乱は他の方向よりも風の方向で2高くなります。これを行うには、クラウド散布式の除数を変更します。6個ではなく、5個に風力を加えたものになります。



  float cloudDispersal = cellClimate.clouds * (1f / (5f + windStrength));
      
      





さらに、風の方向は、風が吹く方向を決定します。したがって、散乱の主な方向として反対方向を使用する必要があります。



  HexDirection mainDispersalDirection = windDirection.Opposite(); float cloudDispersal = cellClimate.clouds * (1f / (5f + windStrength));
      
      





これで、近傍が散乱の主な方向にあるかどうかを確認できます。もしそうなら、雲の散乱に風の力を掛けなければなりません。



  ClimateData neighborClimate = climate[neighbor.Index]; if (d == mainDispersalDirection) { neighborClimate.clouds += cloudDispersal * windStrength; } else { neighborClimate.clouds += cloudDispersal; }
      
      











北西風、フォース4。



支配的な風は、土地全体に水分の方向分布を追加します。風が強いほど、効果は強くなります。



絶対高さ



雨の影を取得する2番目の要素は山です。私たちは、自然にも同様に山があるという厳密な分類はありません。絶対的な高さのみが重要です。実際、空気が山の上を移動すると、空気は山に強制的に上昇し、冷却され、水分が少なくなる可能性があり、空気が山を通過する前に降水が発生します。その結果、反対側では、乾燥した空気、つまり雨の影が得られます。



最も重要なことは、空気が上昇するほど、空気が少なくなることです。シミュレーションでは、これを各セルの最大クラウド値の強制的な制限として想像できます。可視セルの高さが高いほど、この最大値は低くなります。これを行う最も簡単な方法は、最大値を1マイナス見かけの高さを最大高さで割った値に設定することです。しかし、実際には、最大でマイナス1で除算しましょう。これにより、雲のごく一部が最高のセルを通過することもできます。降水量を計算した後、散乱する前にこの最大値を割り当てます。



  float precipitation = cellClimate.clouds * precipitationFactor; cellClimate.clouds -= precipitation; cellClimate.moisture += precipitation; float cloudMaximum = 1f - cell.ViewElevation / (elevationMaximum + 1f); HexDirection mainDispersalDirection = windDirection.Opposite();
      
      





その結果、許容されるよりも多くの雲が得られた場合、余分な雲を単に湿度に変換します。実際、これは実際の山で起こるように、追加の降水量を追加する方法です。



  float cloudMaximum = 1f - cell.ViewElevation / (elevationMaximum + 1f); if (cellClimate.clouds > cloudMaximum) { cellClimate.moisture += cellClimate.clouds - cloudMaximum; cellClimate.clouds = cloudMaximum; }
      
      











高地が原因の雨の影。



ユニティパッケージ



シミュレーションを完了します



この段階では、すでに水循環の非常に高品質な部分シミュレーションが行われています。それを少し整理して、セルのレリーフのタイプを決定するために適用してみましょう。



並列計算



ネタバレで前述したように、セルが形成される順序はシミュレーション結果に影響します。理想的には、これはすべきではなく、実際にすべてのセルを並行して形成します。これは、形成の現在の段階のすべての変化を気候の2番目のリストに適用することによって、することができますnextClimate







  List<ClimateData> climate = new List<ClimateData>(); List<ClimateData> nextClimate = new List<ClimateData>();
      
      





他のすべての人と同様に、このリストをクリアして初期化します。その後、各サイクルでリストを交換します。この場合、シミュレーションは2つのリストを交互に使用し、現在と次の気候データを適用します。



  void CreateClimate () { climate.Clear(); nextClimate.Clear(); ClimateData initialData = new ClimateData(); for (int i = 0; i < cellCount; i++) { climate.Add(initialData); nextClimate.Add(initialData); } for (int cycle = 0; cycle < 40; cycle++) { for (int i = 0; i < cellCount; i++) { EvolveClimate(i); } List<ClimateData> swap = climate; climate = nextClimate; nextClimate = swap; } }
      
      





セルが隣の気候に影響を与える場合、現在のデータではなく、次の気候データを変更する必要があります。



  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } ClimateData neighborClimate = nextClimate[neighbor.Index]; … nextClimate[neighbor.Index] = neighborClimate; }
      
      





そして、次の気候データを現在の気候リストにコピーする代わりに、次の気候データを取得し、それらに現在の湿度を追加して、すべてを次のリストにコピーします。その後、現在のリストのデータをリセットして、次のサイクルで更新されるようにします。



 // cellClimate.clouds = 0f; ClimateData nextCellClimate = nextClimate[cellIndex]; nextCellClimate.moisture += cellClimate.moisture; nextClimate[cellIndex] = nextCellClimate; climate[cellIndex] = new ClimateData();
      
      





これを行っている間、湿度レベルを最大1に設定して、陸上の細胞が水中よりも濡れないようにします。



  nextCellClimate.moisture += cellClimate.moisture; if (nextCellClimate.moisture > 1f) { nextCellClimate.moisture = 1f; } nextClimate[cellIndex] = nextCellClimate;
      
      











並列コンピューティング。



ソース湿度



シミュレーションでは、特に土地の割合が高いと、乾燥した土地が多くなりすぎる可能性があります。画像を改善するために、デフォルトの0.1のカスタム初期湿度レベルを追加できます。



  [Range(0f, 1f)] public float startingMoisture = 0.1f;
      
      











上は元の湿度のスライダーです。



この値は初期気候リストの湿度に使用しますが、以下には使用しません。



  ClimateData initialData = new ClimateData(); initialData.moisture = startingMoisture; ClimateData clearData = new ClimateData(); for (int i = 0; i < cellCount; i++) { climate.Add(initialData); nextClimate.Add(clearData); }
      
      











元の湿度で。



バイオームの定義



最後に、セルリリーフのタイプを設定するために、高さの代わりに湿度を使用します。完全に乾燥した土地には雪を使用し、乾燥した地域には雪を使用します。次に、石、十分に湿気のある草、水が飽和した水面下の細胞に土地を使用します。0.2の増分で5つの間隔を使用する最も簡単な方法。



  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); float moisture = climate[i].moisture; if (!cell.IsUnderwater) { if (moisture < 0.2f) { cell.TerrainTypeIndex = 4; } else if (moisture < 0.4f) { cell.TerrainTypeIndex = 0; } else if (moisture < 0.6f) { cell.TerrainTypeIndex = 3; } else if (moisture < 0.8f) { cell.TerrainTypeIndex = 1; } else { cell.TerrainTypeIndex = 2; } } else { cell.TerrainTypeIndex = 2; } cell.SetMapData(moisture); } }
      
      











バイオーム。



均一な分布を使用する場合、結果はあまり良くなく、不自然に見えます。0.05、0.12、0.28、0.85など、他のしきい値を使用することをお勧めします。



  if (moisture < 0.05f) { cell.TerrainTypeIndex = 4; } else if (moisture < 0.12f) { cell.TerrainTypeIndex = 0; } else if (moisture < 0.28f) { cell.TerrainTypeIndex = 3; } else if (moisture < 0.85f) { cell.TerrainTypeIndex = 1; }
      
      











変更されたバイオーム。



ユニティパッケージ



パート26:バイオームと川





このパートでは、水循環を河川と気温で補完し、さらに興味深いバイオームを細胞に割り当てます。



チュートリアルはUnity 2017.3.0p3を使用して作成されました。









熱と水が地図を活気づけます。



川の世代



川は水循環の結果です。実際、それらは、チャネル侵食の助けを借りて引き裂かれた排水によって形成されます。これは、セルのシンクの値に基づいて川を追加できることを意味します。ただし、これは実際の川に似たものが得られることを保証するものではありません。川を始めるとき、それは可能な限り多くの細胞を通って流れなければなりません。これは、細胞を並行して処理する水循環のシミュレーションとは一致しません。さらに、通常、マップ上の河川の数の制御が必要です。



川は非常に異なるため、個別に生成します。水循環シミュレーションの結果を使用して川の位置を特定しますが、川はシミュレーションに影響しません。



なぜ川の流れが時々間違っているのですか?
TriangulateWaterShore



, . , . , , . , . , , . («»).



  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary( e1, e2, cell.HasIncomingRiver && cell.IncomingRiver == direction, indices ); } … }
      
      





高湿度セル



マップでは、セルに川がある場合とない場合があります。さらに、分岐または接続できます。現実には、川ははるかに柔軟性がありますが、大きな川だけを作成するこの近似値で対処する必要があります。最も重要なことは、ランダムに選択される大きな川の始まりの位置を決定する必要があることです。



川は水を必要とするため、川の水源は湿度の高いセル内になければなりません。しかし、これでは十分ではありません。川は斜面を流れ落ちるので、理想的には源の高さを大きくする必要があります。水位より上のセルが高ければ高いほど、川の水源の役割のより良い候補になります。セルの高さを最大の高さで割ることにより、これをマップデータとして視覚化できます。結果が水位に対して得られるように、分割する前に両方の高さからそれを差し引きます。



  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … float data = (float)(cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); cell.SetMapData(data); } }
      
      

















湿度と高度。デフォルト設定の大きなカード番号1208905299。



最適な候補は、高湿度と高さの両方を持つセルです。これらの基準を乗算することにより、これらの基準を組み合わせることができます。結果は、河川の水源の適合度または重量の値になります。



  float data = moisture * (cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); cell.SetMapData(data);
      
      











河川の水源の重み。



理想的には、これらの重みを使用して、ソースセルのランダム選択を拒否します。正しい重みでリストを作成して選択することはできますが、これは重要なアプローチであり、生成プロセスの速度が低下します。 4つのレベルに分けられた重要度のより単純な分類で十分です。最初の候補は、値が0.75を超える重みになります。良い候補者の重みは0.5からです。適格な候補者は0.25を超えています。他のすべてのセルは破棄されます。それがグラフィカルにどのように見えるかを見てみましょう。



  float data = moisture * (cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); if (data > 0.75f) { cell.SetMapData(1f); } else if (data > 0.5f) { cell.SetMapData(0.5f); } else if (data > 0.25f) { cell.SetMapData(0.25f); } // cell.SetMapData(data);
      
      











河川源の重みのカテゴリ。



この分類スキームを使用すると、マップの最も湿気の多い地域に水源を持つ河川が得られる可能性があります。それでも、比較的乾燥した地域や低い地域で河川を造る可能性は残っており、変動性が高まります。これらの基準に基づいてセルのリストを埋める



メソッドCreateRivers



追加します。適格なセルはこのリストに1回、良好なセルは2回、主要な候補は4回追加されます。水中のセルは常に破棄されるため、確認することはできません。



  void CreateRivers () { List<HexCell> riverOrigins = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (cell.IsUnderwater) { continue; } ClimateData data = climate[i]; float weight = data.moisture * (cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); if (weight > 0.75f) { riverOrigins.Add(cell); riverOrigins.Add(cell); } if (weight > 0.5f) { riverOrigins.Add(cell); } if (weight > 0.25f) { riverOrigins.Add(cell); } } ListPool<HexCell>.Add(riverOrigins); }
      
      





このメソッドはCreateClimate



、湿度データを利用できるようにするために後で呼び出す必要があります



  public void GenerateMap (int x, int z) { … CreateRegions(); CreateLand(); ErodeLand(); CreateClimate(); CreateRivers(); SetTerrainType(); … }
      
      





分類が完了すると、地図上のデータの視覚化を取り除くことができます。



  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … // float data = // moisture * (cell.Elevation - waterLevel) / // (elevationMaximum - waterLevel); // if (data > 0.6f) { // cell.SetMapData(1f); // } // else if (data > 0.4f) { // cell.SetMapData(0.5f); // } // else if (data > 0.2f) { // cell.SetMapData(0.25f); // } } }
      
      





リバーポイント



いくつの川が必要ですか?このパラメーターはカスタマイズ可能でなければなりません。河川の長さはさまざまであるため、河川を収容する陸地セルの数を決定する河川ポイントを使用して河川を制御する方が論理的です。最大20%、デフォルト値10%のパーセンテージでそれらを表現しましょう。寿司の割合と同様に、これは目標値であり、保証値ではありません。その結果、適切な土地をカバーするには短すぎる候補者や川が少なすぎる可能性があります。そのため、最大パーセンテージは大きすぎてはいけません。



  [Range(0, 20)] public int riverPercentage = 10;
      
      











スライダーパーセントの川。



セルの数として表される河川ポイントを決定するには、で生成された陸上セルの数を覚えておく必要がありますCreateLand







  int cellCount, landCells; … void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); landCells = landBudget; for (int guard = 0; guard < 10000; guard++) { … } if (landBudget > 0) { Debug.LogWarning("Failed to use up " + landBudget + " land budget."); landCells -= landBudget; } }
      
      





内部では、CreateRivers



で行うのと同じ方法で河川ポイント数を計算できますCreateLand







  void CreateRivers () { List<HexCell> riverOrigins = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { … } int riverBudget = Mathf.RoundToInt(landCells * riverPercentage * 0.01f); ListPool<HexCell>.Add(riverOrigins); }
      
      





さらに、ポイントとソースセルがまだある間、元のリストからランダムなセルを取得および削除し続けます。ポイントの数が完了すると、コンソールに警告が表示されます。



  int riverBudget = Mathf.RoundToInt(landCells * riverPercentage * 0.01f); while (riverBudget > 0 && riverOrigins.Count > 0) { int index = Random.Range(0, riverOrigins.Count); int lastIndex = riverOrigins.Count - 1; HexCell origin = riverOrigins[index]; riverOrigins[index] = riverOrigins[lastIndex]; riverOrigins.RemoveAt(lastIndex); } if (riverBudget > 0) { Debug.LogWarning("Failed to use up river budget."); }
      
      





さらに、川を直接作成する方法を追加します。パラメーターとして、彼は初期セルが必要であり、完了後、川の長さを返す必要があります。長さゼロを返すメソッドを保存することから始めます。



  int CreateRiver (HexCell origin) { int length = 0; return length; }
      
      





で追加したサイクルの最後にこのメソッドを呼び出しCreateRivers



、残りのポイントの数を減らします。選択したセルに川が流れていない場合にのみ、新しい川が作成されるようにします。



  while (riverBudget > 0 && riverOrigins.Count > 0) { … if (!origin.HasRiver) { riverBudget -= CreateRiver(origin); } }
      
      





現在の川



海または他の水域に流れる川を作成することは論理的です。ソースから開始すると、すぐに長さ1が取得されます。その後、ランダムな近傍を選択して長さを増やします。水中セルに到達するまで移動を続けます。



  int CreateRiver (HexCell origin) { int length = 1; HexCell cell = origin; while (!cell.IsUnderwater) { HexDirection direction = (HexDirection)Random.Range(0, 6); cell.SetOutgoingRiver(direction); length += 1; cell = cell.GetNeighbor(direction); } return length; }
      
      











ランダムな川。



このような素朴なアプローチの結果として、主に以前に生成された川の置き換えにより、ランダムに散らばった川の破片が得られます。隣人が実際に存在するかどうかを確認しないため、これはエラーにつながることさえあります。サイクルのすべての方向をチェックし、そこに隣人がいることを確認する必要があります。そうである場合、この方向を潜在的なフロー方向のリストに追加しますが、これは川がこの隣をまだ流れていない場合のみです。次に、このリストからランダムな値を選択します。



  List<HexDirection> flowDirections = new List<HexDirection>(); … int CreateRiver (HexCell origin) { int length = 1; HexCell cell = origin; while (!cell.IsUnderwater) { flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor || neighbor.HasRiver) { continue; } flowDirections.Add(d); } HexDirection direction = // (HexDirection)Random.Range(0, 6); flowDirections[Random.Range(0, flowDirections.Count)]; cell.SetOutgoingRiver(direction); length += 1; cell = cell.GetNeighbor(direction); } return length; }
      
      





この新しいアプローチでは、利用可能なフロー方向がゼロになる場合があります。これが発生すると、川はそれ以上流れることができなくなり、終了する必要があります。この時点で長さが1の場合、これは元のセルから漏れることができなかったことを意味します。つまり、川がまったくない可能性があります。この場合、川の長さはゼロです。



  flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } if (flowDirections.Count == 0) { return length > 1 ? length : 0; }
      
      











保存された川。



走り去る



これで、作成済みの川を保存できますが、川の孤立した断片を取得できます。これは、高さを無視しているために発生します。川をより高い高さまで強制的に流すたびに、HexCell.SetOutgoingRiver



この試みを中断し、川の断裂に至りました。したがって、川を上に流す方向もスキップする必要があります。



  if (!neighbor || neighbor.HasRiver) { continue; } int delta = neighbor.Elevation - cell.Elevation; if (delta > 0) { continue; } flowDirections.Add(d);
      
      











流れる川。



そのため、川の多くの断片を取り除きますが、いくつかはまだ残っています。この瞬間から、最もmostい川を取り除くことは洗練の問題になります。そもそも、川はできるだけ早く流れることを好みます。彼らは必ずしも最短ルートを選択するとは限りませんが、この可能性は高くなります。これをシミュレートするために、リストに下方向を3回追加します。



  if (delta > 0) { continue; } if (delta < 0) { flowDirections.Add(d); flowDirections.Add(d); flowDirections.Add(d); } flowDirections.Add(d);
      
      





急カーブを避ける



流下に加えて、水にも慣性があります。川は、急に急に曲がるよりも、まっすぐに流れるか、わずかに曲がる傾向があります。川の最後の方向を追跡することで、この歪みを追加できます。電流の潜在的な方向がこの方向から大きく逸脱していない場合は、リストに再度追加します。これはソースにとって問題ではないため、常に追加し直します。



  int CreateRiver (HexCell origin) { int length = 1; HexCell cell = origin; HexDirection direction = HexDirection.NE; while (!cell.IsUnderwater) { flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … if (delta < 0) { flowDirections.Add(d); flowDirections.Add(d); flowDirections.Add(d); } if ( length == 1 || (d != direction.Next2() && d != direction.Previous2()) ) { flowDirections.Add(d); } flowDirections.Add(d); } if (flowDirections.Count == 0) { return length > 1 ? length : 0; } // HexDirection direction = direction = flowDirections[Random.Range(0, flowDirections.Count)]; cell.SetOutgoingRiver(direction); length += 1; cell = cell.GetNeighbor(direction); } return length; }
      
      





これにより、川がジグザグに見える可能性が大幅に低下します。









急旋回が少ない。



河川合流点



川は、以前に作成された川の水源のすぐ隣を流れることがあります。この川の水源がより高い高度にない場合、新しい川が古い川に流れ込むと判断できます。その結果、2つの隣接する川ではなく、1つの長い川が得られます。



これを行うには、入ってくる川がある場合、または現在の川の水源である場合にのみ、隣人を通過させます。この方向が上向いていないと判断したため、外向きの川があるかどうかを確認します。もしあれば、再び古い川を見つけました。これはめったに起こらないので、他の近隣の水​​源をチェックすることはせず、すぐに川を合流させます。



  HexCell neighbor = cell.GetNeighbor(d); // if (!neighbor || neighbor.HasRiver) { // continue; // } if (!neighbor || neighbor == origin || neighbor.HasIncomingRiver) { continue; } int delta = neighbor.Elevation - cell.Elevation; if (delta > 0) { continue; } if (neighbor.HasOutgoingRiver) { cell.SetOutgoingRiver(d); return length; }
      
      

















プールの前後の川。



距離を保つ



ソースロールの適切な候補者は通常一緒にグループ化されるため、川のクラスターを取得します。さらに、貯水池のすぐ近くで水源をとる川があり、長さ1の川ができます。水源を分配して、川または貯水池に隣接する水を捨てることができます。私たちはこれを内部のループで選択されたソースの隣人をバイパスしますCreateRivers



ルールに違反している隣人を見つけた場合、ソースは私たちに合わず、スキップする必要があります。



  while (riverBudget > 0 && riverOrigins.Count > 0) { int index = Random.Range(0, riverOrigins.Count); int lastIndex = riverOrigins.Count - 1; HexCell origin = riverOrigins[index]; riverOrigins[index] = riverOrigins[lastIndex]; riverOrigins.RemoveAt(lastIndex); if (!origin.HasRiver) { bool isValidOrigin = true; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = origin.GetNeighbor(d); if (neighbor && (neighbor.HasRiver || neighbor.IsUnderwater)) { isValidOrigin = false; break; } } if (isValidOrigin) { riverBudget -= CreateRiver(origin); } }
      
      





また、川は相次いで流れるが、より広い地域をカバーする傾向がある。















距離なしでそれと。



川を湖で終わらせる



すべての川が貯水池に到達するわけではなく、渓谷で動けなくなったり、他の川に阻まれたりします。多くの場合、実際の川も消えているように見えるため、これは特に問題ではありません。これは、たとえば、地下に流れたり、湿地に分散したり、乾燥したりする場合に発生する可能性があります。私たちの川はこれを視覚化できないため、単純に終わります。



ただし、このようなケースの数を最小限に抑えることを試みることができます。川を合流させたり、川を上に流したりすることはできませんが、実際にはよく見られる湖で終わるようにすることはできます。このためにCreateRiver



動かなくなった場合、セル内の水位を上げる必要があります。この可能性は、このセルの近隣の最小高さに依存します。したがって、近隣を調査するときにこれを追跡するには、コードを少し変更する必要があります。



  while (!cell.IsUnderwater) { int minNeighborElevation = int.MaxValue; flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); // if (!neighbor || neighbor == origin || neighbor.HasIncomingRiver) { // continue; // } if (!neighbor) { continue; } if (neighbor.Elevation < minNeighborElevation) { minNeighborElevation = neighbor.Elevation; } if (neighbor == origin || neighbor.HasIncomingRiver) { continue; } int delta = neighbor.Elevation - cell.Elevation; if (delta > 0) { continue; } … } … }
      
      





立ち往生している場合は、まず、まだソースにいるかどうかを確認する必要があります。はいの場合、リバーをキャンセルします。それ以外の場合、すべての隣接セルが少なくとも現在のセルと同じかどうかを確認します。もしそうなら、このレベルまで水を上げることができます。これにより、セルの高さが同じレベルのままでない限り、1つのセルから湖が作成されます。その場合、単純に水位より1レベル下の高さを割り当てます。



  if (flowDirections.Count == 0) { // return length > 1 ? length : 0; if (length == 1) { return 0; } if (minNeighborElevation >= cell.Elevation) { cell.WaterLevel = minNeighborElevation; if (minNeighborElevation == cell.Elevation) { cell.Elevation = minNeighborElevation - 1; } } break; }
      
      

















湖のない川と湖のある川の端。この場合、川の割合は20です。



マップを生成するために使用される水位を超える水中セルがある場合があることに注意してください。彼らは海抜の湖を示します。



追加の湖



立ち往生していない場合でも、湖を作成することもできます。これにより、川が湖に出入りする可能性があります。立ち往生していない場合、湖を作成するには、水位を上げてから現在のセルの高さを上げ、セルの高さを下げます。これは、近隣の最小高さが少なくとも現在のセルの高さに等しい場合にのみ適用されます。これは、川のサイクルの終わりに、次のセルに進む前に行います。



  while (!cell.IsUnderwater) { … if (minNeighborElevation >= cell.Elevation) { cell.WaterLevel = cell.Elevation; cell.Elevation -= 1; } cell = cell.GetNeighbor(direction); }
      
      

















追加の湖なしで、それらと共に。



いくつかの湖は美しいですが、無制限に多くの湖を作成できます。したがって、追加の湖のカスタム確率を追加し、デフォルト値を0.25にします。



  [Range(0f, 1f)] public float extraLakeProbability = 0.25f;
      
      





可能であれば、彼女は追加の湖を生成する可能性を制御します。



  if ( minNeighborElevation >= cell.Elevation && Random.value < extraLakeProbability ) { cell.WaterLevel = cell.Elevation; cell.Elevation -= 1; }
      
      

















追加の湖。



複数のセルを持つ湖を作成するのはどうですか?
, , , . . : . , . , , , .


ユニティパッケージ



温度



水は細胞のバイオームを決定できる要因の1つにすぎません。もう1つの重要な要素は温度です。水のシミュレーションのように温度の流れと拡散をシミュレートできますが、興味深い気候を作成するには、1つの複雑な要素のみが必要です。したがって、温度を単純に保ち、各セルに設定してみましょう。



温度と緯度



温度に対する最大の影響は緯度です。赤道では暑く、極では寒く、両極の間はスムーズに移行します。DetermineTemperature



特定のセルの温度を返すメソッド作成しましょう開始するには、セルのZ座標を次元Zで除算して緯度として使用し、この値を温度として使用します。



  float DetermineTemperature (HexCell cell) { float latitude = (float)cell.coordinates.Z / grid.cellCountZ; return latitude; }
      
      





温度を定義し、SetTerrainType



マップデータとして使用します。



  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); float temperature = DetermineTemperature(cell); cell.SetMapData(temperature); float moisture = climate[i].moisture; … } }
      
      











温度としての緯度、南半球。



下から上に向かって線形の温度勾配が増加します。これを使用して、南半球をシミュレートし、下部に極を、上部に赤道を配置できます。しかし、半球全体を記述する必要はありません。温度差が小さいか、まったく差がない場合、面積を小さくすることができます。これを行うために、低温と高温をカスタマイズ可能にします。これらの温度を0〜1の範囲で設定し、極値をデフォルト値として使用します。



  [Range(0f, 1f)] public float lowTemperature = 0f; [Range(0f, 1f)] public float highTemperature = 1f;
      
      











温度スライダー。



補間器として緯度を使用して、線形補間を使用して温度範囲を適用します。緯度は0から1までの値として表されるため、を使用できますMathf.LerpUnclamped







  float DetermineTemperature (HexCell cell) { float latitude = (float)cell.coordinates.Z / grid.cellCountZ; float temperature = Mathf.LerpUnclamped(lowTemperature, highTemperature, latitude); return temperature; }
      
      





低温は必ずしも高温より低いわけではないことに注意してください。必要に応じて、それらを裏返すことができます。



半球



これで、温度を取ると南半球、おそらく北半球をシミュレートできます。ただし、半球間を切り替えるには別の設定オプションを使用する方がはるかに便利です。列挙とそのフィールドを作成しましょう。したがって、両方の半球を作成するオプションも追加します。これはデフォルトで適用可能です。



  public enum HemisphereMode { Both, North, South } public HemisphereMode hemisphere;
      
      











半球の選択。



北半球が必要な場合は、1から減算することで緯度を単純に反転できます。両方の半球をシミュレートするには、極を地図の上下に、赤道を中央に配置する必要があります。これを行うには、緯度を2倍にします。下半球は正しく処理され、上の半球は1〜2の緯度になります。これを修正するには、1を超えると2から緯度を引きます。



  float DetermineTemperature (HexCell cell) { float latitude = (float)cell.coordinates.Z / grid.cellCountZ; if (hemisphere == HemisphereMode.Both) { latitude *= 2f; if (latitude > 1f) { latitude = 2f - latitude; } } else if (hemisphere == HemisphereMode.North) { latitude = 1f - latitude; } float temperature = Mathf.LerpUnclamped(lowTemperature, highTemperature, latitude); return temperature; }
      
      











両方の半球。



これにより、赤道が寒く、極が暖かく、エキゾチックなマップを作成できる可能性が生まれます。



寒いほど高い



緯度に加えて、温度も高度によって大きく影響されます。平均して、登るほど寒くなります。河川候補で行ったように、これを要因に変えることができます。この場合、セルの高さを使用します。さらに、このインジケータは高さとともに減少します。つまり、1から高さを水位に対する最大値で割った値に等しくなります。最高レベルのインジケーターがゼロにならないように、除数に追加します。次に、このインジケータを使用して温度をスケーリングします。



  float temperature = Mathf.LerpUnclamped(lowTemperature, highTemperature, latitude); temperature *= 1f - (cell.ViewElevation - waterLevel) / (elevationMaximum - waterLevel + 1f); return temperature;
      
      











高さは温度に影響します。



温度変動



ランダムな温度変動を追加することで、温度勾配の単純さを目立たなくすることができます。より現実的にするための小さなチャンスですが、変動が大きすぎると、彼らはarbitrary意的に見えます。温度変動の力をカスタマイズ可能にして、デフォルト値0.1で最大温度偏差として表現してみましょう。



  [Range(0f, 1f)] public float temperatureJitter = 0.1f;
      
      











温度変動スライダー。



このような変動は、局所的な変化がほとんどなく、滑らかでなければなりません。これにはノイズテクスチャを使用できます。HexMetrics.SampleNoise



0.1でスケーリングされたセル位置を呼び出して使用します。チャンネルWを取り、中央に配置し、振動係数でスケーリングします。次に、この値を以前に計算された温度に追加します。



  temperature *= 1f - (cell.ViewElevation - waterLevel) / (elevationMaximum - waterLevel + 1f); temperature += (HexMetrics.SampleNoise(cell.Position * 0.1f).w * 2f - 1f) * temperatureJitter; return temperature;
      
      

















0.1と1の値の温度変動



。4つのノイズチャネルからランダムに選択して、各マップの変動にわずかな変動性を追加できます。チャンネルを一度に設定してからSetTerrainType



、カラーチャンネルのインデックスを作成しますDetermineTemperature







  int temperatureJitterChannel; … void SetTerrainType () { temperatureJitterChannel = Random.Range(0, 4); for (int i = 0; i < cellCount; i++) { … } } float DetermineTemperature (HexCell cell) { … float jitter = HexMetrics.SampleNoise(cell.Position * 0.1f)[temperatureJitterChannel]; temperature += (jitter * 2f - 1f) * temperatureJitter; return temperature; }
      
      











最大の力で異なる温度変動。



ユニティパッケージ



バイオーム



湿度と温度に関するデータが得られたので、バイオームマトリックスを作成できます。このマトリックスにインデックスを付けることにより、バイオームをすべてのセルに割り当て、1つのデータディメンションのみを使用するよりも複雑なランドスケープを作成できます。



バイオームマトリックス



気候モデルは多数ありますが、いずれも使用しません。私たちはそれを非常に単純にします、私たちはロジックにのみ興味があります。乾燥とは砂漠(寒いまたは暑い)を意味し、砂を使用します。寒くて濡れているとは雪を意味します。高温多湿とは、多くの植物、つまり草を意味します。それらの間に、タイガまたはツンドラがあります。これは、地球の灰色がかったテクスチャとして指定します。これらのバイオーム間の移行を作成するには、4x4マトリックスで十分です。



以前は、5つの水分間隔に基づいてレリーフタイプを割り当てました。最も乾燥したストリップを0.05まで下げ、残りを保存します。温度帯域には、0.1、0.3、0.6以上を使用します。便宜上、これらの値を静的配列に設定します。



  static float[] temperatureBands = { 0.1f, 0.3f, 0.6f }; static float[] moistureBands = { 0.12f, 0.28f, 0.85f };
      
      





バイオームに基づいてレリーフタイプのみを指定しますが、それを使用して他のパラメーターを決定できます。したがって、個々のバイオームの構成を記述するHexMapGenerator



構造Biome



定義しましょうこれまでのところ、バンプインデックスと対応するコンストラクターメソッドのみが含まれています。



  struct Biome { public int terrain; public Biome (int terrain) { this.terrain = terrain; } }
      
      





この構造を使用して、行列データを含む静的配列を作成します。X座標として湿度を使用し、Yとして温度を使用します。最も低い温度のラインを雪で、2番目のラインをツンドラで、他の2つを草で埋めます。次に、最も乾燥した柱を砂漠に置き換えて、温度の選択を再定義します。



  static Biome[] biomes = { new Biome(0), new Biome(4), new Biome(4), new Biome(4), new Biome(0), new Biome(2), new Biome(2), new Biome(2), new Biome(0), new Biome(1), new Biome(1), new Biome(1), new Biome(0), new Biome(1), new Biome(1), new Biome(1) };
      
      











1次元配列のインデックスを持つバイオームのマトリックス。



バイオームの定義



SetTerrainType



バイオーム内の細胞を決定するには、サイクル内の温度と湿度の範囲を調べて、必要なマトリックスインデックスを決定します。これらを使用して、目的のバイオームを取得し、細胞トポグラフィーのタイプを指定します。



  void SetTerrainType () { temperatureJitterChannel = Random.Range(0, 4); for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); float temperature = DetermineTemperature(cell); // cell.SetMapData(temperature); float moisture = climate[i].moisture; if (!cell.IsUnderwater) { // if (moisture < 0.05f) { // cell.TerrainTypeIndex = 4; // } // … // else { // cell.TerrainTypeIndex = 2; // } int t = 0; for (; t < temperatureBands.Length; t++) { if (temperature < temperatureBands[t]) { break; } } int m = 0; for (; m < moistureBands.Length; m++) { if (moisture < moistureBands[m]) { break; } } Biome cellBiome = biomes[t * 4 + m]; cell.TerrainTypeIndex = cellBiome.terrain; } else { cell.TerrainTypeIndex = 2; } } }
      
      











バイオームマトリックスに基づく救済。



バイオームのセットアップ



マトリックスで定義されたバイオームを超えることができます。たとえば、マトリックスでは、すべての乾燥バイオームは砂砂漠として定義されていますが、すべての乾燥砂漠が砂で満たされているわけではありません。非常に異なって見える多くの岩が多い砂漠があります。それでは、砂漠のセルの一部を石で置き換えましょう。これは単に高さに基づいて行います。砂は標高が低く、通常は裸の岩が上にあります。



セルの高さが水位よりも最大の高さに近いときに、砂が石に変わると仮定します。これは最初に計算できる岩の多い砂漠の高さの線ですSetTerrainType



。セルが砂で満たされ、その高さが十分に大きい場合、バイオームのレリーフを石に変更します。



  void SetTerrainType () { temperatureJitterChannel = Random.Range(0, 4); int rockDesertElevation = elevationMaximum - (elevationMaximum - waterLevel) / 2; for (int i = 0; i < cellCount; i++) { … if (!cell.IsUnderwater) { … Biome cellBiome = biomes[t * 4 + m]; if (cellBiome.terrain == 0) { if (cell.Elevation >= rockDesertElevation) { cellBiome.terrain = 3; } } cell.TerrainTypeIndex = cellBiome.terrain; } else { cell.TerrainTypeIndex = 2; } } }
      
      











砂と岩の砂漠。



高さに基づく別の変更は、温度に関係なく、乾燥しすぎていない場合にのみ、最大の高さのセルを強制的に雪のピークに変えることです。これにより、高温多湿の赤道付近で雪がピークになる可能性が高くなります。



  if (cellBiome.terrain == 0) { if (cell.Elevation >= rockDesertElevation) { cellBiome.terrain = 3; } } else if (cell.Elevation == elevationMaximum) { cellBiome.terrain = 4; }
      
      











最大高さの雪のキャップ。



植物



それでは、バイオームに植物細胞のレベルを決定させましょう。これを行うにはBiome



、植物フィールドに追加し、コンストラクターに含めます。



  struct Biome { public int terrain, plant; public Biome (int terrain, int plant) { this.terrain = terrain; this.plant = plant; } }
      
      





最も寒くて乾燥したバイオームでは、植物はまったくありません。他のすべての点では、気候が暖かく湿っているほど、植物が増えます。湿度の2番目の列は、最も熱い行の植物の最初のレベルのみを受け取るため、[0、0、0、1]です。3列目は、雪を除いてレベルを1つ増やします。つまり、[0、1、1、2]です。そして、最も濡れた列が再びそれらを増やします。つまり、[0、2、2、3]になります。biomes



プラント構成を追加してアレイを変更します。



  static Biome[] biomes = { new Biome(0, 0), new Biome(4, 0), new Biome(4, 0), new Biome(4, 0), new Biome(0, 0), new Biome(2, 0), new Biome(2, 1), new Biome(2, 2), new Biome(0, 0), new Biome(1, 0), new Biome(1, 1), new Biome(1, 2), new Biome(0, 0), new Biome(1, 1), new Biome(1, 2), new Biome(1, 3) };
      
      











植物レベルのバイオームのマトリックス。



これで、セルの植物のレベルを設定できます。



  cell.TerrainTypeIndex = cellBiome.terrain; cell.PlantLevel = cellBiome.plant;
      
      











植物のあるバイオーム。



植物は今では異なって見えますか?
, . (1, 2, 1) (0.75, 1, 0.75). (1.5, 3, 1.5) (2, 1.5, 2). — (2, 4.5, 2) (2.5, 3, 2.5).



, : (13, 114, 0).


バイオームの植物のレベルを変更できます。まず、雪の地形にそれらが表示されないようにする必要があります。これは既に設定できます。第二に、まだ最大値になっていない場合は、川沿いの植物のレベルを上げましょう。



  if (cellBiome.terrain == 4) { cellBiome.plant = 0; } else if (cellBiome.plant < 3 && cell.HasRiver) { cellBiome.plant += 1; } cell.TerrainTypeIndex = cellBiome.terrain; cell.PlantLevel = cellBiome.plant;
      
      











変更された植物。



水中バイオーム



その瞬間まで、私たちは水中の細胞を完全に無視しました。それらに少しのバリエーションを追加しましょう。それらすべてに地球のテクスチャを使用するわけではありません。高さを基にした単純なソリューションは、より興味深い画像を作成するのに十分です。たとえば、水位の1つ下のセルに草を使用してみましょう。また、水位より上のセル、つまり川によって作られた湖にも草を使用しましょう。負の高さのセルは深海域であるため、石を使用します。他のすべてのセルは接地されたままです。



  void SetTerrainType () { … if (!cell.IsUnderwater) { … } else { int terrain; if (cell.Elevation == waterLevel - 1) { terrain = 1; } else if (cell.Elevation >= waterLevel) { terrain = 1; } else if (cell.Elevation < 0) { terrain = 3; } else { terrain = 2; } cell.TerrainTypeIndex = terrain; } } }
      
      











水中変動。



海岸沿いの水中セルの詳細を追加しましょう。これらは、水上に少なくとも1つの隣接セルを持つセルです。そのようなセルが浅い場合は、ビーチを作成します。崖の隣にある場合は、視覚的なディテールが支配的になり、石を使用します。



これを判断するために、水位の1つ下のセルに隣接するセルをチェックします。隣接する陸のセルとの崖や斜面による接続の数を数えましょう。



  if (cell.Elevation == waterLevel - 1) { int cliffs = 0, slopes = 0; for ( HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++ ) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } int delta = neighbor.Elevation - cell.WaterLevel; if (delta == 0) { slopes += 1; } else if (delta > 0) { cliffs += 1; } } terrain = 1; }
      
      





これで、この情報を使用してセルを分類できます。まず、隣人の半分以上が土地である場合、湖または湾を扱っています。これらのセルには、草のテクスチャを使用します。それ以外の場合、崖がある場合は、石を使用します。それ以外の場合、斜面がある場合は、砂を使用してビーチを作成します。残っている唯一のオプションは、海岸から離れた浅いエリアで、これにはまだ草を使用しています。



  if (cell.Elevation == waterLevel - 1) { int cliffs = 0, slopes = 0; for ( HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++ ) { … } if (cliffs + slopes > 3) { terrain = 1; } else if (cliffs > 0) { terrain = 3; } else if (slopes > 0) { terrain = 0; } else { terrain = 1; } }
      
      

















海岸の変動。



最後に、最も寒い温度範囲に緑色の水中セルがないことを確認しましょう。そのような細胞には、地球を使用します。



  if (terrain == 1 && temperature < temperatureBands[0]) { terrain = 2; } cell.TerrainTypeIndex = terrain;
      
      





多くの設定オプションを備えた、非常に興味深く自然に見えるランダムカードを生成する機会を得ました。



ユニティパッケージ



パート27:カードの折りたたみ





この最後の部分では、地図を最小化し、東端と西端を接続するためのサポートを追加します。



チュートリアルはUnity 2017.3.0p3を使用して作成されました。









折り畳むことで、世界が一周できます。



折りたたみカード



マップを使用して、さまざまなサイズのエリアをモデル化できますが、それらは常に長方形に制限されます。惑星全体ではなく、1つの島または大陸全体の地図を作成できます。惑星は球形で、表面の動​​きを妨げる剛体の境界はありません。一方向に移動し続けると、遅かれ早かれ開始点に戻ります。



球の周りに六角形のグリッドをラップすることはできません;そのようなオーバーラップは不可能です。最良の近似では、12個のセルが五角形でなければならない正二十面体トポロジーが使用されます。ただし、歪みや例外なしで、メッシュを円柱に巻き付けることができます。これを行うには、マップの東端と西端を接続するだけです。ラッピングロジックを除き、他のすべては同じままです。



円柱は球の貧弱な近似です。極をモデル化できないためです。しかし、これは多くのゲームの開発者が東から西への折りたたみを使用して惑星地図をモデル化することを止めませんでした。極地は単にゲームゾーンの一部ではありません。



北と南に曲がってみてはいかがですか?
, . , , . -, -. .


円筒形の折りたたみを実装するには、2つの方法があります。最初の方法は、実際にその表面とその上のすべてを曲げて地図を円筒形にし、東と西のエッジが接触するようにすることです。今、あなたは平らな表面ではなく、実際のシリンダーでプレーします。2番目のアプローチは、フラットマップを保存し、テレポーテーションまたは複製を使用して折りたたむことです。ほとんどのゲームは2番目のアプローチを使用するため、それを採用します。



オプションの折りたたみ



マップを折りたたむ必要があるかどうかは、そのスケール(ローカルまたは惑星)によって異なります。折りたたみをオプションにすることで、両方のサポートを使用できます。これを行うには、デフォルトで折りたたみがオンになった状態で[ 新しいマップ作成 ]メニューに新しいスイッチを追加します。









折りたたみオプションを備えた新しいマップのメニュー。 選択を追跡するため



NewMapMenu



フィールドと、変更方法を追加します。スイッチの状態が変化したときにこのメソッドが呼び出されるようにします。



  bool wrapping = true; … public void ToggleWrapping (bool toggle) { wrapping = toggle; }
      
      





新しいマップが要求されると、最小化オプションの値を渡します。



  void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z, wrapping); } else { hexGrid.CreateMap(x, z, wrapping); } HexMapCamera.ValidatePosition(); Close(); }
      
      





変更しHexMapGenerator.GenerateMap



、彼はこの新しい引数を取り、その後にそれを転送されたことの道をHexGrid.CreateMap







  public void GenerateMap (int x, int z, bool wrapping) { … grid.CreateMap(x, z, wrapping); … }
      
      





code> HexGridは折りたたまれているかどうかを知る必要があるため、フィールドを追加してCreateMap



設定します。他のクラスは、グリッドが最小化されているかどうかに応じてロジックを変更する必要があるため、フィールドを一般化します。さらに、インスペクターを使用してデフォルト値を設定できます。



  public int cellCountX = 20, cellCountZ = 15; public bool wrapping; … public bool CreateMap (int x, int z, bool wrapping) { … cellCountX = x; cellCountZ = z; this.wrapping = wrapping; … }
      
      





HexGrid



CreateMap



2つの場所で自分自身呼び出しますcollapse引数に独自のフィールドを使用するだけです。



  void Awake () { … CreateMap(cellCountX, cellCountZ, wrapping); } … public void Load (BinaryReader reader, int header) { … if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z, wrapping)) { return; } } … }
      
      











グリッド折りたたみスイッチはデフォルトでオンになっています。



保存と読み込み



折りたたみはカードごとに設定されるため、保存してロードする必要があります。これは、ファイルの保存形式を変更する必要があるため、のバージョン定数を大きくすることを意味しますSaveLoadMenu







  const int mapFileVersion = 5;
      
      





保存するときHexGrid



は、マップサイズの後にブール値の折りたたみ値を書き込むだけにします。



  public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); writer.Write(wrapping); … }
      
      





ダウンロードするときは、正しいバージョンのファイルでのみ読み取ります。異なる場合、これは古いカードであり、最小化しないでください。この情報をローカル変数に保存し、現在の折りたたみ状態と比較します。異なる場合、他のサイズのマップをロードする場合と同じ方法で既存のマップトポロジを再利用することはできません。



  public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } bool wrapping = header >= 5 ? reader.ReadBoolean() : false; if (x != cellCountX || z != cellCountZ || this.wrapping != wrapping) { if (!CreateMap(x, z, wrapping)) { return; } } … }
      
      





折りたたみメトリック



マップを折りたたむには、たとえば距離を計算するときなど、ロジックを大幅に変更する必要があります。したがって、グリッドへの直接リンクを持たないコードに触れることができます。この情報を引数として渡す代わりに、に追加しましょうHexMetrics



マップの幅に一致する折りたたみサイズを含む静的整数を追加します。ゼロより大きい場合、折りたたみ可能なカードを扱っています。これを確認するには、プロパティを追加します。



  public static int wrapSize; public static bool Wrapping { get { return wrapSize > 0; } }
      
      





各呼び出しの折りたたみサイズを設定する必要がありますHexGrid.CreateMap







  public bool CreateMap (int x, int z, bool wrapping) { … this.wrapping = wrapping; HexMetrics.wrapSize = wrapping ? cellCountX : 0; … }
      
      





このデータはPlayモードでの再コンパイルには耐えられないため、に設定してみましょうOnEnable







  void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; HexMetrics.wrapSize = wrapping ? cellCountX : 0; ResetVisibility(); } }
      
      





セル幅



折りたたみ可能なカードを使用する場合、セルの幅で測定されるX軸に沿った位置を処理する必要があります。このために使用できますがHexMetrics.innerRadius * 2f



、毎回乗算を追加しない方が便利です。定数を追加しましょうHexMetrics.innerDiameter







  public const float innerRadius = outerRadius * outerToInner; public const float innerDiameter = innerRadius * 2f;
      
      





すでに3箇所で直径を使用できます。まず、HexGrid.CreateCell



新しいセルを配置するとき。



  void CreateCell (int x, int z, int i) { Vector3 position; position.x = (x + z * 0.5f - z / 2) * HexMetrics.innerDiameter; … }
      
      





第二HexMapCamera



に、カメラの位置を制限します。



  Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * HexMetrics.innerDiameter; position.x = Mathf.Clamp(position.x, 0f, xMax); … }
      
      





またHexCoordinates



、位置から座標への変換で



  public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / HexMetrics.innerDiameter; … }
      
      





ユニティパッケージ



カードのセンタリング



マップが折りたたまれていない場合、東端と西端が明確に定義されているため、水平方向の中心が明確になります。しかし、折りたたみ可能なカードの場合、すべてが異なります。東端も西端も中央もありません。または、中心がカメラのある場所であると想定できます。これは、マップを常に視点の中心に配置する必要があるため便利です。その後、どこにいても、マップの東端または西端が見えなくなります。



フラグメント列のマップ



マップの視覚化がカメラに対して中央に配置されるように、カメラの動きに応じて要素の配置を変更する必要があります。西に移動する場合は、現在東部の端にあるものを取得し、西部の端に移動する必要があります。同じことが反対方向にも当てはまります。



理想的には、カメラが隣接するセルの列に移動したらすぐに、セルの最も遠い列をすぐに反対側に移動する必要があります。ただし、それほど正確である必要はありません。代わりに、マップフラグメント全体を転送できます。これにより、メッシュを変更せずにマップの一部を移動できます。



フラグメントの列全体を同時に移動するため、各グループの親列オブジェクトを作成して、それらをグループ化します。これらのオブジェクトの配列をに追加し、でHexGrid



初期化しCreateChunks



ます。コンテナとしてのみ使用するため、コンポーネントへのリンクを追跡するだけですTransform



フラグメントの場合と同様に、それらの初期位置はグリッド座標のローカル原点にあります。



  Transform[] columns; … void CreateChunks () { columns = new Transform[chunkCountX]; for (int x = 0; x < chunkCountX; x++) { columns[x] = new GameObject("Column").transform; columns[x].SetParent(transform, false); } … }
      
      





これで、フラグメントはグリッドではなく、対応する列の子になります。



  void CreateChunks () { … chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(columns[x], false); } } }
      
      











列にグループ化されたフラグメント。



すべてのフラグメントが列の子になったため、フラグメントではCreateMap



なく、すべての列を直接破棄するだけで十分ですしたがって、娘の断片を取り除きます。



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





テレポート列



位置XをパラメーターとしてHexGrid



新しいメソッドに追加しCenterMap



ます。位置を列インデックスに変換し、Unity単位のフラグメント幅で除算します。これは、カメラが現在配置されている列のインデックスになります。つまり、マップの中央の列になります。



  public void CenterMap (float xPosition) { int centerColumnIndex = (int) (xPosition / (HexMetrics.innerDiameter * HexMetrics.chunkSizeX)); }
      
      





中央の列のインデックスが変更された場合にのみ、マップの視覚化を変更するだけで十分です。それで、フィールドでそれを追跡しましょう。マップを作成するときにデフォルト値-1を使用して、新しいマップが常に中央に配置されるようにします。



  int currentCenterColumnIndex = -1; … public bool CreateMap (int x, int z, bool wrapping) { … this.wrapping = wrapping; currentCenterColumnIndex = -1; … } … public void CenterMap (float xPosition) { int centerColumnIndex = (int) (xPosition / (HexMetrics.innerDiameter * HexMetrics.chunkSizeX)); if (centerColumnIndex == currentCenterColumnIndex) { return; } currentCenterColumnIndex = centerColumnIndex; }
      
      





中央の列のインデックスがわかったので、列の半分の数を単純に減算して加算することにより、最小および最大のインデックスを決定できます。奇数列の整数値を使用するため、これは完全に機能します。偶数の場合、列を完全に中央に配置することはできないため、インデックスの1つは必要以上に1ステップ進みます。これにより、マップの最も遠いエッジの方向に1列のオフセットが作成されますが、これは問題ではありません。



  currentCenterColumnIndex = centerColumnIndex; int minColumnIndex = centerColumnIndex - chunkCountX / 2; int maxColumnIndex = centerColumnIndex + chunkCountX / 2;
      
      





これらのインデックスは、負または自然最大列インデックスより大きい場合があります。カメラがマップの自然中心に近い場合にのみ、最小値はゼロです。私たちのタスクは、これらの相対インデックスに対応するように列を移動することです。これは、ループ内の各列のローカルX座標を変更することで実行できます。



  int minColumnIndex = centerColumnIndex - chunkCountX / 2; int maxColumnIndex = centerColumnIndex + chunkCountX / 2; Vector3 position; position.y = position.z = 0f; for (int i = 0; i < columns.Length; i++) { position.x = 0f; columns[i].localPosition = position; }
      
      





各列について、インデックスが最小インデックスよりも低いかどうかを確認します。もしそうなら、それは中心の左に遠すぎます。彼はマップの反対側にテレポートしなければなりません。これは、X座標をマップの幅に等しくすることで実行できます。同様に、列のインデックスが最大のインデックスよりも大きい場合は、中心から右に離れすぎているため、反対側にテレポートする必要があります。



  for (int i = 0; i < columns.Length; i++) { if (i < minColumnIndex) { position.x = chunkCountX * (HexMetrics.innerDiameter * HexMetrics.chunkSizeX); } else if (i > maxColumnIndex) { position.x = chunkCountX * -(HexMetrics.innerDiameter * HexMetrics.chunkSizeX); } else { position.x = 0f; } columns[i].localPosition = position; }
      
      





移動カメラ



これを変更しHexMapCamera.AdjustPosition



て、折りたたみ可能なカードを操作するときに、代わりにをClampPosition



呼び出すようにしますWrapPosition



最初に、新しいメソッドをWrapPosition



複製ClampPosition



にしますが、唯一の違いがありますCenterMap



最終的に、を呼び出します



  void AdjustPosition (float xDelta, float zDelta) { … transform.localPosition = grid.wrapping ? WrapPosition(position) : ClampPosition(position); } … Vector3 WrapPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * HexMetrics.innerDiameter; 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); grid.CenterMap(position.x); return position; }
      
      





カードがすぐに中央にくるように、OnEnable



メソッドを呼び出しますValidatePosition







  void OnEnable () { instance = this; ValidatePosition(); }
      
      







カメラの中心にあるときに左右に移動します。



カメラの移動は引き続き制限されますが、マップはカメラに対して中心に配置され、必要に応じてマップフラグメントの列をテレポートします。小さな地図とリモートカメラでは、これがはっきりと見えますが、大きな地図では、テレポートされた断片がカメラの表示範囲外にあります。明らかに、それらの間にまだ三角形分割がないため、マップの最初の東と西のエッジのみが目立ちます。



カメラを折りたたむには、そのX座標の制限を削除しWrapPosition



ます。代わりに、X座標はゼロよりも小さい間はマップの幅だけ増加し続け、マップの幅よりも大きい場合は減少します。



  Vector3 WrapPosition (Vector3 position) { // float xMax = (grid.cellCountX - 0.5f) * HexMetrics.innerDiameter; // position.x = Mathf.Clamp(position.x, 0f, xMax); float width = grid.cellCountX * HexMetrics.innerDiameter; while (position.x < 0f) { position.x += width; } while (position.x > width) { position.x -= width; } float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); grid.CenterMap(position.x); return position; }
      
      







ロールアップカメラはマップに沿って移動します。



折りたたみ可能なシェーダーテクスチャ



三角測量空間を除き、ゲームモードでカメラを最小化することは感知できないはずです。ただし、これが発生すると、地形と水の半分が視覚的に変化します。これは、世界の位置を使用してこれらのテクスチャをサンプリングするために発生します。フラグメントの鋭いテレポーテーションは、テクスチャの位置を変更します。



この問題を解決するには、フラグメントサイズの倍数のタイルにテクスチャを表示します。フラグメントサイズはの定数から計算されるHexMetrics



ため、HexMetrics.cgincシェーダーインクルードファイル作成し、それに対応する定義を貼り付けましょう。基本的なタイルスケールは、フラグメントサイズとセルの外半径から計算されます。他のメトリックを使用する場合、それに応じてファイルを変更する必要があります。



 #define OUTER_TO_INNER 0.866025404 #define OUTER_RADIUS 10 #define CHUNK_SIZE_X 5 #define TILING_SCALE (1 / (CHUNK_SIZE_X * 2 * OUTER_RADIUS / OUTER_TO_INNER))
      
      





これにより、タイルスケールが0.00866025404になります。この値の整数倍を使用する場合、テクスチャリングはフラグメントのテレポーテーションの影響を受けません。さらに、マップの東と西のエッジのテクスチャは、接続を正しく三角形分割した後、シームレスに結合します。テレイン



シェーダーのUVスケールとして0.02を使用しました代わりに、0.01732050808の2倍のタイルスケールを使用できます。スケールは以前よりも少し小さくなり、テクスチャのスケールはわずかに増加しましたが、視覚的には見えません。



  #include "../HexMetrics.cginc" #include "../HexCellData.cginc" … float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3( IN.worldPos.xz * (2 * TILING_SCALE), IN.terrain[index] ); … }
      
      





UVノイズのRoadsシェーダーでは、0.025のスケールを使用しました。代わりに、トリプルタイルスケールを使用できます。これにより0.02598076212が得られますが、これはかなり近い値です。



  #include "HexMetrics.cginc" #include "HexCellData.cginc" … void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * (3 * TILING_SCALE)); … }
      
      





最後に、Water.cgincでは、泡に0.015、波に0.025を使用しました。ここでも、これらの値を2倍および3倍のタイルスケールに置き換えることができます。



 #include "HexMetrics.cginc" float Foam (float shore, float2 worldXZ, sampler2D noiseTex) { shore = sqrt(shore) * 0.9; float2 noiseUV = worldXZ + _Time.y * 0.25; float4 noise = tex2D(noiseTex, noiseUV * (2 * TILING_SCALE)); … } … float Waves (float2 worldXZ, sampler2D noiseTex) { float2 uv1 = worldXZ; uv1.y += _Time.y; float4 noise1 = tex2D(noiseTex, uv1 * (3 * TILING_SCALE)); float2 uv2 = worldXZ; uv2.x += _Time.y; float4 noise2 = tex2D(noiseTex, uv2 * (3 * TILING_SCALE)); … }
      
      





ユニティパッケージ



東と西の連合



この段階では、マップを折り畳むことの唯一の視覚的証拠は、最東列と最西列の間の小さなギャップです。このギャップは、折りたたむことなくマップの反対側のセル間のエッジと角度の接続をまだ三角形分割していないために発生します。









端のスペースバー。



折りたたみ隣人



東西接続を三角測量するために、反対側のセルを互いに隣接させる必要があります。これまではHexGrid.CreateCell



、Xのインデックスがゼロより大きい場合にのみ、前のセルとのE-W接続が確立されるため、これを行っていませんこの接続を折りたたむには、マップの折り畳みがオンになっているときに、行の最後のセルを同じ行の最初のセルに接続する必要があります。



  void CreateCell (int x, int z, int i) { … if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); if (wrapping && x == cellCountX - 1) { cell.SetNeighbor(HexDirection.E, cells[i - x]); } } … }
      
      





隣人E – Wの接続を確立したら、ギャップの部分的な三角形分割を取得します。歪みが誤って隠されているため、エッジの接続は理想的ではありません。これについては後で対処します。









化合物E – W



NE-SWリンクも折りたたむ必要があります。これは、各偶数行の最初のセルを前の行の最後のセルに接続することで実行できます。前のセルになります。



  if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - cellCountX]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - cellCountX - 1]); } else if (wrapping) { cell.SetNeighbor(HexDirection.SW, cells[i - 1]); } } else { … } }
      
      











NE – SW接続。



最後に、SE – NW接続は、最初の下の各奇数行の終わりに確立されます。これらのセルは、前の行の最初のセルに接続する必要があります。



  if (z > 0) { if ((z & 1) == 0) { … } else { cell.SetNeighbor(HexDirection.SW, cells[i - cellCountX]); if (x < cellCountX - 1) { cell.SetNeighbor(HexDirection.SE, cells[i - cellCountX + 1]); } else if (wrapping) { cell.SetNeighbor( HexDirection.SE, cells[i - cellCountX * 2 + 1] ); } } }
      
      











コンパウンドSE – NW。



ノイズフォールディング



ギャップを完全に隠すには、頂点の位置を歪めるために使用されるノイズが、マップの東端と西端で完全に揃うようにする必要があります。シェーダーに使用したのと同じトリックを使用できますが、ノイズスケールは歪みに対して0.003でした。タイルを確実に作成するには、スケールを大幅に大きくする必要があります。これにより、頂点がより混distortionとした歪みになります。



別の解決策は、ノイズをテーリングすることではなく、マップの端でノイズを滑らかに減衰させることです。1つのセルの幅に沿って滑らかな減衰を実行すると、歪みによりギャップのない滑らかな遷移が作成されます。この領域のノイズはわずかに滑らかになり、長距離からは変化がシャープに見えますが、頂点のわずかな歪みを使用する場合、これはそれほど明白ではありません。



温度変動はどうですか?
. , . , . , .


カードを折りたたまない場合は、HexMetrics.SampleNoise



1つのサンプルで対応できますしかし、折り畳むときは減衰を追加する必要があります。したがって、サンプルを返す前に、変数に保存してください。



  public static Vector4 SampleNoise (Vector3 position) { Vector4 sample = noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); return sample; }
      
      





最小化する場合、2番目のサンプルと混合する必要があります。マップの東部でトランジションを実行するため、2番目のサンプルを西に移動する必要があります。



  Vector4 sample = noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); if (Wrapping && position.x < innerDiameter) { Vector4 sample2 = noiseSource.GetPixelBilinear( (position.x + wrapSize * innerDiameter) * noiseScale, position.z * noiseScale ); }
      
      





減衰は、1つのセルの幅にわたって、西部から東部への単純な線形補間を使用して実行されます。



  if (Wrapping && position.x < innerDiameter) { Vector4 sample2 = noiseSource.GetPixelBilinear( (position.x + wrapSize * innerDiameter) * noiseScale, position.z * noiseScale ); sample = Vector4.Lerp( sample2, sample, position.x * (1f / innerDiameter) ); }
      
      











ノイズミキシング、不完全なソリューション。



その結果、東側のセルの一部が負のX座標を持つため、完全に一致しません。この領域に近づかないように、遷移領域をセル幅の西半分に移動しましょう。



  if (Wrapping && position.x < innerDiameter * 1.5f) { Vector4 sample2 = noiseSource.GetPixelBilinear( (position.x + wrapSize * innerDiameter) * noiseScale, position.z * noiseScale ); sample = Vector4.Lerp( sample2, sample, position.x * (1f / innerDiameter) - 0.5f ); }
      
      











正しい減衰。



セル編集



三角形分割が正しいようになったので、地図と折り畳みの継ぎ目ですべてを編集できることを確認しましょう。結局のところ、テレポートされたフラグメントでは、座標が誤っており、大きなブラシが縫い目で切れています。









ブラシがトリミングされます。



これを修正するには、HexCoordinates



折りたたみを報告する必要があります。これを行うには、コンストラクターメソッドでX座標を一致させます。軸座標Xは、オフセットのX座標からZ座標の半分を減算することで取得されることがわかっているため、この情報を使用して逆変換を実行し、ゼロ座標が小さいかどうかを確認できます。その場合、展開されたマップの東側を超えた座標があります。各方向でマップの半分以下をテレポートするため、折り畳みサイズをXに1回追加するだけで十分です。そして、オフセット座標が折りたたみサイズよりも大きい場合、減算を実行する必要があります。



  public HexCoordinates (int x, int z) { if (HexMetrics.Wrapping) { int oX = x + z / 2; if (oX < 0) { x += HexMetrics.wrapSize; } else if (oX >= HexMetrics.wrapSize) { x -= HexMetrics.wrapSize; } } this.x = x; this.z = z; }
      
      





マップの下部または上部を編集するときにエラー

が発生することがあります。これは、頂点のゆがみにより、マップの外側のセルの行にカーソルが表示される場合に発生します。これはHexGrid.GetCell



、ベクトルパラメータと座標を一致させないために発生するバグですこれを修正GetCell



するには、必要なチェックを実行するパラメーターとして座標を持つメソッド適用します。



  public HexCell GetCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); // int index = // coordinates.X + coordinates.Z * cellCountX + coordinates.Z / 2; // return cells[index]; return GetCell(coordinates); }
      
      





沿岸折りたたみ



三角測量は地形に適していますが、東西の継ぎ目に沿って、水の海岸の端はありません。実際、彼らはただ、崩壊しません。それらは反転され、マップの反対側に引き伸ばされます。









水の端がありません。



これは、海岸の水を三角測量するときに、隣人の位置を使用するためです。これを修正するには、カードの反対側を扱っていると判断する必要があります。タスクを簡素化するHexCell



ために、インデックスのプロパティにセル列を追加します。



  public int ColumnIndex { get; set; }
      
      





このインデックスをに割り当てますHexGrid.CreateCell



これは、オフセット座標Xをフラグメントサイズで割ったものに単純に等しくなります。



  void CreateCell (int x, int z, int i) { … cell.Index = i; cell.ColumnIndex = x / HexMetrics.chunkSizeX; … }
      
      





HexGridChunk.TriangulateWaterShore



現在のセルとその隣のセルの列インデックスを比較することで、最小化されているもの判断できます。隣人の列のインデックスが1段階下の場合、西側にいます。隣人は東側にいます。したがって、隣人を西に向ける必要があります。同様に、反対方向に。



  Vector3 center2 = neighbor.Position; if (neighbor.ColumnIndex < cell.ColumnIndex - 1) { center2.x += HexMetrics.wrapSize * HexMetrics.innerDiameter; } else if (neighbor.ColumnIndex > cell.ColumnIndex + 1) { center2.x -= HexMetrics.wrapSize * HexMetrics.innerDiameter; }
      
      











海岸のRi骨、しかし角はありません。



そのため、私たちは海岸のrib骨の世話をしましたが、これまでのところコーナーを扱っていませんでした。次の隣人にも同じことをする必要があります。



  if (nextNeighbor != null) { Vector3 center3 = nextNeighbor.Position; if (nextNeighbor.ColumnIndex < cell.ColumnIndex - 1) { center3.x += HexMetrics.wrapSize * HexMetrics.innerDiameter; } else if (nextNeighbor.ColumnIndex > cell.ColumnIndex + 1) { center3.x -= HexMetrics.wrapSize * HexMetrics.innerDiameter; } Vector3 v3 = center3 + (nextNeighbor.IsUnderwater ? HexMetrics.GetFirstWaterCorner(direction.Previous()) : HexMetrics.GetFirstSolidCorner(direction.Previous())); … }
      
      











適切に縮小された海岸。



カード生成



東と西を接続するオプションは、マップの生成に影響します。マップを最小化する場合、生成アルゴリズムも最小化する必要があります。これにより、別のマップが作成されますが、ゼロ以外のマップボーダーXを使用する場合折り畳みは明らかではありません。















デフォルト設定の大きなマップ1208905299。折り畳みありとなし。



最小化を使用しても意味がありません場合は地図ボーダーにXを。しかし、同時にリージョンがマージされるため、単にそれを取り除くことはできません。代わりに折りたたむ場合は、単にRegionBorderを使用できますすべての場合で



変更してください。この新しい変数は、collapseオプションの値に応じて、またはまたは等しくなります。以下に、最初のケースのみの変更を示しました。HexMapGenerator.CreateRegions



mapBorderX



borderX



regionBorder



mapBorderX







  int borderX = grid.wrapping ? regionBorder : mapBorderX; MapRegion region; switch (regionCount) { default: region.xMin = borderX; region.xMax = grid.cellCountX - borderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); break; … }
      
      





同時に、地域は分離されたままですが、これはマップの東側と西側に異なる地域がある場合にのみ必要です。これが尊重されない場合が2つあります。1つは、リージョンが1つしかない場合です。2つ目は、マップを水平に分割する2つの領域がある場合です。これらの場合、borderX



ゼロの値を割り当てることができます。これにより、陸地が東西の継ぎ目を横切ることができます。



  switch (regionCount) { default: if (grid.wrapping) { borderX = 0; } region.xMin = borderX; region.xMax = grid.cellCountX - borderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); break; case 2: if (Random.value < 0.5f) { … } else { if (grid.wrapping) { borderX = 0; } region.xMin = borderX; region.xMax = grid.cellCountX - borderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); } break; … }
      
      











1つの地域が崩壊しています。



一見、すべてが正しく機能しているように見えますが、実際には継ぎ目に沿ってギャップがあります。これは、侵食率をゼロに設定すると、より顕著になります。















侵食が無効になると、レリーフの継ぎ目が目立つようになります。



継ぎ目がレリーフ破片の成長を妨げるため、ギャップが発生します。最初に追加されるものを決定するために、セルからフラグメントの中心までの距離が使用され、マップの反対側のセルは非常に遠くなる可能性があるため、ほとんど点灯しません。もちろん、これは間違っています。HexCoordinates.DistanceTo



最小化されたマップについて知っていることを確認する必要があります。



間の距離を計算しHexCoordinates



、3つの軸のそれぞれに沿った絶対距離を合計し、結果を半分にします。 Zに沿った距離は常に真ですが、それに沿って折りたたむとXとYの距離に影響する場合があります。それでは、X + Yの個別の計算から始めましょう。



  public int DistanceTo (HexCoordinates other) { // 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; int xy = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); return (xy + (z < other.z ? other.z - z : z - other.z)) / 2; }
      
      





折り畳むことで任意のセルの距離が短くなるかどうかを判断するのは簡単な作業ではないため、別の座標を西側に折り畳む場合のX + Yを計算してみましょう。値が元のX + Yより小さい場合は、それを使用します。



  int xy = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (HexMetrics.Wrapping) { other.x += HexMetrics.wrapSize; int xyWrapped = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (xyWrapped < xy) { xy = xyWrapped; } }
      
      





これにより距離が短くならない場合は、他の方向に短く回すことができるため、確認します。



  if (HexMetrics.Wrapping) { other.x += HexMetrics.wrapSize; int xyWrapped = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (xyWrapped < xy) { xy = xyWrapped; } else { other.x -= 2 * HexMetrics.wrapSize; xyWrapped = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (xyWrapped < xy) { xy = xyWrapped; } } }
      
      





これで、折りたたみ可能なマップで常に最短距離を取得できます。地形の破片は継ぎ目によってブロックされなくなり、陸地が丸まることができます。















侵食と侵食のないレリーフを正しく折りたたみます。



ユニティパッケージ



世界を旅する



マップの生成と三角測量を検討したので、次に分隊、調査、および可視性のチェックに移りましょう。



テストシーム



チームを世界中に移動するときに最初に遭遇する障害は、探索できないマップの端です。









カードの継ぎ目を調べることはできません。



マップの端に沿ったセルは、マップの突然の完了を隠すために未探索にされます。ただし、マップを最小化すると、北と南のセルのみがマークされ、東と西のセルはマークされません。HexGrid.CreateCell



これを考慮して変更してください。



  if (wrapping) { cell.Explorable = z > 0 && z < cellCountZ - 1; } else { cell.Explorable = x > 0 && z > 0 && x < cellCountX - 1 && z < cellCountZ - 1; }
      
      





レリーフ機能の可視性



次に、継ぎ目に沿って可視性が機能するかどうかを確認しましょう。地形に対しては機能しますが、地形機能に対しては機能しません。折りたたまれているオブジェクトは、折りたたまれていない最後のセルの可視性を取得しているように見えます。









オブジェクトの不正確な可視性。



これは、使用するテクスチャフォールディングモードにHexCellShaderData



クランプモードが設定されているために発生しますこの問題を解決するには、クランプモードを変更して繰り返します。ただし、これInitialize



Uの座標に対してのみ行う必要があるため、個別に設定wrapModeU



wrapModeV



ます。



  public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; // cellTexture.wrapMode = TextureWrapMode.Clamp; cellTexture.wrapModeU = TextureWrapMode.Repeat; cellTexture.wrapModeV = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } … }
      
      





分隊と列



別の問題は、ユニットがまだ折り畳まれていないことです。ユニットが配置されている列を移動した後、ユニットは同じ場所に残ります。









ユニットは転送されず、反対側にあります。



この問題は、フラグメントで行ったように、分隊の子要素を列にすることで解決できます。まず、それらをグリッドの直接の子にしませんHexGrid.AddUnit







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





ユニットが移動すると、別の列に表示される場合があります。つまり、親を変更する必要があります。これを可能にするために、HexGrid



一般メソッドに追加し、MakeChildOfColumn



パラメーターとしてTransform



子要素のコンポーネントと列インデックスを渡します。



  public void MakeChildOfColumn (Transform child, int columnIndex) { child.SetParent(columns[columnIndex], false); }
      
      





プロパティが設定されたときにこのメソッドを呼び出しますHexUnit.Location







  public HexCell Location { … set { … Grid.MakeChildOfColumn(transform, value.ColumnIndex); } }
      
      





これにより、ユニット作成の問題が解決されます。ただし、移動するときに目的の列に移動する必要もあります。これを行うには、HexUnit.TravelPath



インデックスの現在の列を追跡する必要があります。このメソッドの最初では、これはパスの最初のセル列のインデックス、または再コンパイルによって移動が中断された場合は現在のインデックスです。



  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); // Grid.DecreaseVisibility( // currentTravelLocation ? currentTravelLocation : pathToTravel[0], // VisionRange // ); if (!currentTravelLocation) { currentTravelLocation = pathToTravel[0]; } Grid.DecreaseVisibility(currentTravelLocation, VisionRange); int currentColumn = currentTravelLocation.ColumnIndex; … }
      
      





移動の各反復中に、次の列のインデックスが異なるかどうかを確認し、そうであれば、順序の親を変更します。



  int currentColumn = currentTravelLocation.ColumnIndex; float t = Time.deltaTime * travelSpeed; for (int i = 1; i < pathToTravel.Count; i++) { … Grid.IncreaseVisibility(pathToTravel[i], VisionRange); int nextColumn = currentTravelLocation.ColumnIndex; if (currentColumn != nextColumn) { Grid.MakeChildOfColumn(transform, nextColumn); currentColumn = nextColumn; } … }
      
      





これにより、ユニットはフラグメントと同様に移動できます。ただし、カードの継ぎ目を移動するとき、ユニットはまだ崩壊していません。代わりに、彼らは突然間違った方向に動き始めます。これは、継ぎ目の位置に関係なく発生しますが、最も顕著なのは、マップ全体を飛び越えたときです。





マップ全体で競馬。



ここでは、海岸で使用されたのと同じアプローチを使用できますが、今回は、デタッチメントが移動するカーブをオフにします。次の列が東に向いている場合、他の方向についても同様に、曲線を東にテレポートします。曲線の制御点を変更する必要があるa



b



、それはまた、コントロールポイントに影響を与えますc







  for (int i = 1; i < pathToTravel.Count; i++) { currentTravelLocation = pathToTravel[i]; a = c; b = pathToTravel[i - 1].Position; // c = (b + currentTravelLocation.Position) * 0.5f; // Grid.IncreaseVisibility(pathToTravel[i], VisionRange); int nextColumn = currentTravelLocation.ColumnIndex; if (currentColumn != nextColumn) { if (nextColumn < currentColumn - 1) { ax -= HexMetrics.innerDiameter * HexMetrics.wrapSize; bx -= HexMetrics.innerDiameter * HexMetrics.wrapSize; } else if (nextColumn > currentColumn + 1) { ax += HexMetrics.innerDiameter * HexMetrics.wrapSize; bx += HexMetrics.innerDiameter * HexMetrics.wrapSize; } Grid.MakeChildOfColumn(transform, nextColumn); currentColumn = nextColumn; } c = (b + currentTravelLocation.Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], VisionRange); … }
      
      







折りたたみ式の動き。



最後に行うことは、チームが移動する最初のセルを見たときにチームの最初のターンを変更することです。このセルが東西の継ぎ目の反対側にある場合、ユニットは間違った方向を向いています。



地図を最小化するとき、正確に北または南にないポイントを見るには2つの方法があります。あなたは東または西を見ることができます。それは移動の方向でもあるため、ポイントに最も近い距離に対応する方向を見るのが論理的ですので、で使用しましょうLookAt







最小化するとき、X軸に沿った相対距離をチェックします。マップの幅の負の半分よりも小さい場合は、西に目を向ける必要があります。それ以外の場合、距離がマップの幅の半分よりも大きい場合は、東に折りたたむ必要があります。



  IEnumerator LookAt (Vector3 point) { if (HexMetrics.Wrapping) { float xDistance = point.x - transform.localPosition.x; if (xDistance < -HexMetrics.innerRadius * HexMetrics.wrapSize) { point.x += HexMetrics.innerDiameter * HexMetrics.wrapSize; } else if (xDistance > HexMetrics.innerRadius * HexMetrics.wrapSize) { point.x -= HexMetrics.innerDiameter * HexMetrics.wrapSize; } } … }
      
      





したがって、折りたたみ機能を備えた完全に機能するマップがあります。これで、六角形マップに関する一連のチュートリアルが終了しました。前のセクションで述べたように、他のトピックも検討できますが、六角形マップに固有のものではありません。おそらく、今後の一連のチュートリアルでそれらを検討します。



最後のパッケージをダウンロードし、プレイモードでターンエラーを取得しました
, Rotation . . . 5.


最後のパッケージをダウンロードしましたが、スクリーンショットのようにグラフィックが美しくありません
. - .


最後のパッケージをダウンロードしましたが、同じカードが常に生成されます
seed (1208905299), . , Use Fixed Seed .


ユニティパッケージ



All Articles