Unity C#で手続き的に生成された世界地図、パート3

画像



これは、UnityとC#を使用して手続き的に生成された世界の地図に関するシリーズの3番目の記事です。 サイクルは4つの記事で構成されます。



内容



パート1



はじめに

ノイズ発生

はじめに

DEM生成



パート2



1つの軸でカードを最小化する

両軸でカードを最小化する

隣接する要素を検索する

ビットマスク

注ぐ



パート3(この記事):



ヒートマップの生成

湿度マップの生成

川の世代



パート4



バイオーム生成

球面地図の生成



ヒートマップの生成



ヒートマップは、生成されたワールドの温度を決定します。 作成するヒートマップは、標高と緯度のデータに基づきます。 単純なノイズ勾配で緯度データを取得できます。 偶発的なノイズライブラリは、次の機能を提供します。



ImplicitGradient gradient = new ImplicitGradient (1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1);
      
      





世界を崩壊させると、熱勾配としてY軸に沿った単一の勾配で十分です。



ヒートマップテクスチャを生成するには、TextureGeneratorクラスに新しい関数を追加します。 これにより、ヒートマップで発生する変更を視覚的に追跡できます。



 public static Texture2D GetHeatMapTexture(int width, int height, Tile[,] tiles) { var texture = new Texture2D(width, height); var pixels = new Color[width * height]; for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { pixels[x + y * width] = Color.Lerp(Color.blue, Color.red, tiles[x,y].HeatValue); //darken the color if a edge tile if (tiles[x,y].Bitmask != 15) pixels[x + y * width] = Color.Lerp(pixels[x + y * width], Color.black, 0.4f); } } texture.SetPixels(pixels); texture.wrapMode = TextureWrapMode.Clamp; texture.Apply(); return texture; }
      
      





温度勾配は次のようになります。



画像



地球の赤道と同様に、地図の中央に暖かいストリップが必要なので、このデータは素晴らしい出発点です。 これがヒートマップの基礎となり、そこから作業を開始します。



次に、記事の前の部分のHeightTypeエリア(高さのタイプ)に似たHeatTypeエリア(ヒートタイプ)を割り当てる必要があります。



 public enum HeatType { Coldest, Colder, Cold, Warm, Warmer, Warmest }
      
      





新しい変数を使用して、Unityインスペクターからこれらのタイプの熱をカスタマイズ可能にします。



 float ColdestValue = 0.05f; float ColderValue = 0.18f; float ColdValue = 0.4f; float WarmValue = 0.6f; float WarmerValue = 0.8f;
      
      





LoadTilesでは、発熱量に基づいて、各タイルにHeatTypeを割り当てます。



 //    if (heatValue < ColdestValue) t.HeatType = HeatType.Coldest; else if (heatValue < ColderValue) t.HeatType = HeatType.Colder; else if (heatValue < ColdValue) t.HeatType = HeatType.Cold; else if (heatValue < WarmValue) t.HeatType = HeatType.Warm; else if (heatValue < WarmerValue) t.HeatType = HeatType.Warmer; else t.HeatType = HeatType.Warmest;
      
      





これで、各HeatTypeの新しい色をTextureGeneratorクラスに追加できます。



 //    private static Color Coldest = new Color(0, 1, 1, 1); private static Color Colder = new Color(170/255f, 1, 1, 1); private static Color Cold = new Color(0, 229/255f, 133/255f, 1); private static Color Warm = new Color(1, 1, 100/255f, 1); private static Color Warmer = new Color(1, 100/255f, 0, 1); private static Color Warmest = new Color(241/255f, 12/255f, 0, 1); public static Texture2D GetHeatMapTexture(int width, int height, Tile[,] tiles) { var texture = new Texture2D(width, height); var pixels = new Color[width * height]; for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { switch (tiles[x,y].HeatType) { case HeatType.Coldest: pixels[x + y * width] = Coldest; break; case HeatType.Colder: pixels[x + y * width] = Colder; break; case HeatType.Cold: pixels[x + y * width] = Cold; break; case HeatType.Warm: pixels[x + y * width] = Warm; break; case HeatType.Warmer: pixels[x + y * width] = Warmer; break; case HeatType.Warmest: pixels[x + y * width] = Warmest; break; } // ,     if (tiles[x,y].Bitmask != 15) pixels[x + y * width] = Color.Lerp(pixels[x + y * width], Color.black, 0.4f); } } texture.SetPixels(pixels); texture.wrapMode = TextureWrapMode.Clamp; texture.Apply(); return texture; }
      
      





このヒートマップを生成すると、次の画像が得られます。



画像



これで、割り当てられたHeatTypeエリアを明確に見ることができます。 ただし、これらのデータはこれまでのバンドにすぎません。 緯度に基づいた温度データのみを教えてくれます。 実際には、温度は多くの要因に依存するため、フラクタルノイズとこの勾配ノイズを混合します。



Generatorにいくつかの新しい変数と新しいフラクタルを追加します。



 int HeatOctaves = 4; double HeatFrequency = 3.0; private void Initialize() { //    ImplicitGradient gradient = new ImplicitGradient (1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1); ImplicitFractal heatFractal = new ImplicitFractal(FractalType.MULTI, BasisType.SIMPLEX, InterpolationType.QUINTIC, HeatOctaves, HeatFrequency, Seed); //      HeatMap = new ImplicitCombiner (CombinerType.MULTIPLY); HeatMap.AddSource (gradient); HeatMap.AddSource (heatFractal); }
      
      





乗算演算を使用してフラクタルと勾配を組み合わせると、緯度に基づいて最終的なノイズが乗算されます。 乗算操作を以下に示します。



画像



左側は勾配ノイズ、中央はフラクタルノイズ、右側は乗算演算の結果です。 ご覧のとおり、より快適なヒートマップが用意されています。



では、緯度を見てみましょう。 高さマップを検討する必要があります。最も高い山の山頂を寒くしたいのです。 これはLoadTiles関数で構成できます。



 //      .  =  if (t.HeightType == HeightType.Grass) { HeatData.Data[tX, tY] -= 0.1f * t.HeightValue; } else if (t.HeightType == HeightType.Forest) { HeatData.Data[tX, tY] -= 0.2f * t.HeightValue; } else if (t.HeightType == HeightType.Rock) { HeatData.Data[tX, tY] -= 0.3f * t.HeightValue; } else if (t.HeightType == HeightType.Snow) { HeatData.Data[tX, tY] -= 0.4f * t.HeightValue; }
      
      





この設定は、緯度と高さの両方を考慮した最終ヒートマップを提供します。



画像



湿度マップの生成



湿度マップはヒートマップのようなものです。 最初に、フラクタルを生成して、ランダムな値からベースを埋めます。 次に、ヒートマップに基づいてこのデータを変更します。



ヒートマップコードに非常に似ているため、水分生成コードを簡単に説明します。



まず、新しいMoistureTypeでTileクラスを拡張しましょう。



 public enum MoistureType { Wettest, Wetter, Wet, Dry, Dryer, Dryest }
      
      





Generatorクラスには、Unity Inspectorから見える新しい変数が必要です。



 int MoistureOctaves = 4; double MoistureFrequency = 3.0; float DryerValue = 0.27f; float DryValue = 0.4f; float WetValue = 0.6f; float WetterValue = 0.8f; float WettestValue = 0.9f;
      
      





TextureGeneratorには、新しいMoistureMap関数とそれに関連する色が必要です。



 //  private static Color Dryest = new Color(255/255f, 139/255f, 17/255f, 1); private static Color Dryer = new Color(245/255f, 245/255f, 23/255f, 1); private static Color Dry = new Color(80/255f, 255/255f, 0/255f, 1); private static Color Wet = new Color(85/255f, 255/255f, 255/255f, 1); private static Color Wetter = new Color(20/255f, 70/255f, 255/255f, 1); private static Color Wettest = new Color(0/255f, 0/255f, 100/255f, 1);
      
      





 public static Texture2D GetMoistureMapTexture(int width, int height, Tile[,] tiles) { var texture = new Texture2D(width, height); var pixels = new Color[width * height]; for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { Tile t = tiles[x,y]; if (t.MoistureType == MoistureType.Dryest) pixels[x + y * width] = Dryest; else if (t.MoistureType == MoistureType.Dryer) pixels[x + y * width] = Dryer; else if (t.MoistureType == MoistureType.Dry) pixels[x + y * width] = Dry; else if (t.MoistureType == MoistureType.Wet) pixels[x + y * width] = Wet; else if (t.MoistureType == MoistureType.Wetter) pixels[x + y * width] = Wetter; else pixels[x + y * width] = Wettest; } } texture.SetPixels(pixels); texture.wrapMode = TextureWrapMode.Clamp; texture.Apply(); return texture; }
      
      





最後に、LoadTiles関数は、水分値(MoistureValue)に基づいて湿度のタイプ(MoistureType)を設定します。



 //   float moistureValue = MoistureData.Data[x,y]; moistureValue = (moistureValue - MoistureData.Min) / (MoistureData.Max - MoistureData.Min); t.MoistureValue = moistureValue; //   if (moistureValue < DryerValue) t.MoistureType = MoistureType.Dryest; else if (moistureValue < DryValue) t.MoistureType = MoistureType.Dryer; else if (moistureValue < WetValue) t.MoistureType = MoistureType.Dry; else if (moistureValue < WetterValue) t.MoistureType = MoistureType.Wet; else if (moistureValue < WettestValue) t.MoistureType = MoistureType.Wetter; else t.MoistureType = MoistureType.Wettest;
      
      





MoistureMapの元のノイズをレンダリングすると、次の結果が得られます。



画像



残っているのは、高さマップに従って湿度マップを設定することだけです。 LoadTiles関数でこれを行います。



 //    if (t.HeightType == HeightType.DeepWater) { MoistureData.Data[tX, tY] += 8f * t.HeightValue; } else if (t.HeightType == HeightType.ShallowWater) { MoistureData.Data[tX, tY] += 3f * t.HeightValue; } else if (t.HeightType == HeightType.Shore) { MoistureData.Data[tX, tY] += 1f * t.HeightValue; } else if (t.HeightType == HeightType.Sand) { MoistureData.Data[tX, tY] += 0.25f * t.HeightValue; }
      
      





特定のタイルの高さに応じて湿度マップを設定した後、更新された湿度マップはより良く見えます。



画像



川の世代



私が説明する川の生成方法は、力ずくで説得力のある川を作成する問題を解決する試みです。



アルゴリズムの最初のステップは、マップ上のランダムなタイルを選択することです。 選択したタイルは土地でなければならず、特定の境界線より上の高さの値を持つ必要があります。



このタイルから始めて、すべての下位に位置する隣接タイルを決定し、それに移動します。 このようにして、水のタイルに達するまでパスを作成します。



生成されたパスが基準(川の長さ、ベンドの数、交差点の数)を満たす場合、将来の使用のために保存します。



それ以外の場合は、破棄して再試行します。 以下のコードを使用して開始できます。



 private void GenerateRivers() { int attempts = 0; int rivercount = RiverCount; Rivers = new List<River> (); //   while (rivercount > 0 && attempts < MaxRiverAttempts) { //    int x = UnityEngine.Random.Range (0, Width); int y = UnityEngine.Random.Range (0, Height); Tile tile = Tiles[x,y]; //   if (!tile.Collidable) continue; if (tile.Rivers.Count > 0) continue; if (tile.HeightValue > MinRiverHeight) { //      River river = new River(rivercount); // ,        river.CurrentDirection = tile.GetLowestNeighbor (); //      FindPathToWater(tile, river.CurrentDirection, ref river); //     if (river.TurnCount < MinRiverTurns || river.Tiles.Count < MinRiverLength || river.Intersections > MaxRiverIntersections) { //   -    for (int i = 0; i < river.Tiles.Count; i++) { Tile t = river.Tiles[i]; t.Rivers.Remove (river); } } else if (river.Tiles.Count >= MinRiverLength) { //  -     Rivers.Add (river); tile.Rivers.Add (river); rivercount--; } } attempts++; } }
      
      





FindPathToWater()再帰関数は、土地の高さ、既存の川、優先方向に基づいて選択する最適なパスを決定します。 遅かれ早かれ、彼女は水タイルへの道を見つけるでしょう。 この関数は、パスが完了するまで再帰的に呼び出されます。



 private void FindPathToWater(Tile tile, Direction direction, ref River river) { if (tile.Rivers.Contains (river)) return; // ,        if (tile.Rivers.Count > 0) river.Intersections++; river.AddTile (tile); //    Tile left = GetLeft (tile); Tile right = GetRight (tile); Tile top = GetTop (tile); Tile bottom = GetBottom (tile); float leftValue = int.MaxValue; float rightValue = int.MaxValue; float topValue = int.MaxValue; float bottomValue = int.MaxValue; //     if (left.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(left)) leftValue = left.HeightValue; if (right.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(right)) rightValue = right.HeightValue; if (top.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(top)) topValue = top.HeightValue; if (bottom.GetRiverNeighborCount(river) < 2 && !river.Tiles.Contains(bottom)) bottomValue = bottom.HeightValue; //     ,   ,    if (bottom.Rivers.Count == 0 && !bottom.Collidable) bottomValue = 0; if (top.Rivers.Count == 0 && !top.Collidable) topValue = 0; if (left.Rivers.Count == 0 && !left.Collidable) leftValue = 0; if (right.Rivers.Count == 0 && !right.Collidable) rightValue = 0; //  ,     if (direction == Direction.Left) if (Mathf.Abs (rightValue - leftValue) < 0.1f) rightValue = int.MaxValue; if (direction == Direction.Right) if (Mathf.Abs (rightValue - leftValue) < 0.1f) leftValue = int.MaxValue; if (direction == Direction.Top) if (Mathf.Abs (topValue - bottomValue) < 0.1f) bottomValue = int.MaxValue; if (direction == Direction.Bottom) if (Mathf.Abs (topValue - bottomValue) < 0.1f) topValue = int.MaxValue; //   float min = Mathf.Min (Mathf.Min (Mathf.Min (leftValue, rightValue), topValue), bottomValue); //     -  if (min == int.MaxValue) return; //    if (min == leftValue) { if (left.Collidable) { if (river.CurrentDirection != Direction.Left){ river.TurnCount++; river.CurrentDirection = Direction.Left; } FindPathToWater (left, direction, ref river); } } else if (min == rightValue) { if (right.Collidable) { if (river.CurrentDirection != Direction.Right){ river.TurnCount++; river.CurrentDirection = Direction.Right; } FindPathToWater (right, direction, ref river); } } else if (min == bottomValue) { if (bottom.Collidable) { if (river.CurrentDirection != Direction.Bottom){ river.TurnCount++; river.CurrentDirection = Direction.Bottom; } FindPathToWater (bottom, direction, ref river); } } else if (min == topValue) { if (top.Collidable) { if (river.CurrentDirection != Direction.Top){ river.TurnCount++; river.CurrentDirection = Direction.Top; } FindPathToWater (top, direction, ref river); } } }
      
      





川を生成するプロセスの後、水に通じるいくつかの経路ができます。 次のようになります。



画像画像



多くのパスが交差しており、これらの川を掘ると、交差点でサイズが一致しないため、少し奇妙に見えます。 したがって、どの川が交差しているかを判断し、それらをグループ化する必要があります。



RiverGroupクラスが必要です。



 public class RiverGroup { public List<River> Rivers = new List<River>(); }
      
      





また、交差する川をグループ化するコード:



 private void BuildRiverGroups() { //   ,      for (var x = 0; x < Width; x++) { for (var y = 0; y < Height; y++) { Tile t = Tiles[x,y]; if (t.Rivers.Count > 1) { //   ==  RiverGroup group = null; //        ? for (int n=0; n < t.Rivers.Count; n++) { River tileriver = t.Rivers[n]; for (int i = 0; i < RiverGroups.Count; i++) { for (int j = 0; j < RiverGroups[i].Rivers.Count; j++) { River river = RiverGroups[i].Rivers[j]; if (river.ID == tileriver.ID) { group = RiverGroups[i]; } if (group != null) break; } if (group != null) break; } if (group != null) break; } //    --    if (group != null) { for (int n=0; n < t.Rivers.Count; n++) { if (!group.Rivers.Contains(t.Rivers[n])) group.Rivers.Add(t.Rivers[n]); } } else //    --   { group = new RiverGroup(); for (int n=0; n < t.Rivers.Count; n++) { group.Rivers.Add(t.Rivers[n]); } RiverGroups.Add (group); } } } } }
      
      





それで、私たちには川のグループがあり、川を渡って水に向かって流れます。 これらのグループをレンダリングすると、次の結果が得られ、各グループはランダムな色で表されます。



画像



この情報を基に、川を「掘り」始めます。 各河川グループについて、グループ内で最も長い川を掘ることから始めます。 残りの川は最も長い道に沿って掘ります。



以下のコードは、川のグループを掘り始める方法を示しています。



 private void DigRiverGroups() { for (int i = 0; i < RiverGroups.Count; i++) { RiverGroup group = RiverGroups[i]; River longest = null; //       for (int j = 0; j < group.Rivers.Count; j++) { River river = group.Rivers[j]; if (longest == null) longest = river; else if (longest.Tiles.Count < river.Tiles.Count) longest = river; } if (longest != null) { //     DigRiver (longest); for (int j = 0; j < group.Rivers.Count; j++) { River river = group.Rivers[j]; if (river != longest) { DigRiver (river, longest); } } } } }
      
      





可能な限り多くのパラメーターをランダム化しようとするため、リバーコードを掘るのは少し複雑です。



また、川が水に近づくにつれて広がることも重要です。 DigRiver()コードはあまり美しくありませんが、仕事はします:



 private void DigRiver(River river) { int counter = 0; //     ? int size = UnityEngine.Random.Range(1,5); river.Length = river.Tiles.Count; //    int two = river.Length / 2; int three = two / 2; int four = three / 2; int five = four / 2; int twomin = two / 3; int threemin = three / 3; int fourmin = four / 3; int fivemin = five / 3; //     int count1 = UnityEngine.Random.Range (fivemin, five); if (size < 4) { count1 = 0; } int count2 = count1 + UnityEngine.Random.Range(fourmin, four); if (size < 3) { count2 = 0; count1 = 0; } int count3 = count2 + UnityEngine.Random.Range(threemin, three); if (size < 2) { count3 = 0; count2 = 0; count1 = 0; } int count4 = count3 + UnityEngine.Random.Range (twomin, two); // ,         if (count4 > river.Length) { int extra = count4 - river.Length; while (extra > 0) { if (count1 > 0) { count1--; count2--; count3--; count4--; extra--; } else if (count2 > 0) { count2--; count3--; count4--; extra--; } else if (count3 > 0) { count3--; count4--; extra--; } else if (count4 > 0) { count4--; extra--; } } } //   for (int i = river.Tiles.Count - 1; i >= 0 ; i--) { Tile t = river.Tiles[i]; if (counter < count1) { t.DigRiver (river, 4); } else if (counter < count2) { t.DigRiver (river, 3); } else if (counter < count3) { t.DigRiver (river, 2); } else if ( counter < count4) { t.DigRiver (river, 1); } else { t.DigRiver(river, 0); } counter++; } }
      
      





川を掘ると、次のようなものが得られます。



画像



私たちは説得力のある川を見ましたが、マップに湿気を提供することを確認する必要があります。 河川は砂漠地帯には表示されないため、河川周辺が乾燥しているかどうかを確認する必要があります。



このプロセスを簡素化するには、河川データに基づいて湿度マップを設定する新しい関数を追加します。



 private void AdjustMoistureMap() { for (var x = 0; x < Width; x++) { for (var y = 0; y < Height; y++) { Tile t = Tiles[x,y]; if (t.HeightType == HeightType.River) { AddMoisture (t, (int)60); } } } }
      
      





追加される湿度は、元のタイルからの距離に基づいて変化します。 川から遠くなるほど、タイルが受ける水分は少なくなります。



 private void AddMoisture(Tile t, int radius) { int startx = MathHelper.Mod (tX - radius, Width); int endx = MathHelper.Mod (tX + radius, Width); Vector2 center = new Vector2(tX, tY); int curr = radius; while (curr > 0) { int x1 = MathHelper.Mod (tX - curr, Width); int x2 = MathHelper.Mod (tX + curr, Width); int y = tY; AddMoisture(Tiles[x1, y], 0.025f / (center - new Vector2(x1, y)).magnitude); for (int i = 0; i < curr; i++) { AddMoisture (Tiles[x1, MathHelper.Mod (y + i + 1, Height)], 0.025f / (center - new Vector2(x1, MathHelper.Mod (y + i + 1, Height))).magnitude); AddMoisture (Tiles[x1, MathHelper.Mod (y - (i + 1), Height)], 0.025f / (center - new Vector2(x1, MathHelper.Mod (y - (i + 1), Height))).magnitude); AddMoisture (Tiles[x2, MathHelper.Mod (y + i + 1, Height)], 0.025f / (center - new Vector2(x2, MathHelper.Mod (y + i + 1, Height))).magnitude); AddMoisture (Tiles[x2, MathHelper.Mod (y - (i + 1), Height)], 0.025f / (center - new Vector2(x2, MathHelper.Mod (y - (i + 1), Height))).magnitude); } curr--; } }
      
      





この設定では、河川の存在を考慮して、湿度の更新されたマップが提供されます。 次のパートでは、バイオームの生成を開始するときに役立ちます。



更新された湿度マップは次のようになります。



画像



記事の第4部がまもなく準備できます。 これは、世界を作成するために生成したすべてのマップを使用する最良の部分です。



githubの3番目のパートコードのソース: World Generatorパート3

4番目の部分、最後



All Articles