はじめに
ワールド自体がランダム生成される無限に移動できるオープンワールド風のサンドボックスゲーム遊びたい・・・遊びたくない?
ということで、まずはフィールドを作成中です。
概要設計は プレイヤーキャラの移動にあわせたリアルタイムTerrain生成破棄 で行っているので、
未読であれば先にそちらをご確認ください。
ただし、本記事では上記の記事と矛盾した記述がある可能性があります。本記事のほうが後から再検証したものなので、その場合は本記事の記述を優先してもらえればと思います。
大まかな処理の流れ
Terrainを操作する必要があるのは大きく以下の2箇所だけです。
・初期生成
初期生成の範囲を生成
生成したTerrainを保持管理するための入れ物を生成
初期生成の範囲のTerrainを生成
Terrain生成とは主に、ハイトマップを作成して割当、SetNeighborsで周辺のTerrainと接続、テクスチャ貼り付け、の3つを指す
・プレイヤー移動時のアップデート
基本的には生成済みのTerrainの有効/無効の切り替えだけにしたい
でも初期生成の範囲外は新しく生成したい
用語の定義
チャンク - この記事ではTerrain1つ分のことを1チャンクとして扱います。
チャンクサイズ - Terrainの一辺の長さをチャンクサイズと読んでいます。Terrainは正方形のものとして扱っています。
名前空間
そこまで大層な規模のものでもないので、今のところ切り分ける必要を感じず、登場するクラスの全部をsgffu.Terrainに置いてあります。
sgffuは自作の Sandbox game foundation for Unity の略称で、今回のTerrain周りの処理はそのうちの機能の1つとして作成しています。
実際に出来上がったもの
例によってコードはGitHubにあります。
https://github.com/mkgask/sgffu/tree/master/Assets/Scripts/Terrain
前回記事に貼ったツイッターの動画のように、前の段階で動くものとしては一応出来ていましたが、手を加え直してもう一回動くところまで持ってこれたので、一旦ここまでで何を作ったのか書いていきます。
登場するクラス一覧こちら
- TerrainEntityCore - Terrainを生成するための情報というか主にハイトマップをキャッシュするためのJSON入出力用データクラス
- TerrainEntity - Terrainを保持するGameObject1つを外部から管理。TerrainEntityCoreを継承
- TerrainCollection - TerrainEntityの配列を保持
- TerrainConfig - Terrain生成用のパラメータを保持
- TerrainEntityFactory - TerrainEntityの生成を担当
- TerrainCollectionFactory - TerrainCollectionの生成を担当
- TerrainConfigRepository - TerrainConfigをファイルに保存、ファイルから再生成を行う
- TerrainEntityRepository - TerrainEntityをファイルに保存、ファイルから再生成を行う
- TerrainService - Terrain機能をサービスとしてゲームに提供する窓口
- TerrainFactory - Terrain生成処理のエントリポイント
こういうコードって処理の下流から見せるか上流から見せるか迷いますね。
個人的には大まかに分かっていれば下流からのほうが理解しやすいので、今回は下流からです。
途中で飽きたら途中から雑になるかもしれないので先に謝っておきます。申し訳ありません。
あ、あとコードはUnity5.6で動くようにC#4.0相当です。
書いた後に人に見せたくないと思ったクラス図
コードを書ききった後に後付けで書いたクラス図です。
このクラス図を見ることによって、気分の悪さ、嫌悪感、吐き気や怒り、憤りを感じるなどの症状が起きても私は責任を持てません。
赤矢印が依存、白矢印はis-aなどクラス図のルールがあるようですが、
見た目の問題で白矢印にしただけなので実際にはだいたい全部依存です。
TerrainEntityCore
Terrainを生成するための情報を保持するデータクラス
public class TerrainEntityCore
{
public int pos_x = int.MinValue; // ワールド座標とかではなく、中心のTerrain用GameObjectから何個分離れているかの相対値
public int pos_z = int.MinValue; // 同上
public float[,] height_map = new float[0,0]; // Terrain1つ分のハイトマップ
public float terrain_height = float.MinValue; // 地形高さ上限
}
主にハイトマップをファイルにキャッシュしておくためのクラスです。
Terrain位置からTerrainEntityを生成する時には使わず、生成したTerrainEntityをJSONファイルとして保存する時に、このクラスにアップキャストして、このクラスにある情報だけを保存するようになっています。
あと保存したJSONからTerrainEntityに再展開する際も、一旦このクラスを経由します。
TerrainEntity
Terrainを保持するGameObject1つを外部から管理
public class TerrainEntity : TerrainEntityCore
{
public GameObject game_object = null; // Terrainを持つGameObjectを保持
public UnityTerrain terrain = null; // GameObjectにアタッチされたTerrainコンポーネントを保持
public TerrainEntity(int x, int z, float[,] height_map, float terrain_height,
GameObject game_object, UnityTerrain terrain
) {
this.pos_x = x;
this.pos_z = z;
this.height_map = height_map;
this.terrain_height = terrain_height;
this.game_object = game_object;
this.terrain = terrain;
}
public void enable()
{
this.game_object.SetActive(true);
}
public void disable()
{
this.game_object.SetActive(false);
}
public bool status()
{
return this.game_object.activeInHierarchy;
}
public void setNeighbors(TerrainEntity left, TerrainEntity top, TerrainEntity right, TerrainEntity bottom)
{
// 独自の名前空間にTerrainを使っていて名称の混乱が起きているので、using UnityTerrain = UnityEngine.Terrainしています。
UnityTerrain left_terrain = null;
UnityTerrain top_terrain = null;
UnityTerrain right_terrain = null;
UnityTerrain bottom_terrain = null;
if(left != null) { left_terrain = left.terrain; }
if(top != null) { top_terrain = top.terrain; }
if(right != null) { right_terrain = right.terrain; }
if(bottom != null) { bottom_terrain = bottom.terrain; }
this.terrain.SetNeighbors(left_terrain, top_terrain, right_terrain, bottom_terrain);
}
public void setTexture(Texture2D texture, int chunk_size)
{
Debug.Assert(0 < chunk_size);
TerrainData tData = this.terrain.terrainData;
Vector3 tDataSize = tData.size;
tData.alphamapResolution = chunk_size;
// 今のところテクスチャ一枚をすべてのTerrainの全面に貼る機能しかありません。
SplatPrototype[] splatprototype = new SplatPrototype[1];
splatprototype[0] = new SplatPrototype();
splatprototype[0].texture = texture;
splatprototype[0].tileSize = new Vector2(tDataSize.x, tDataSize.z);
tData.splatPrototypes = splatprototype;
int al_w = tData.alphamapWidth;
int al_h = tData.alphamapHeight;
float[,,] map = new float[al_w, al_h, 1];
for (int x = 0; x < al_w; x += 1) {
for (int z = 0; z < al_h; z += 1) {
map[x, z, 0] = 1f;
}
}
tData.SetAlphamaps(0, 0, map);
this.terrain.terrainData = tData;
}
public float getHeight(float offset_x, float offset_z)
{
Debug.Assert(offset_x < 1f);
Debug.Assert(offset_z < 1f);
// ワールド座標をチャンクサイズで割って小数部だけ取り出せば、Terrain内の欲しい位置が割り出せる算段
TerrainData tData = terrain.terrainData;
int max_w = tData.heightmapWidth;
int max_h = tData.heightmapHeight;
int x = Mathf.FloorToInt(max_w * offset_x);
int z = Mathf.FloorToInt(max_h * offset_z);
return this.height_map[x, z] * this.terrain_height;
}
}
TerrainEntityは宣言通り、Terrain1つ分の管理を行ってもらっています。
今のところTerrainに期待することは、
- 有効/無効切り替え
- 不自然な地形にならない綺麗なハイトマップ
- 近隣のTerrainとの接続
- 地面テクスチャ
- 指定の位置の地形高さの取得
の5つで、このうちハイトマップ生成だけは、TerrainEntityやTerrain、GameObjectの生成の前に作成処理をしておきたいところで、Terrain1つに収まらないワールド座標を利用するものなので、TerrainServiceのほうで行っています。
なので残りの4つをTerrainEntityにメソッドとして実装しています。
TerrainCollection
TerrainEntityの配列を保持
public class TerrainCollection
{
public Dictionary<long, Dictionary<long, TerrainEntity>> entities = new Dictionary<long, Dictionary<long, TerrainEntity>>();
public int terrain_chunk_size = 0;
public int terrain_chunk_offset = 0;
public int terrain_pos_start = 0;
public int terrain_pos_end = 0;
public TerrainEntity this[long x, long z]
{
set {
if (!entities.ContainsKey(x)) {
entities.Add(x, new Dictionary<long, TerrainEntity>());
}
if (!entities[x].ContainsKey(z)) {
entities[x].Add(z, value);
return;
}
entities[x][z] = value;
}
get {
try {
return entities[x][z];
} catch(KeyNotFoundException e) {
return null;
}
}
}
}
前回記事では「実装のポイント 4 Terrain管理」の通り、TerrainEntityを二次元配列[,]で保持していたのですが、
配列のキーはマイナス値を持てないので、0,0を中心としたTerrain位置と整合性がとれず、
オフセットを入れたところを世界の中心とすると、オフセット値が世界の限界値になってしまい、
世界の果てをどうしても作りたくない今回の仕様とは非常に相性が悪いので、
Dictionary>で保持するように変更しました。
pos_xとpos_zを持っているので、Listで、FirstOrDefaultあたりで引っ掛ければ拾えるなーとかも考えたんですが、Listの内部実装を見る限り普通に総当りで検索かけてるように見受けられたので、それならDictionaryでハッシュで拾ってもらったほうがおそらく速いだろう、となりました。(実測は出来てません)
TerrainConfig
Terrain生成用のパラメータを保持
public class TerrainConfig
{
public int chunk_effective_range = 0;
public int chunk_size = 0;
public float actual_chunk_size = 0f;
public float terrain_height = 0f;
public int base_map_resolution = 0;
public int detail_resolution = 0;
public int resolution_per_path = 0;
public float perlin_noise_scale = 0f;
public string texture_filepath = "";
}
これもただのデータクラスです。
JSON保存・読み込み用で、JSONを書き換えればこのクラスとして読み出されて、Terrainの生成に影響を与えることが出来るようになっています。
TerrainEntityFactory
TerrainEntityの生成を担当
public class TerrainEntityFactory
{
public static TerrainEntity create(int x, int z, float[,] heights, TerrainConfig config, GameObject parent)
{
GameObject game_object = createGameObject("Terrain-" + x + "-" + z, parent);
TerrainData tData = createTerrainData(config);
tData.SetHeights(0, 0, heights);
game_object.GetComponent<UnityTerrain>().terrainData = tData;
game_object.GetComponent<TerrainCollider>().terrainData = tData;
game_object.transform.position = new Vector3(x * config.chunk_size, 0f, z * config.chunk_size);
TerrainEntity entity = new TerrainEntity(
x,
z,
heights,
config.terrain_height,
game_object,
game_object.GetComponent<UnityTerrain>()
);
return entity;
}
public static TerrainEntity createFromCore(TerrainEntityCore core, TerrainConfig config, GameObject parent)
{
return create(
core.pos_x,
core.pos_z,
core.height_map,
config,
parent
);
}
private static GameObject createGameObject(string name, GameObject parent)
{
GameObject game_object = new GameObject(name);
game_object.AddComponent<UnityTerrain>();
game_object.AddComponent<TerrainCollider>();
game_object.transform.SetParent(parent.transform);
return game_object;
}
private static TerrainData createTerrainData(TerrainConfig config)
{
TerrainData tData = new TerrainData();
tData.size = new Vector3(config.actual_chunk_size, config.terrain_height, config.actual_chunk_size);
tData.heightmapResolution = config.chunk_size;
tData.baseMapResolution = config.base_map_resolution;
tData.SetDetailResolution(config.detail_resolution, config.resolution_per_path);
return tData;
}
}
このクラスの外で生成されたパラメータを与えることで、TerrainEntityインスタンスの生成、そこに持たせるTerrainコンポーネントの生成、それをアタッチするGameObjectの生成まで行ってもらっています。
GameObject parentにはTerrainControllerをアタッチしたGameObjectが入ってくるので、各TerrainはそのGameObjectの子として生成されていきます。
C#の仕様上、ダウンキャストに一部制限があるので、TerrainEntityCoreからTerrainEntityにキャストじゃなくて生成するようcreateFromCoreメソッドを持っています。
TerrainCollectionFactory
TerrainCollectionの生成を担当
public class TerrainCollectionFactory
{
public static TerrainCollection create(TerrainConfig terrain_config, int world_size)
{
int terrain_size = world_size / terrain_config.chunk_size;
TerrainCollection terrain_collection = new TerrainCollection();
terrain_collection.terrain_chunk_size = terrain_size;
terrain_collection.terrain_chunk_offset = Mathf.FloorToInt(terrain_size / 2);
terrain_collection.terrain_pos_start = terrain_collection.terrain_chunk_offset * -1;
terrain_collection.terrain_pos_end = terrain_collection.terrain_chunk_offset;
return terrain_collection;
}
}
いよいよ書くことが無くなってきたような・・・
というかこのcreateメソッド、今見返すとTerrain初期生成用のパラメータをアレコレ作ってるんですが、ここでやる必要が・・・?あるんだろうか・・・?
TerrainConfigに持たせておいたほうがよほど健全なようなというかTerrainCollectionの中身詰める直前に作ればいいだけで変数に取っておく必要は・・・?
とりあえずは動いているので、あとで気が向いたら直していきましょう。(こういうのばっかり)
TerrainConfigRepository
TerrainConfigをファイルに保存、ファイルから再生成を行う
public class TerrainConfigRepository
{
public static TerrainConfig createDefault()
{
int chunk_size = 128;
return new TerrainConfig {
chunk_effective_range = 2,
chunk_size = chunk_size,
terrain_height = 128,
base_map_resolution = 64,
detail_resolution = 1024,
resolution_per_path = 512,
perlin_noise_scale = 0.002f,
actual_chunk_size = calcurate_actual_chunk_size(chunk_size),
texture_filepath = "Terrain/Grounds/diffuse_light1.png"
};
}
public static TerrainConfig loadFile(TerrainConfig terrain_config_default)
{
TerrainConfig terrain_config = ConfigFile.load<TerrainConfig>(ConfigFile.terrainConfigFilename, terrain_config_default);
terrain_config.actual_chunk_size = calcurate_actual_chunk_size(terrain_config.chunk_size);
return terrain_config;
}
public static float calcurate_actual_chunk_size(int chunk_size) {
return chunk_size / (Mathf.Max(chunk_size / 64, 0.5f) * 2);
}
}
TerrainConfigのいわゆる永続化を担当する予定です。
今のところ初期生成後はJSONからの値の読み出ししかしないので、編集した値をJSONに再保存する機能がまだありません。
JSONの値を書き換えれば反映されるので、だいぶ後回しになりそうです。
前回記事で「実装のポイント 3 Terrainのサイズと位置」で書いていた、TerrainData.sizeに指定した値と実際のTerrain一辺の長さがズレている件について、このクラスでcalcurate_actual_chunk_sizeメソッドを使って調整後の値を生成しています。
TerrainEntityRepository
TerrainEntityをファイルに保存、ファイルから再生成を行う
public class TerrainEntityRepository
{
const string file_path_base = "Worlds/{world_name}/Terrain/{terrain_name}.json";
public static string file_path = "";
private static TerrainEntityCore default_entity_core = new TerrainEntityCore();
public static void reset(string world_name)
{
file_path = file_path_base.Replace("{world_name}", world_name);
}
public static TerrainEntity get(string terrain_name, TerrainConfig config, GameObject parent)
{
file_path = file_path.Replace("{terrain_name}", terrain_name);
TerrainEntityCore core = ConfigFile.load<TerrainEntityCore>(file_path, default_entity_core);
return TerrainEntityFactory.createFromCore(core, config, parent);
}
public static bool set(TerrainEntity entity)
{
file_path = file_path.Replace("{terrain_name}", entity.game_object.name);
return ConfigFile.save<TerrainEntityCore>(file_path, (TerrainEntityCore)entity);
}
}
setにTerrainEntityを渡してJSONに保存、getJSONからTerrainEntityを再生成してくれる予定です。(まだちゃんと動いてない)
TerrainEntityCoreのところでも少し触れましたが、TerrainEntityを直接保存や読み出しせず、間にTerrainEntityCoreを挟んでいます。
JSONのファイル入出力にはUtf8Jsonを利用させていただいていますが、このライブラリはネットワーク通信用な気がして仕方がないので、のちのち別のものも検討する必要がありそうです。beautifier欲しい。
(2017/12/09追記)
Utf8Jsonには、JsonSerializer.PrettyPrintというBeautifierがあるとコメントでご指摘とコード例をいただきました。
// save(humanreadable)
var jsonBytes = JsonSerializer.PrettyPrintByteArray(JsonSerializer.Serialize(data));
File.WriteAllBytes("path", jsonBytes);
ありがとうございました。
TerrainService
Terrain機能をサービスとしてゲームに提供する窓口
public class TerrainService
{
public static GameObject terrain_parent;
public static TerrainConfig terrain_config;
private static TerrainCollection terrain_collection;
private static WorldConfig world_config;
private static int preview_left_top_x = 0;
private static int preview_left_top_z = 0;
private static int preview_right_bottom_x = 0;
private static int preview_right_bottom_z = 0;
private static Texture2D texture;
// TerrainService初期化
// ConfigData.instantiate_texture2Dは後述
public static void reset(GameObject game_object, TerrainConfig terrain_config, WorldConfig world_config)
{
terrain_parent = game_object;
TerrainService.terrain_config = terrain_config;
TerrainService.world_config = world_config;
terrain_collection = TerrainCollectionFactory.create(terrain_config, world_config.world_size);
TerrainEntityRepository.reset(world_config.world_name);
texture = ConfigData.instantiate_texture2D(
StrOpe.i + "/Resources/" + terrain_config.texture_filepath,
terrain_config.detail_resolution,
terrain_config.detail_resolution
);
}
// プレイヤー移動時のTerrainアップデートを行います。
// 生成済みのTerrainは有効/無効の切り替えのみ。
// 未生成のTerrainは新しく生成します。
public static IEnumerator update(int player_x, int player_z)
{
int effective_range = terrain_config.chunk_effective_range;
int left_top_x = player_x - effective_range;
int left_top_z = player_z - effective_range;
int right_bottom_x = player_x + effective_range;
int right_bottom_z = player_z + effective_range;
int chunk_size = terrain_config.chunk_size;
float terrain_seed = world_config.terrain_seed;
float perlin_noise_scale = terrain_config.perlin_noise_scale;
bool terrain_create = false;
for (int x = left_top_x; x <= right_bottom_x; x += 1) {
for (int z = left_top_z; z <= right_bottom_z; z += 1) {
yield return null;
if (terrain_collection[x, z] == null) {
terrain_create = true;
createTerrain(x, z, chunk_size, terrain_seed, perlin_noise_scale);
} else {
if (!terrain_collection[x, z].status()) {
terrain_collection[x, z].enable();
}
}
}
}
if (terrain_create) {
for (int x = left_top_x; x <= right_bottom_x; x += 1) {
for (int z = left_top_z; z <= right_bottom_z; z += 1) {
if (!terrain_collection[x, z].status()) {
setupTerrain(x, z, chunk_size);
}
}
}
}
for (int x = preview_left_top_x; x <= preview_right_bottom_x; x += 1) {
for (int z = preview_left_top_z; z <= preview_right_bottom_z; z += 1) {
yield return null;
if (left_top_x <= x && x <= right_bottom_x &&
left_top_z <= z && z <= right_bottom_z) {
continue;
}
if(terrain_collection[x, z] == null) {
continue;
}
terrain_collection[x, z].disable();
}
}
preview_left_top_x = left_top_x;
preview_left_top_z = left_top_z;
preview_right_bottom_x = right_bottom_x;
preview_right_bottom_z = right_bottom_z;
}
// Terrain初期化時のみ使用します。
// TerrainConfigで指定した範囲のTerrainを順次生成します。
public static void createUnityTerrains()
{
int chunk_size = terrain_config.chunk_size;
float terrain_seed = world_config.terrain_seed;
float perlin_noise_scale = terrain_config.perlin_noise_scale;
int xs = terrain_collection.terrain_pos_start;
int xe = terrain_collection.terrain_pos_end;
int zs = terrain_collection.terrain_pos_start;
int ze = terrain_collection.terrain_pos_end;
for (int x = xs; x < xe; x += 1) {
for (int z = zs; z < ze; z += 1) {
createTerrain(x, z, chunk_size, terrain_seed, perlin_noise_scale);
}
}
preview_left_top_x = xs;
preview_left_top_z = zs;
preview_right_bottom_x = xe;
preview_right_bottom_z = ze;
}
// Terrain初期化時のみ使用します。
// createUnityTerrainsで作ったTerrain達に対して追加の調整を実施していきます。
public static void setupTerrainCollection()
{
int chunk_size = terrain_config.chunk_size;
for (int xx = terrain_collection.terrain_pos_start; xx < terrain_collection.terrain_pos_end; xx += 1) {
for (int zz = terrain_collection.terrain_pos_start; zz < terrain_collection.terrain_pos_end; zz += 1) {
setupTerrain(xx, zz, chunk_size);
}
}
}
// Terrain生成
private static void createTerrain(int x, int z, int chunk_size, float terrain_seed, float perlin_noise_scale)
{
int xs = x * chunk_size;
int zs = z * chunk_size;
float[,] heights = createHeightMap(xs, zs, terrain_seed, perlin_noise_scale);
terrain_collection[x, z] = TerrainEntityFactory.create(x, z, heights, terrain_config, terrain_parent);
terrain_collection[x, z].disable();
}
// createTerrainで生成したTerrainに対して、追加で調整を行います。
private static void setupTerrain(int x, int z, int chunk_size)
{
terrain_collection[x, z].setNeighbors(
terrain_collection[x - 1, z],
terrain_collection[x, z + 1],
terrain_collection[x + 1, z],
terrain_collection[x, z - 1]
);
terrain_collection[x, z].setTexture(texture, chunk_size);
TerrainEntityRepository.set(terrain_collection[x, z]);
terrain_collection[x, z].enable();
}
// Terrain1つ分のハイトマップ生成
// ここが壊れていると針の山が出来たり地形に切れ目が出来ます
// Mathf.PerlinNoiseに渡す引数を作っているRand.calucurate_perlin_valueについては後述
private static float[,] createHeightMap(int xs, int zs, float terrain_seed, float perlin_noise_scale)
{
int x_max = terrain_config.chunk_size + 1;
int z_max = terrain_config.chunk_size + 1;
float[,] heights = new float[z_max, x_max];
for (int x = 0; x < x_max; x += 1) {
for (int z = 0; z < z_max; z += 1) {
float xx = Rand.calucurate_perlin_value(xs + x, terrain_seed, perlin_noise_scale);
float zz = Rand.calucurate_perlin_value(zs + z, terrain_seed, perlin_noise_scale);
heights[z, x] = Mathf.PerlinNoise(xx, zz);
}
}
return heights;
}
// ワールド座標を受け取って、その位置の地形高さを返します。
// Terrain生成後にプレイヤーキャラクターを初期化しているので、地形に埋まらないようにこれで高さを取得しています。
public static float getHeight(float x, float z)
{
int integer_part_x = Mathf.CeilToInt(x / terrain_config.chunk_size);
int integer_part_z = Mathf.CeilToInt(z / terrain_config.chunk_size);
float r = terrain_collection[integer_part_x, integer_part_z].getHeight(
x - integer_part_x,
z - integer_part_z
);
return r;
}
}
やだ、このクラスデカ過ぎ・・・?
今更見返してみると、ゲームとサービスのインターフェースになる窓口クラスのわりにビジネスロジックばっかり書いてあるように見えます。
Terrain機能内部クラスへの命令出しとそのための準備をしているだけのはずなんですが。
もう一枚、間に立つレイヤーとしてのクラスを作って挟んだほうがすっきり綺麗なコードに見えるかもしれませんね。
SetNeighborsでのTerrain接続は、接続先のTerrainが存在していないと接続出来ないので、Terrainの生成だけを先に行っておいて、後から生成済みのTerrainに対してSetNeighborsしていく処理になっています。
が。
確か接続する両方のTerrainからSetNeighborsしないといけないみたいな記述をどこかで見かけた気がするのですが、今の処理では、一旦生成と接続を行った後に、その外側に生成したTerrainに対して内側から再接続をかける処理が無く、外側のTerrainを生成した際に内側のTerrainに片側からSetNeighborsするだけしか出来ていません。
一定範囲のTerrainを生成後に、その外周を回ってTerrainが存在するならSetNeighborsし直す、みたいな処理が必要なのかもしれません。
TerrainFactory
Terrain生成処理のエントリポイント
public class TerrainFactory
{
const string terrain_gameobject_name = "Terrain";
public static void create()
{
GameObject terrain_gameobject = GameObject.FindWithTag(terrain_gameobject_name);
TerrainConfig terrain_config = TerrainConfigRepository.get(TerrainConfigRepository.createDefault());
WorldConfig world_config = (SceneService.transition_scene_data as AllowWorldCreate).world_config;
TerrainService.reset(terrain_gameobject, terrain_config, world_config);
TerrainService.createUnityTerrains();
TerrainService.setupTerrainCollection();
Observable.FromCoroutine(x =>
TerrainService.update(0, 0)
).Subscribe(x => {
MessageBroker.Default.Publish(new TerrainCreated{});
});
MessageBroker.Default.Receive<playerTerrainChunkMove>().Subscribe(x => {
MainThreadDispatcher.StartCoroutine(TerrainService.update(x.x, x.z));
});
}
}
このTerrainFactory.create()をゲーム初期化時に呼び出すことでTerrain処理を開始し、初期生成とプレイヤー移動時のTerrainアップデートの待ち受けを行います。
補助処理
後述と書いたメソッドが2つほどあるので、そちらを紹介します。
namespace sgffu.Config
{
/// <summary>
/// 設定情報処理クラス
/// </summary>
public class ConfigData
{
public static Texture2D instantiate_texture2D(string path, int w, int h)
{
byte[] texture_data = (new File(path, "")).readBytes();
Texture2D texture = new Texture2D(w, h);
if (!texture.LoadImage(texture_data)) {
throw new Texture2DdontLoadException();
}
return texture;
}
}
}
なんかここだけsummary書いてありました。
あと別の名前空間切ってあったのでそこも入れてあります。
ファイルパスからTexture2D作るのはInstantiateでは出来ないのでメソッドを捏造しました。
namespace sgffu.Utility
{
public class Rand
{
public static uint xorshift(int base_numeric, float base_numeric_scale = 1f)
{
int y = Mathf.RoundToInt(base_numeric * base_numeric_scale) + 1234567890;
y = y ^ (y << 13);
y = y ^ (y >> 17);
return (uint)(y ^ (y << 5));
}
public static uint xorshift(string base_string, float base_numeric_scale = 1f)
{
int sum = 0;
foreach (var item in base_string.ToCharArray())
{
sum += (int)item;
}
return xorshift(sum, base_numeric_scale);
}
public static float calucurate_perlin_value(int pos, float terrain_seed, float scale)
{
return terrain_seed + (pos * scale);
}
}
}
入力してもらったワールド名からランダムなシード値を生成するのにxorshiftを使っています。
それとMathf.PerlinNoiseに渡すための引数を作るcalucurate_perlin_valueメソッド。
calucurate_perlin_valueはterrain_seedを渡せるようにしてあるのですが、Mathf.PerlinNoiseは0~10までの値であれば処理してくれるので、xorshiftで作ったシード値を更に加工してから渡します。
加工処理が何故かWorld機能側に置いてあるのもこれは改善項目としてカウントしたほうがいいのでしょうか。
(前略)
terrain_seed = ((seed / Mathf.Pow(10, seed.ToString().Length - 1)) + 4.5f) / 2,
(後略)
seedにはxorshiftで作ってもらった確か7~8桁くらいの整数が入っていて、小数点位置を移動して1以下の値にし、calucurate_perlin_valueでは単純な足し算しかしていないので上限である10からは遠い方の値がTerrain用のシード値になるようにしてあります。
微調整入れずにゼロ付近が中心でいいのではという案もあったんですが、プレイヤーキャラの出現位置を0, 0にしてあるので、そこを挟んでプラスマイナスが逆転するだけだと、おそらく、前と後ろ、左と右で同じ地形が見えてしまうのではと考えてこういう処理を入れたような覚えがあります。
あと数学力の無さ。
まだ足りてない機能
- プレイヤーから遠く離れたTerrainはDestroy必要
- 複数テクスチャ貼り付け
- 草、石、岩、木のランダム配置
- 地形高さ算出のパターンは複数欲しい
他にも、Terrainはバイオームと密接に紐付いていて欲しいので、将来的にバイオームの情報を使って地形生成や装飾オブジェクトの有無なんかを切り替えていきたいところです。
感想
- 自分で作ったもののレベルの低さに自分で絶望
- 手続き型にしか見えない
- 依存性の注入・・・? Zenject・・・? うっ、頭が・・・
- 依存性の逆転・・・? SOLID原則・・・? 何それ食べ物? 美味しいの?
- 自分しか触らないからってとりあえずpublicにしとくのやめよう?