simplestarの技術ブログ

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

PlayFab:ログインタイミングを気にせずプログラミングする術~ファイル編

コンテキスト

PlayFab から世界データ拾ってきたいなぁ
でもログインまだだった 失敗

なんてことが絶対に起こらないような

PlayFab から世界データ取得を成功させる仕組みを考えたい

もう作ってた!

simplestar-tech.hatenablog.com

しばらく Unity から離れてたから自分で記事にしていることすら忘れていた

このアイディアをファイルに対しても使ってみます。

ファイルのダウンロードについてはこちらの Qiita の記事が参考になりました。(これも自分が書いてる)
qiita.com

実装詳細

using PlayFab;
using PlayFab.ClientModels;
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class PlayFabContentFile : MonoBehaviour
{
    /// <summary>
    /// Content Delivery Network からファイルコンテンツを取得
    /// </summary>
    /// <param name="key">PlayFabのTitleのRoot/以下のファイルオブジェクトキー</param>
    /// <param name="onComplete">完了時アクションでnullが返ってきたら失敗を意味する</param>
    public void GetContentFileData(string key, Action<byte[]> onComplete)
    {
        GetDownloadUrl(key, presignedUrl =>
        {
            GetFile(key, presignedUrl, onComplete);
        });
    }

    void GetDownloadUrl(string key, Action<string> onComplete)
    {
        PlayFabLogin.AfterLoginCall(() =>
        {
            PlayFabClientAPI.GetContentDownloadUrl(new GetContentDownloadUrlRequest()
            {
                Key = key,
                ThruCDN = true
            }, result => onComplete?.Invoke(result.URL),
            error => Debug.LogError(error.GenerateErrorReport()));
        });
    }

    void GetFile(string key, string preauthorizedUrl, Action<byte[]> onComplete)
    {
        StartCoroutine(this.GetData(key, preauthorizedUrl, onComplete));
    }

    IEnumerator GetData(string key, string preauthorizedUrl, Action<byte[]> onComplete)
    {
        UnityWebRequest www = UnityWebRequest.Get(preauthorizedUrl);
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.LogError(www.error);
            onComplete?.Invoke(null);
        }
        else
        {
            // 結果をバイナリデータとして取得する
            onComplete?.Invoke(www.downloadHandler.data);
        }
    }
}

テストコード

// オンラインから現在のプレイヤーチャンクが所属する世界データを読み込む
// this.playerChunkCenter→"000"
var worldIndex = "000";
var objectKey = $"world/cubedata{worldIndex}.gz";
playFabContentFile.GetContentFileData(objectKey, cubedata => {
    if (null != cubedata)
    {
        Debug.Log($"download length = {cubedata.Length}");
        cubedata = GZipCompressor.Decompress(cubedata);
        Debug.Log($"unzip length = {cubedata.Length}");
    }
    else
    {
        Debug.LogError($"キー{objectKey}でダウンロードできなかったんだけど…");
    }
});

実行結果

一度も失敗を経験することなく、期待通りの動作を確認できました。

f:id:simplestar_tech:20191113234155p:plain
大成功!

データの解凍についてはこちらの記事を参照
baba-s.hatenablog.com

自分はこんな実装が欲しかったのでちょっと変更

using ICSharpCode.SharpZipLib.GZip;
using System.IO;

// 使い方
//var compressedData = GZipCompressor.Compress(rawData);
//var rawData = GZipCompressor.Decompress(compressedData);

/// <summary>
/// gzip で byte[] の圧縮や解凍を行うクラス
/// </summary>
public static class GZipCompressor
{
    public static byte[] Compress(byte[] rawData)
    {
        using (var memoryStream = new MemoryStream())
        {
            Compress(memoryStream, rawData);
            return memoryStream.ToArray();
        }
    }

    public static byte[] Unzip(byte[] compressedData)
    {
        using (var memoryStream = new MemoryStream())
        {
            Decompress(memoryStream, compressedData);
            return memoryStream.ToArray();
        }
    }

    private static void Compress(Stream stream, byte[] rawData)
    {
        using (var gzipOutputStream = new GZipOutputStream(stream))
        {
            gzipOutputStream.Write(rawData, 0, rawData.Length);
        }
    }

    private static void Decompress(Stream stream, byte[] compressedData)
    {
        var buffer = new byte[4096];
        using (var memoryStream = new MemoryStream(compressedData))
        using (var gzipOutputStream = new GZipInputStream(memoryStream))
        {
            for (int r = -1; r != 0; r = gzipOutputStream.Read(buffer, 0, buffer.Length))
            {
                if (r > 0)
                {
                    stream.Write(buffer, 0, r);
                }
            }
        }
    }
}

ゲームではこのデータの解凍処理が終わったところで、それぞれの世界データを確保してデータを書き込み
これを使って世界メッシュの生成が進むとよいだろうと思う