見出し画像

[Unity][C#Script] 自作スクリプトで自然な感じの地形を生成する(ダウンロードあり)

 Unity の標準機能のみをつかって、地形を自動生成してみます。つまり無料でたくさんの地形をつくりまくれるという話です。どのような地形かというと、下のスクリーンショットのような、マインクラフトのような凹凸のある、しかしなだらかにしたような地形です。

画像10

キャラクターが通れるくらいの幅の道があって、段差もある。
→ 待ち構える敵を倒していく、アクションゲームなどに使えるかも。
→ あるきまわってアイテムなどを見つけるゲームに使えるかも。

画像11

こんな雰囲気の、うねうねした自然な感じの「高低差」と「道(隘路)」と「広場」もった地形です。

 使っている機能は「パーリンノイズ」と「メッシュ」です。これをプロシージャル(手続き的)に加工して、ゲームに向いた地形を作ってみようというわけです。

パーリンノイズはマイクラの地形の自動生成にも使われている(と思われる)、連続性のあるランダムです。

メッシュは、いわゆるポリゴンです。これは敷き詰めて地面にできます。

kanさんすごい。北村さんもすごい。いつもお世話になっております。

 つまり、ポリゴンで地形をつくって、パーリンノイズでその頂点を上下させればいいんじゃない? という発想です。

1.  メッシュの生成

 平面を正三角形で埋めるスクリプトを作りました。
 このスクリプトを使って、まずは平面を作ってみます。

 1) Hierarchy に 空の GameObject を作ります。
 2) Mesh Filter を追加します。
 3) Mesh Rendererを追加します。
 4) Projectウィンドウの適当なところで Create > Material にて新規マテリアルを作成し、先程の  GameObject に追加します。
 5) NewScript にて「AutoGenerateFloor」の名前で新規スクリプトを生成し、以下のサンプルを打ち込みました。

サンプルスクリプト。
素直に正方形にしておけばよかった……

// K1Togami 2020/05/31

using UnityEngine;

public class AutoGenerateFloor : MonoBehaviour
{

   public int width = 30;
   public int height = 18;
   public float scale = 5.0f;

   const float TriangleHeight = 0.86660254f;
   const float TriangleHeightDouble = 1.7320508f;

   Mesh mesh;

   [ContextMenu("生成")]
   private void makeGround()
   {

       mesh = new Mesh();

       int p;

       mesh.Clear();


       var vertices = new Vector3[((width + 1) * 2 + 1) * (height + 1) + width + 1];
       var triangles = new int[(width * 2 + 1) * (height * 2 + 2) * 3];

       // メッシュ作成
       // 初段
       for (p = 0; p <= width; p++)
       {
           vertices[p].x = p * scale;
           vertices[p].z = 0f;
           vertices[p].y = 0f;
       }
       for (int i = 0; i <= height; i++)
       {
           // 左端処理
           vertices[p].x = 0f;
           vertices[p].z = i * TriangleHeightDouble * scale + TriangleHeight * scale;
           vertices[p].y = 0f;
           p++;

           for (int j = 0; (j <= width - 1); j++)
           {
               vertices[p].x = j * scale + 0.5f * scale;
               vertices[p].z = i * TriangleHeightDouble * scale + TriangleHeight * scale;
               vertices[p].y = 0f;
               p++;
           }

           // 右端処理
           vertices[p].x = width * scale;
           vertices[p].z = i * TriangleHeightDouble * scale + TriangleHeight * scale;
           vertices[p].y = 0f;
           p++;

           // 縦列はワンループで2つの三角形がペアです。
           for (int j = 0; j <= width; j++)
           {
               vertices[p].x = j * scale;
               vertices[p].z = (i + 1f) * TriangleHeightDouble * scale;
               vertices[p].y = 0f;
               p++;
           }
       }

       p = 0;
       // メッシュ順作成
       for (int i = 0; i <= height; i++)
       {
           for (int j = 0; j < width; j++)
           {
               // 三角形4個を一組にして定義していきます。
               triangles[p + 0] = j + (((width + 1) * 2 + 1) * i);
               triangles[p + 1] = j + (width + 1) * (i * 2 + 1) + i;
               triangles[p + 2] = j + (width + 1) * (i * 2 + 1) + i + 1;

               triangles[p + 3] = j + (((width + 1) * 2 + 1) * i);  // 
               triangles[p + 4] = j + (width + 1) * (i * 2 + 1) + i + 1;
               triangles[p + 5] = j + (((width + 1) * 2 + 1) * i) + 1;

               triangles[p + 6] = j + (width + 1) * (i * 2 + 1) + i;
               triangles[p + 7] = j + (((width + 1) * 2 + 1) * (i + 1));
               triangles[p + 8] = j + (width + 1) * (i * 2 + 1) + i + 1;

               triangles[p + 9] = j + (width + 1) * (i * 2 + 1) + i + 1;
               triangles[p + 10] = j + (((width + 1) * 2 + 1) * (i + 1));
               triangles[p + 11] = j + (((width + 1) * 2 + 1) * (i + 1)) + 1;

               p += 12;
           }
           // 右恥と、左端処理
           triangles[p + 0] = width + (((width + 1) * 2 + 1) * i);
           triangles[p + 1] = width + (width + 1) * (i * 2 + 1) + i;
           triangles[p + 2] = width + (width + 1) * (i * 2 + 1) + i + 1;

           triangles[p + 3] = width + (width + 1) * (i * 2 + 1) + i;
           triangles[p + 4] = width + (((width + 1) * 2 + 1) * (i + 1));
           triangles[p + 5] = width + (width + 1) * (i * 2 + 1) + i + 1;

           p += 6;
       }

       mesh.vertices = vertices;
       mesh.triangles = triangles;

       mesh.RecalculateNormals();
       var filter = GetComponent<MeshFilter>();
       filter.sharedMesh = mesh;
   }
}

 6) Auto Generate Floor のあたりで右クリックすると、「生成」が選べるようになっているはずです。ここで「生成」してみます。

画像2

[ContextMenu("生成")] で、なんとプロパティをエディタから実行できるようになるようです。すばらしい。

画像2

 正三角形が敷き詰められたフロアができました。
 Width と Height と Scale を変更すると、大きさが変更できます。
 あまり大きい値を入れるとエラーになります。

[memo]
・正三角形でつくってあるので、隣接する頂点間の距離が等距離です。
・がんばればヘックスマップに流用できます。
・ヘックスだいすき。

2. パーリンノイズで頂点を上下させます。

 サンプル中の頂点 yを、パーリンノイズで変化させます。
 5箇所書き換えます。

vertices[p].y = 0f;

// ↓

vertices[p].y =  Mathf.PerlinNoise(vertices[p].x, vertices[p].z) * scale;

「実行」するとちょっとモリっとします。

画像3

うーん、イマイチ。周期と振幅のせいだろうとあたりをつけ、
private void makeGround() の前に、周期と振幅を追加します。

   public float wave = 0.05f;
   public float peak = 20f;

この周期と振幅をつかって、パーリンノイズの効き具合を調整します。

vertices[p].y =  Mathf.PerlinNoise(vertices[p].x, vertices[p].z) * scale;

// ↓

vertices[p].y = Mathf.PerlinNoise(vertices[p].x * wave, vertices[p].z * wave) * peak;

実行結果は……

画像4

なるほど、なるほど……「パーリンノイズ」らしくはなりました。
でも、まだまだ全然「ゲームに使えそう」ではありません。

3.ランダムの調整

 乱数を制するものは、ゲ制(ゲーム制作)を制す!
ということで、ここであきらめてはおもしろくありませんし、なんとなくイケる予感もするので、ひとくふうしてみました。
 山のなかに、「道」を作ってみます。
 山の中腹をフラットにして、道をつくるメソッドを追加します。

  float groundHeight(float x, float z)
   {
       float y;
       y = Mathf.PerlinNoise(x * wave, z * wave) * peak;

       // 中間フラットにする。
       if (y > (peak * 0.50f))
       {
           if (y < (peak * 0.65f))
           {
               y = peak * 0.50f;
           }
           else
           {
               y -= peak * 0.15f;
           }
       }

       return y;
   }

 そして、また5箇所書き換えます。

vertices[p].y = Mathf.PerlinNoise(vertices[p].x * wave, vertices[p].z * wave) * peak;

// ↓

vertices[p].y = groundHeight(vertices[p].x, vertices[p].z);

標準値から Wave = 0.01、Peakを80に変更し、「生成」

画像5

 おお、なんとなく、なんとなくイケる気がしてきました。

 というところから、以下の修正を加えました。
・UVを貼れるようにした
・パーリンノイズの周期を3重にし、道を2段にした
・乱数とシードの追加。乱数は地形生成完了ごとに変更される。なので、同じ乱数シードからは同じ地形がつくられる。
・作った地形を保存する機能を追加した。メッシュの保存から保存できる。

4.スクリプトのダウンロードと使い方

以下からダウンロードして試してみることができます。

1) Unityのエディタ上で Hierarchy > Create Empty で Game Object を作ります。

2) Game Object に Add Compornent で Mesh > Mesh Filter と、Mesh > Mesh Renderer を追加します。

3) ダウンロードしたスクリプトを Project に放り込み、Game Object の Add Compornent から追加します。

4) Auto Generate Floor のあたりで右クリックすると、「生成」が選べるようになっているはずです。ここで「生成」できます。

5) MeshCollider を追加すれば、地形の上を歩くこともできます。

6) Static にすれば、NavMeshをつけることもできました。

7) マテリアルをわりあてます。
 手頃なマテリアルに心当たりがない場合は、Unity社から提供されている Standard Assets をインポートして、Environment > TerrainAssets > SurfaceTexture にある、Criff Albedo Specular または GrassRock Albedo Specular をアサインするとよいでしょう。
 マップの大きさにもよりますが、Tiling を X、Y ともに40など大きい値にすると、自然な感じになります。

なお、本スクリプトは特に使用制限はありませんので、ご自由にお使いください。バグがあった場合はごめんなさい。(こっそり教えてください)

画像8

設定のサンプルです。

5.サンプル画像・動画

 空に、超オススメのアセット Mewlist さんの Massive Clouds を使ってみます。そして、地形テクスチャはそのへんにあったもの(自作)を貼ってみました。

パラメータ次第で、意外と表情がかわるかもしれません。
また、興味がございましたら、ぜひスクリプトを改変して、固定にしてある道を作る幅のパラメータをかえたり、敵やアイテムを自動生成などにチャレンジしてみてください。

6.ダンジョン用に改造

現在は、天井をつけてダンジョン用に改造しています。
そのうち公開すると思いますので、そのときはよろしくおねがいしますね。

ex.関係ありそうな記事





この記事が参加している募集

ゲームの作り方

この記事が気に入ったらサポートをしてみませんか?