simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

CubeWalk:世界の境界の向こう側は無

世界データは最大 67MB

まずは事実から
バイナリキャッシュサーバーに 16 x 16 x 16 x 16 x 16 x 16 x 4 byte のデータを格納しています。
一時間おきに PlayFab からパスワード付きのリクエストを https で送信して PlayFab の CDNgzip 圧縮データをアップロードしています。
クライアントゲームは PlayFab にログイン成功後、最新のデータを CDN からダウンロードして gzip 解凍して

はい、ここまで
ここから zero を 1 にする作業です。

キャッシュサーバーに保存される世界データを小世界と呼ぶことにします。小世界は世界に 16 x 16 x 16 個存在し、現在関心を置いているのは中央が原点の小世界。
将来的に他の小世界から始まることを考えると、プレイヤーの開始位置は世界で様々としたときに
自分がいる小世界のデータをダウンロードして開始することになります。

小世界の果てに着いたらどうなるの?

どうやって小世界を特定するかは後回しにして、もし自身の小世界の終わりにたどり着いたとしたら、そこはどうなっているのでしょうか?
そこは無であり、ブロックを置こうとしても置けない空間です。

それでは小世界の移動ができないのか?

いいえ、小世界をまたぐゲートをまたぐことで小世界と小世界をつなぐことができます。
ゲートは世界を管理できる simplestar だけが設置できるものとし、このゲームの人気が出ない限り出現することはありません。

ゲートはどこに存在するのか?

小世界は上下左右に 6つの小世界と隣接しています。
世界の中央から上下左右に 6軸を延長して境界と交差したブロックを門とし、特別な破壊不能ブロックによって囲われて作られています。

想像しているゲートはこんな感じ

f:id:simplestar_tech:20191116193641p:plain
ゲートの案

プレイヤーが物理的にこのゲートに入った後、そのゲートの先にある小世界のデータのダウンロードに成功した時、その門の向こう側が構築され
プレイヤーは二つの世界を自由に行き来できるようになります。

門をくぐる行為が成功しない限り、門の向こう側の世界は無としてありつづけます。

小世界の名前

始まりの小世界は原点を表す 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] まで
実際に遠目で見てみるとこんな感じ

f:id:simplestar_tech:20191116233850p:plain
水平な世界の図

キューブの情報はダウンロード後にすべて作るとしてこのようにします。

        /// <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;
        }

世界の果ての様子

期待通りこんな感じでした。

f:id:simplestar_tech:20191117005258p:plain
小世界の果て