破棄もちゃんと出来た。破棄というか無効化だけだけども pic.twitter.com/rnKIMXQQsS
— みかげあすか (@mkgask) 2017年10月21日
プレイヤーキャラクターのモデルはお宮式琴葉葵です。
Copyright (C) 2017 AI,Inc. All Rights Reserved.
はじめに
ワールド自体がランダム生成される無限に移動できるオープンワールド風のサンドボックスゲーム遊びたい・・・遊びたくない?
というわけでまずはフィールドを自由に歩けるようにTerrainから作ります。
仕様検討
そういうゲームを作った経験は無いので、最近遊んだMinecraft、7 days to dieの経験と、UnityのTerrainの仕様から構造を推測します。
UnityのTerrainは、大きな一枚のTerrainを使うだけでなく、小さいTerrainを複数組み合わせて使えるようになっています。
なので、プレイヤーキャラの足元に小さなTerrainを一枚と、その周囲を囲むようにTerrainを配置し、プレイヤーキャラの移動にあわせて、前方のTerrainを生成、後方のTerrainを破棄していくのが、一番単純で作りやすい動作になるかと考えました。
概要設計
まずはエンティティとしてのTerrainEntityが必要です。
このエンティティにはTerrainのGameObject1つ分の管理だけを任せます。
データとしてはTerrain内の各座標の高さを持つハイトマップを持たせた方が良さそうで、あとは機能として、ハイトマップの生成、前後左右とのTerrainの接続、Terrainへのテクスチャの張り込みあたりは持たせてよさそうです。
そしてそのエンティティを束ねて管理するためのTerrainCollection。
Terrainの生成破棄の管理と、周囲のTerrainとの接続は任せられそうです。
そのTerrainCollectionに指示出しを行うTerrainService。
ここまでは、Unity管理外からTerrainのGameObjectを直接操作すればいい部分です。
最後にUnityの管理下に置いてTerrainServiceと接続させるTerrainController。
UnityのHierarchiesには、Terrainという名前の空のGameObjectを一個置いて、TerrainControllerスクリプトだけアタッチします。
TerrainControllerのStart()でTerrainServiceのinit()を呼んで地形の初期生成、Update()でプレイヤーの移動情報を受け取って、必要であれば随時TerrainServiceのupdate()を呼ぶ、という形でいけるのではと考えました。
実装
GitHubにアップ済みです。
https://github.com/mkgask/sgffu/tree/master/Assets/Scripts/Terrain
これ以降は補足解説なので、コードを見ながら読んでいただければと思います。
実装のポイント 1 ハイトマップ
該当箇所
TerrainEntity.cs TerrainEntity()
最初にハイトマップの生成で引っかかりました。
パーリンノイズを使うと自然な勾配を表現出来ると調べたので、ggって出てきたサンプルコードの通りにMathf.PerlinNoiseを使っていたつもりだったのですが、どうにも地形の高さが合いません。
追加で調べていくと、「Unityのハイトマップはxとzを逆にする必要がある」と出てきて、その情報を元に試行錯誤した結果、ハイトマップを保持するfloat[,]配列にはfloat[x,z]ではなくfloat[z,x]で渡せば、思った通りの結果が得られました。
シード値追加してのパーリンノイズでちゃんとした値がまだ作れてないので、ここはまだ試行錯誤中です。
(どれだけ入力値が変動しても、グラフに出したら360度ぐるぐる回るように作らないと無限に歩けないのでは・・・)
実装のポイント 2 Terrain同士の接続
該当箇所
TerrainCollection.cs update()
TerrainEntity.cs setNeighbors()
Terrain.SetNeighbors(Terrain left, Terrain top, Terrain right, Terrain bottom)メソッドを使うことで、Terrain同士が接続され、地形の継ぎ目が無くなり、TerrainのLODも接続したTerrain全体で1枚であるかのように扱ってくれる?(これは詳細未確認)ようになります。
が、この3次元のワールドで左とか右ってどっち?とか、frontとbackじゃなくてtopとbottomなの? Terrain、Y方向に広げないよね? とか、単語に色々惑わされました。
Unityを起動した状態でシーンビュー右上のカメラ表示向きの操作からYの三角錐をクリックで正解が分かりました。
この状態だと上下(前後)方向がZ軸(+が上(前)、-が下(後))、左右方向がX軸(+が右、-が左)になっていて、言われてみればUnityのワールド座標そうなってるよねそうだよね、となりました。
実装のポイント 3 Terrainのサイズと位置
該当箇所
TerrainEntity.cs TerrainEntity()
1つのTerrainの一辺のサイズをローカルのjsonを書き換えたら変えられるようにしてあるのですが、そこで指定したサイズと実際のサイズがズレる現象が起きました。
Terrainの一辺の長さをchunk_size、Terrainの位置をx , z(これは0からTerrainの位置に応じて1ずつ増えます)として、Terrainのサイズの指定を
TerrainData.size = new Vector3(chunk_size, terrain_height, chunk_size);
Terrainの位置の指定を
TerrainObject.transform.position = new Vector3(x * chunk_size, 0f, z * chunk_size);
とすると、
chunk_sizeが32の時は実際のTerrain一辺の長さは32、
chunk_sizeが64の時は実際のTerrain一辺の長さは128、
chunk_sizeが128の時は実際のTerrain一辺の長さは512、
chunk_sizeが256の時は実際のTerrain一辺の長さは2048、
chunk_sizeが512の時は実際のTerrain一辺の長さは8192、
になります。
ズレてる・・・
はて、それではこうしてみたらと思い、
float actual_chunk_size = chunk_size / Mathf.Max(chunk_size / 64, 1);
として、これで前記の数値に合うはずと思ってTerrainDataに設定
TerranData.size = new Vector3(actual_chunk_size, terrain_height, actual_chunk_size);
して見ると、
chunk_sizeが64の時は実際のTerrain一辺の長さは128、
chunk_sizeが128の時は実際のTerrain一辺の長さは256、
chunk_sizeが256の時は実際のTerrain一辺の長さは512、
になり
ズレてる・・・
まだ数値が倍なら、こうすればいけるでしょと
float actual_chunk_size = chunk_size / (Mathf.Max(chunk_size / 64, 0.5f) * 2);
したところ、合うようになりました。
何か自分のTerrainの作り方か設定がおかしい気がして仕方がないのですが、正しい算出方法または設定をご存知の方はコメントください。。。
実装のポイント 4 Terrain管理
該当箇所
TerrainCollection.cs this[int x, int z]
TerrainCollection.cs getHeight()
TerrainEntity.cs getHeight()
複数のTerrainをいくつも組み合わせるので、重複して生成してしまったりしないように管理をちゃんとしたいです。
Terrainや内部のデータが必要になった時、GameObjectから毎回検索するのはかなり遅そうなので、それとは別に管理します。
というわけでTerrainCollectionのメンバ変数にTerrainEntity[,]を用意しました。
生成したTerrainEntityを随時適切な位置に保持していきます。
プレイヤーキャラの位置x, zに対して、Terrainの一辺の長さで割って切り捨てれば(プレイヤーキャラ位置はfloatなのですぐ端数出る)アクセス用のint値が手に入るはずです。
また、今回はTerrain生成後にプレイヤーキャラを生成する処理にしており、プレイヤーキャラの生成時の出現高さが必ずTerrainを越えていなければいけません。
なので、TerrainServiceにプレイヤーキャラ位置を渡せばその座標の高さが返ってくるget_height()メソッドを用意しています。
プレイヤーキャラ位置x, zから前述の手順でTerrainEntityを取得、そこからx, zの小数点以下を使ってTerrain内の座標を取得し、ハイトマップからその位置の地形高さを拾ってくるようになっています。
ですが、ハイトマップの1つ分の地形高さというのも、Terrain内の特定の範囲(Terrainの地形解像度によって変わる)の中の中心高さでしかないはずなので、より厳密にプレイヤーキャラ位置の地形高さを取るには別の手段を取る必要がありそうです。
このまま製作を進めていけば後で必ず必要になりますが、今のところはそこまで厳密な地形高さは不要なので、まだ作っていません。
実装のポイント 5 プレイヤーキャラの移動にあわせたリアルタイムTerrain生成破棄
該当箇所
Characters/Player/PlayerController.cs updatePostCharaMove()
Terrain/TerrainController.cs Start()
プレイヤーキャラの移動とその位置の取得は、今回本記事では扱いません。
任意の手段で移動が可能で、位置が取れているものとします。
プレイヤーキャラが別のTerrain上に移ったら、そのTerrainを基準として周囲のTerrainを必要に応じて生成破棄を行うこととします。
前述の通り、プレイヤーキャラの位置からTerrainEntity[,]配列のアクセス用のint値が手に入るので、この値が変わったら別のTerrainに踏み込んだと判定します。
今回のプロジェクトはUniRxのMessageBrokerをメインの通信手段としているため、別のTerrainに踏み込んだと判定したら、MessageBrokerを介して新しく侵入したTerrain位置を持たせたplayerTerrainChunkMoveイベントを配信します。
playerTerrainChunkMoveはTerrainControllerで購読されており、新しいTerrain位置を使ってTerrainをアップデートします。
新しいTerrain位置を中心とした周囲のTerrainの状況を取得し、TerrainEntity[,]配列内に存在するTerrainはそのまま、存在しないTerrainは生成し、範囲外となったTerrainはSetActive(false)で無効化します。
問題点
とりあえず動く形にはなっていますが、階層の上下の分断はある程度は出来ていても、もう少し左右に分けたい、具体的にはTerrainEntityがTerrainCollectionに依存してぶら下がる形になっているので、TerrainServiceに直接ぶら下げて、生成はTerrainService内で直接、生成後にTerrainCollectionに渡してアレコレしてもらう、という形のほうが依存が1つ減らせて理想の形に近付くのではと感じています。
が、TerrainEntityの生成処理自体をTerrainCollectionに依存してしまっているので、ごそっと切り口から考え直さないとダメそうです。
あとは、Terrain上にオブジェクトをあれこれ配置していった場合に、生成はともかく、破棄と再設置をTerrain同等のタイミングで処理したいところに多少の不安がありますが、それは後から考えることにしました。
他に作っていかないといけない部分が山盛りです。
参考文献
パーリンノイズでマイクラみたいなマップの自動生成【Unity】 - (:3[kanのメモ帳]
UnityのTerrainで地形を自動生成させてみた! - KazumaLab.
matsushimaのブログ: 2-3. Unity で砲台ゲームを作る - 改良
TerrainPerlinNoise - Unify Community Wiki
How to Use Perlin Noise in Your Games – Dev.Mag
パーリンノイズを理解する | プログラミング | POSTD
ランダム地形生成 Part1~パーリンノイズ - Qiita