simplestarの技術ブログ

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

PlayFab:キューブ破壊後にキューブデータをインベントリへ格納

キューブ保存の法則

世界のデータは減りも増えもしない(神様の創造は例外)
キューブを破壊しようとしたら、その破壊をバイナリキャッシュサーバーが判定し
成功したならインベントリにそのキューブの情報が格納される仕組みを作ります。

インベントリの操作

基本的に C# Unity からは API で CloudScript をたたくだけを想定しています。
その先の CloudScript 側でインベントリを操作する具体的な例を見て覚えていくところから

Qiita記事を参考に高速学習
qiita.com
こちらは Unity から直接インベントリのすべてのアイテムを確認する手段ですね。
執筆者のよつのさんは PlayFab 勉強会で帰り道の電車が一緒でいろいろゲームの話をした方です。

アイテムの受け渡し

qiita.com
アイテムを作るにはそのアイテムを管轄する「カタログ」が必要
ItemID がないとやり取りできないので作るほかない

ブロックの 1 つ目を何にするか考えましたが、ここは乾いた土にしました。

f:id:simplestar_tech:20191117113310p:plain
PlayFabのアイテム定義

続いてオンラインのキューブの pick 機能を考えます。
対象のキューブの位置を特定し、これの値を取り出す操作です。

pick対象のキューブのインデックス

ローカルで正しくキューブの破壊が行えているということは、正しいインデックスを取得できているのだと思いますが
何番でしょう?

現在のこの動いているコードがヒント

        /// <summary>
        /// Cube の情報を取得
        /// </summary>
        internal byte* GetCubeData(Vector3 cubePosition, Vector3Int chunkInt3)
        {
            this.ChunkInt3ToChunkKey(chunkInt3, out Vector3Int chunkKeyXYZ);
            var cubeInt3 = this.CubePositionToCubeInt3(cubePosition);
            var cubeIndex = this.CubeInt3ToCubeIndex(cubeInt3);
            var chunkIndex = this.ChunkKeyToChunkIndex(chunkKeyXYZ);
            var pChunkData = this.ppChunkData[chunkIndex];
            return (byte*)(pChunkData + cubeIndex);
        }

小世界ごとのキューブの位置を求めなければならないので
前回の記事のコンテキストを読み込んで
simplestar-tech.hatenablog.com

ひらめくコードは次の通り

        /// <summary>
        /// 小世界データ内でのチャンク情報オフセットを取得
        /// </summary>
        internal int ChunkKeyToMicroWorldChunkOffset(Vector3Int microWorldKey, Vector3Int chunkKeyXYZ)
        {
            const int halfChunk = ChunkConst.ChunkSizeX / 2;
            var offsetX = chunkKeyXYZ.x - microWorldKey.x * ChunkConst.ChunkSizeX - halfChunk;
            offsetX = AdjustLocalOffsetForChunkKey(offsetX);
            var offsetZ = chunkKeyXYZ.z - microWorldKey.z * ChunkConst.ChunkSizeZ - halfChunk;
            offsetZ = AdjustLocalOffsetForChunkKey(offsetZ);
            var offsetY = chunkKeyXYZ.y - microWorldKey.y * ChunkConst.ChunkSizeY - halfChunk;
            offsetY = AdjustLocalOffsetForChunkKey(offsetY);
            var chunkDataOffset = offsetX * ChunkConst.ChunkSizeZ * ChunkConst.ChunkSizeY + offsetZ * ChunkConst.ChunkSizeY + offsetY;
            return chunkDataOffset;
        }

        /// <summary>
        /// 小世界のキーから名前を取得
        /// </summary>
        internal string MicroWorldNameFromKey(Vector3Int microWorldKey)
        {
            return microWorldKey.x.ToString("X") + microWorldKey.y.ToString("X") + microWorldKey.z.ToString("X");
        }

        /// <summary>
        /// チャンクとキューブのキーから小世界のキューブデータのデータオフセットを取得
        /// </summary>
        internal int GetMicroWorldCubeDataByteOffset(Vector3Int chunkKeyXYZ, Vector3Int cubeInt3)
        {
            // キューブが所属する小世界名を判定
            var microWorldKey = this.ChunkKeyToMicroWorldKey(chunkKeyXYZ);
            // キューブのインデックス
            var cubeIndex = this.CubeInt3ToCubeIndex(cubeInt3);
            // 小世界データ内でのチャンク情報オフセット
            var chunkDataOffset = this.ChunkKeyToMicroWorldChunkOffset(chunkKeyXYZ, chunkKeyXYZ);
            var bufferByteChunkDataOffset = chunkDataOffset * ChunkConst.ChunkSizeX * ChunkConst.ChunkSizeY * ChunkConst.ChunkSizeZ * sizeof(int);
            // 小世界データ内でのキューブ情報オフセット
            var bufferByteCubeDataOffset = bufferByteChunkDataOffset + cubeIndex * sizeof(int);
            return bufferByteCubeDataOffset;
        }

破壊のリクエストと戻り値

バイナリキャッシュサーバーにリクエストをなげる準備が整いつつあります
キャッシュサーバー側の実装を byte オフセットに更新しました。

		offset := requestJson.Offset
		responseJson := ResponseJsonCubedata{http.StatusOK, cubedata[offset], cubedata[offset+1], cubedata[offset+2], cubedata[offset+3]}
		res, err := json.Marshal(responseJson)

		if 1 == requestJson.Action {
			cubedata[offset] = requestJson.Category
			cubedata[offset+1] = requestJson.Rotation
			cubedata[offset+2] = requestJson.SideA
			cubedata[offset+3] = requestJson.SideB
		}

クラウドスクリプトからキューブ情報の変更リクエストを行う機能を追加してみます。

// cubedata request
handlers.requestCubeData = function (args, context) {
    var headers = {
    };
    
    var body = {
        action: args.action,
        offset: args.offset,
        category: args.category,
        rotation: args.rotation,
        sideA: args.sideA,
        sideB: args.sideB
    };

    var url = "https://cubedata" + args.index + ".youredomain/cubedata";
    var content = JSON.stringify(body);
    var httpMethod = "post";
    var contentType = "application/json";

    var response = http.request(url, httpMethod, content, contentType, headers);
    return { responseContent: response };
};

実装からわかることは、リクエスト対象キューブのアクション前の値を返し
ステータスでアクションが成功したかどうかを知ることができる

完全な空気キューブが得られた場合は、何か物を空気キューブに置いたということ
空気キューブを空気キューブに置くことも考えられるが、これは意味のないこと

クライアントから試験的に呼び出せるので、キューブの破壊に合わせて遊んでみることにします。

バイナリキャッシュサーバーとCloudScriptとUnityクライアントコードを修正したので
AWS Elastic Beanstalk のデプロイと CloudScript のデプロイを先に行います。

デバッグの末、やっと C# Unity とバイナリキャッシュサーバー間で CloudScript の中継により通信が行えるようになり
CDN 経由で落としてきたデータから世界に最初から落とし穴が作られている様子を確認できました!

f:id:simplestar_tech:20191117170413p:plain
ゲーム開始直後、穴が開いている様子

所感としては、いったんはローカルで素早くキューブの設置、破壊が行えるようにして、後から本当の情報で上書きするのが良さそう
レスポンスをわざわざ待つことない→リクエストは基本的にすべて承認されるとして、待たずにローカルの状態を反映するようにした

キューブの破壊や設置でも同じロジック

バイナリキャッシュサーバーの実装を考えて、結果的にリクエストをすべてさばく仕組みを構築
クライアント改造後の不正なリクエスト連発について、攻撃には弱いですがロジックは単純です。

空気キューブを空気じゃないキューブに詰める → 空気じゃないキューブの取得
空気キューブを空気キューブに詰める → 空気キューブの取得
空気じゃないキューブを空気キューブに詰める → 空気キューブの取得
空気じゃないキューブを空気じゃないキューブに詰める → 空気じゃないキューブの取得

ひとつだけイレギュラーなものとして、ローカルでは空気キューブに空気じゃないキューブを置いたのに
なぜかインベントリに期待と異なる空気じゃないキューブが手に入るというもの

空気じゃないキューブを取得したとき、これをインベントリに詰めたい
ところで、空気じゃないキューブには sideA, sideB が入っているわけです。
それぞれ独立して CloudScript 実行後に、プレイヤーのインベントリに詰めることにします。

プレイヤーのインベントリの更新

CloudScript の実行結果を見て、成功の status 200 を受け取った時を考えてみます。

できました!

f:id:simplestar_tech:20191117192128p:plain
インベントリにキューブアイテムが追加されている様子

具体的な実装は次の通り

    var jsonString = http.request(url, httpMethod, content, contentType, headers);
    response = JSON.parse(jsonString);
    if (200 == response.status) {
        var request = {
            "PlayFabId": currentPlayerId,
            "CatalogVersion": "cube",
            "ItemIds": [
                response.sideA,
                response.sideB
            ]
        };
        server.GrantItemsToUser(request);   
    }

これでキャラクターごとにピックしたキューブ構成要素を正しく取り出すことができるようになりました。

まとめ

キューブ破壊後にキューブデータをインベントリへ格納する仕様を決めて
動作確認できました。
詳細な作業ログとともに具体的な実装を記録することができました。