世界データは最大 67MB
まずは事実から
バイナリキャッシュサーバーに 16 x 16 x 16 x 16 x 16 x 16 x 4 byte のデータを格納しています。
一時間おきに PlayFab からパスワード付きのリクエストを https で送信して PlayFab の CDN に gzip 圧縮データをアップロードしています。
クライアントゲームは PlayFab にログイン成功後、最新のデータを CDN からダウンロードして gzip 解凍して
はい、ここまで
ここから zero を 1 にする作業です。
キャッシュサーバーに保存される世界データを小世界と呼ぶことにします。小世界は世界に 16 x 16 x 16 個存在し、現在関心を置いているのは中央が原点の小世界。
将来的に他の小世界から始まることを考えると、プレイヤーの開始位置は世界で様々としたときに
自分がいる小世界のデータをダウンロードして開始することになります。
小世界の果てに着いたらどうなるの?
どうやって小世界を特定するかは後回しにして、もし自身の小世界の終わりにたどり着いたとしたら、そこはどうなっているのでしょうか?
そこは無であり、ブロックを置こうとしても置けない空間です。
それでは小世界の移動ができないのか?
いいえ、小世界をまたぐゲートをまたぐことで小世界と小世界をつなぐことができます。
ゲートは世界を管理できる simplestar だけが設置できるものとし、このゲームの人気が出ない限り出現することはありません。
ゲートはどこに存在するのか?
小世界は上下左右に 6つの小世界と隣接しています。
世界の中央から上下左右に 6軸を延長して境界と交差したブロックを門とし、特別な破壊不能ブロックによって囲われて作られています。
想像しているゲートはこんな感じ
プレイヤーが物理的にこのゲートに入った後、そのゲートの先にある小世界のデータのダウンロードに成功した時、その門の向こう側が構築され
プレイヤーは二つの世界を自由に行き来できるようになります。
門をくぐる行為が成功しない限り、門の向こう側の世界は無としてありつづけます。
小世界の名前
始まりの小世界は原点を表す 000 の文字列がキーになり
x 方向に進んで突き当たるゲートをくぐると 100 のキーの世界へ行けます
- x 方向だったなら F00 の世界へ行けます
y 方向に上昇してゲートを潜ると010の世界へ行くことになります。
XYZ の値を意味しているわけで 000 ~ FFF の全4096世界を表現します。
自分がどの小世界なのかを判定するのは、チャンクのキーが頼りです。
チャンクには 0~255, 0~255, 0~255 の三次元インデックスが振られています。
世界 000 はチャンク
0 - 8 ~ 7 つまり各軸 248~255, 0 ~ 7 のチャンクのキーインデックスに収まっているときに世界 000 にいることが確定します。
チャンクキーから小世界の名前はわかるのか?
そろそろエンジニア向けの話になってきました。
ロジック書けますか?
それぞれの世界には中心チャンクが存在します。並べてみるとわかってくるのではないでしょうか?
小世界の名前 → 中心チャンクキー
E00 → 224, 0, 0
F00 → 240, 0, 0
000 → 0, 0, 0
100 → 16, 0, 0
200 → 32, 0, 0
こんな感じで 16 ずつ中心チャンクキーが移動します。
つまり 1/16倍して round to int すれば 0 ~ 15 の値になるのでは?
試験してみたところ
残念
0~8 までが 0
9~23 までが 1 になってしまった
本当は 0~7 までが 0 で
9~24 までが 1 であってほしいのに
そこで小さい値を詰めて調整することにした
期待通りのインデックス文字列が得られるようになりました。
for (int i = 0; i < 256; i++) { int index = Mathf.RoundToInt((i + 0.1f) / 16f); if (16 <= index) index = 0; Debug.Log($"i = {i}, index = {index.ToString("X")}"); }
小世界の大きさ
16 x 16 x 16 キューブのチャンクが、前後左右上下で 8 チャンクの距離なので、一方向に 16 x 8 = 128 [cube] まで
実際に遠目で見てみるとこんな感じ
キューブの情報はダウンロード後にすべて作るとしてこのようにします。
/// <summary> /// 未読み込みの場合のみデータをロード /// </summary> void LoadChunkData(ChunkLoadTask chunkLoadTask) { this.ChunkInt3ToChunkKey(chunkLoadTask.chunkInt3, out var chunkKeyXYZ); var byteMax = (byte.MaxValue + 1); int chunkIndex = chunkKeyXYZ.x * byteMax * byteMax + chunkKeyXYZ.z * byteMax + chunkKeyXYZ.y; if (null == this.ppChunkData[chunkIndex]) { // チャンクが所属する小世界のデータをバッファに書き込み var microWorldKey = this.ChunkKeyToMicroWorldKey(chunkKeyXYZ); var microWorldName = this.MicroWorldNameFromKey(microWorldKey); if (this.cubedataLibrary.ContainsKey(microWorldName)) { // cubedataLibrary には小世界すべての情報が格納されているので、目的のチャンク情報にアクセスするためのオフセットを計算 const int eight = 8; const int deight = 16; var offsetX = chunkLoadTask.chunkInt3.x - microWorldKey.x * ChunkConst.ChunkSizeX - eight; offsetX = AdjustLocalOffsetForChunkKey(byteMax, deight, offsetX); var offsetZ = chunkLoadTask.chunkInt3.z - microWorldKey.z * ChunkConst.ChunkSizeZ - eight; offsetZ = AdjustLocalOffsetForChunkKey(byteMax, deight, offsetZ); var offsetY = chunkLoadTask.chunkInt3.y - microWorldKey.y * ChunkConst.ChunkSizeY - eight; offsetY = AdjustLocalOffsetForChunkKey(byteMax, deight, offsetY); var chunkDataOffset = offsetX * deight * deight + offsetZ * deight + offsetY; var bufferByteOffset = chunkDataOffset * deight * deight * deight * 4; // cubedataLibrary から目的のチャンクの情報部分をコピー var chunkDataSize = sizeof(int) * ChunkConst.ChunkSizeX * ChunkConst.ChunkSizeY * ChunkConst.ChunkSizeZ; var microWorldData = this.cubedataLibrary[microWorldName]; var chunkData = new byte[chunkDataSize]; Buffer.BlockCopy(microWorldData, bufferByteOffset, chunkData, 0, chunkDataSize); var nativeArray = new NativeArray<byte>(chunkData, Allocator.TempJob); var pNativeChunkData = (int*)nativeArray.GetUnsafePtr(); var pChunkData = (int*)(UnsafeUtility.Malloc(chunkDataSize, sizeof(int), Allocator.Persistent)); UnsafeUtility.MemCpy(pChunkData, pNativeChunkData, chunkDataSize); this.ppChunkData[chunkIndex] = pChunkData; nativeArray.Dispose(); } else { // 小世界データがライブラリにないので null のままにしておく } } } /// <summary> /// 0をまたぐインデックスのためのオフセット調整 /// </summary> private static int AdjustLocalOffsetForChunkKey(int byteMax, int deight, int offset) { if (deight < offset) { offset = offset - byteMax + deight; } if (0 > offset) { offset += deight; } return offset; }
世界の果ての様子
期待通りこんな感じでした。