simplestarの技術ブログ

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

Unity:オンラインVRM TPSゲーム作りログ2

■前書き
前回 Unity:オンラインVRM TPSゲーム作り作業ログ - simplestarの技術ブログ の続きです。
前回を要約
1.MagicOnion に習熟、形式自由でリアルタイム通信を作れるようになった
2.UI を設計して VRM ファイルとプレイヤー名を指定してサーバールームに入出するサンプルゲームが完成した
TextMesh Pro で日本語が使えるようになった
3.VRM のような 4MB 以上のファイルは送信できないことを gRPC の仕様から確かめた
4.Amazon S3 にユーザーIDで書き込み制約を与えるIAMの権限ポリシーの設定があることを公式ドキュメントから確かめた
5.Unity のクラック対策として中間言語作成前に難読化する必要があること、難読化アセットを購入した
6.次の作業として Unity から S3 アップロード、ダウンロードできるようにして url をリアルタイム通信して 4MB 以上のデータの受け渡しをする

■横道
最近待ち時間などは
にじさんじ所属、童田明治(わらべだめいじー)ちゃんの動画を観ていることが多くなった。
特にマイクラの記録を流している、かわいい
www.youtube.com

■本題
Unityにログイン、ログアウト機能と S3 を触るクライアントのインスタンス化を導入します。
新しくプロジェクト作ってみる
参考にする記事はこちら
qiita.com

まずはテストシーンで S3 に 37MB ほどある VRM をアップロード、ダウンロードできるようになるか見てみます。

まずは記事通りのサインアップ・サインインが行えることを確認しました。
S3 に触るため
NuGet Gallery | AWSSDK.S3Control 3.3.101
をライブラリに追加するのかな、やってみた

サンプルコードはどこだろう
以前の DynamoDB の扱いを真似ることを考えました、確認してみて概要をつかんでみるとこんな実装かな

var credentials = new CognitoAWSCredentials(AWSCognitoIDs.IdentityPoolId, _CognitoPoolRegion);
var _ddbClient = new AmazonDynamoDBClient(credentials , _DynamoRegion);

var request = new DeleteTableRequest
{
    TableName = @"ProductCatalog"
};
_ddbClient .DeleteTableAsync(request);

よし S3 でも同じようなサンプル書けるか試してみます。
Client は作れたんじゃないかな

ここを参考に作ってみます。
docs.aws.amazon.com

あれ S3 と S3.Control は別物?

別物ですね…

おかしいな、どこからダウンロードすれば良いのだろう、探します。

このリンクで探せばいいのか
NuGet Gallery | Packages matching Tags:"aws-sdk-v3"

すぐに見つかった
www.nuget.org

これですね S3.Control と交換しました。
使えるようになりましたね(コードのインテリセンス的に)

次はここを読んで、何かしらのオブジェクトをダウンロードする部分を書いてみます。
AWS SDK for .NET を使用したオブジェクトの取得 - Amazon Simple Storage Service

受け取った stream をそのままファイル出力するようにしてみた
まずは簡易なテキストファイルを S3 に配置して、バケット名とオブジェクトキーで取得できるか試します。

数年前の S3 バケットがあって、いらないから消そうとしたらAccess 違反とのこと
解決方法を探ってみるとバケットポリシーをいったん削除してからなら、同じ操作で解決するとのこと
管理メニューからバケットポリシーを削除することで、アクセス違反起こらずに S3 バケットを消すことが出来ました。

S3 バケットを新たに作成して、適当な浅い ObjectKey で3行だけ書き込んだテキストファイルを配置します。
これをダウンロードするUnityのコードを実行して、何も起こらないことを期待してみます。(IAMポリシーで許可していないからダウンロードされては困る)

よし、try で囲った範囲で Object reference not set to an instance of an object という例外が発生

次にログインしたときの認証ロールのIAMポリシーで S3 の全アクセス権を渡してみます。

さて、単純な S3 GetObject において例外が発生しました。
エラー情報を収集すると
"Invalid login token. Not a valid OpenId Connect identity token."

調べます。
おっと、単なるログインコードミスでした。
次のコードで使っている変数に正しい値を入れることでログイン成功して S3 から GetObject して値を取得することができました。

credentials.AddLogin($"cognito-idp.{AWSCognitoIDs.CognitoPoolRegion.SystemName}.amazonaws.com/{AWSCognitoIDs.UserPoolId}", AWSCognitoIDs.IDToken);

成功している状態を続けて、IAMのポリシーで S3 フルアクセスの権限を外した途端にアクセス違反の例外が発生したので、こちらのセキュリティまわりの動作も問題ないですね。

■S3 アップロードを試します

IAM ポリシーにて sub というユーザーごとに一意の文字列のオブジェクトキー以下しかアクセスできないような仕組みを作ってみます。
それだがガチガチの権限ポリシーにして AWS S3 のバケットへファイルを配置出来るか見てみましょう。

参考コードはこちら
AWS SDK for .NET を使用した S3 バケットへのファイルのアップロード (低レベル API) - Amazon Simple Storage Service

権限まわりはクリアしているので、ビルドを通すよう書き換えれば、アップロードできました。

IAM ポリシーをさらに厳しくして、アップロードに失敗する様子を確認します。

Cognito Identity pool の IdentityID != Cognito User pool の User sub
とのことでハマりました

ずっと Cognito User pool の User sub を渡して、アレーなんで 403 エラー帰るのかなとずっと関連ドキュメントを探し続けて
ようやく参考となる記事を発見

>Cognitoは設定値が紛らわしく、どれを使えば良いの?といった感じ
blog.naoshihoshi.com

Cognito Identity poolのIdentityID はどうやって取得するんでしょう
確かに ID プール管理の最近のログイン ID リストの値を直接指定すれば Clinet からアップロードができました
え、この ID プールの Identity ID はどうやってスクリプトから取得するんでしょう?

CognitoAWSCredentials.GetIdentityId かな?これの値を調べます。
同じ値取れましたー

これで一件落着
IAM のポリシーでアップロード対象を厳しく制限して、アップロードできることを確認しました。

■データの有効期限

設定可能?参考はこちら
www.kabegiwablog.com
作成してから S3 に配置できる最大日数などをバケット単位でルールを設定できる様子

動作確認まで記録してくれていて助かります。

■ Magic Onion と繋いで、リアルタイムダウンロード

テストとして vrm ファイルがアップロードできることを確認します。
大丈夫 37MB のファイルが Unity クライアントからアップロードされていました。

続いて、ダウンロードして byte 配列に直します。
アクセス制限がかかってました。
そういえば PutObject のみしか許可してませんでしたね。

気づいたことに
Async で 37 MB ダウンロードすると 40 秒
そうでない同期読み込みだと 3秒
という速度の差が出ました。

最初はテストなので速度の速い 3 秒を採用しました。
ダウンロードできました。

正しいデータなのか調べます。
正しいデータでした。

S3 送受信と権限ポリシーがクリアになったので、Magic Onion とのつなぎを脳内シミュレートしてみます。

手順としては、ここまでのサンプルを全部引っ越して
ログイン画面を追加しつつ
ログインできたらルーム選択と vrm 選択画面を出し

最初にvrmキャラクターをローカルで登場させ
登場を確認したら vrm データをアップロード

アップロード完了と同時にルームにジョインして
他のメンバーが一斉にダウンロードを開始する流れ

にしてみましょう。

となると、技術課題としてアップロード完了をどうやって知るのかわからないので調べます。

進捗を確認するのはあったんですけどね、完了コールバックとかはどうすれば?

わかった、Complete the upload の処理の後でいいはずですね。

順番に作っていきます。

より具体的に
最初のシーンではユーザー名と vrm 選択画面が出てきて、ゲーム開始ボタンしかありません。
白い部屋の中でチュートリアル的な行動が行えた後、出口が二つ
ソロとマルチ
の扉があります。

キーカードをかざすようにして、ソロの扉はそのまま開き
マルチの扉では、ルーム名とサインインフローが示されます。

そこでユーザーが有効なアカウント情報を入力し、有効なクレデンシャルインスタンスが生成されたら、直ちにVRMデータをアップロードし
完了と同時にルームジョインを行い、ブロードキャストしてVRMのパスを通知します。

通知を受け取った側はダウンロードを開始して、完了と同時に他のプレイヤーをゲーム内に出現させます。

あとのことは、ここまで作ってから考えます。

■最初の部屋

前回の試験環境を最初の部屋としましょう。
つまり、最初に打ち込むのはユーザー名と VRM だけ
ログイン情報はまた今度で、削る形で完成するはずです。

まぁここまではできたのですが
VthirdOersonMotor とかの、None Reference Exception が発生して
インスタンスをクリックすると発生しなくなるという意味不明なエラー解決の方法を探ることになって時間を使っています。

何なの…

これはおそらく Unity のインスペクターの初期化により、必ずインスタンス化されているはずであるという
vThirdPersonMotor の public パラメータの未初期化にアクセスしに行こうとしているからで

解決するにはサードバーティ製のコードに手を入れないといけなくなるか…
いや、Component を取得して、初期値を突っ込めばよいのか
それ採用で
具体的な解はこちら

var vController = vrmRoot.AddComponent<vThirdPersonController>();
vController.freeSpeed = new vThirdPersonMotor.vMovementSpeed();

どうなるでしょうか
問題は解決しましたが、別の問題が発生しました。
同じような対処でいけるか考えてみます。

解決できました。
ユーザーコード側で次のような対処を入れることで、一連の null 参照を解決することができました。

var vInput = vrmRoot.AddComponent<vThirdPersonInput>();
vInput.OnLateUpdate = new UnityEngine.Events.UnityEvent();
var vController = vrmRoot.AddComponent<vThirdPersonController>();
vController.freeSpeed = new vThirdPersonMotor.vMovementSpeed();
vController.OnCrouch = new UnityEngine.Events.UnityEvent();
vController.OnJump = new UnityEngine.Events.UnityEvent();
var vAction = vrmRoot.AddComponent<vGenericAction>();
vAction.OnStartAction = new UnityEngine.Events.UnityEvent();
vAction.OnEndAction = new UnityEngine.Events.UnityEvent();

カメラ位置調整のため
VRM はみな自由にオブジェクト名を決めるので、どうしてもカメラでトラッキングするオブジェクトが見付けられない
そんな問題を解決するべく次の拡張関数を開発してみた

    public static Transform FindHeightRecursively(
        this Transform self,
        float height
    )
    {
        if (height < self.localPosition.y)
        {
            return self;
        }
        foreach (Transform c in self.transform)
        {
            var child = FindHeightRecursively(c, height - self.localPosition.y);
            if (null != child)
            {
                return child;
            }
        }
        return null;
    }

使い方はこんな感じ

        var target = vrmRoot.transform.FindHeightRecursively(1.0f);
        cinemachineFreeLook.Follow = vrmRoot.transform;
        cinemachineFreeLook.LookAt = target;

結果、うまく Chest オブジェクトを選んで追従するようになりました。

今やっと

f:id:simplestar_tech:20190502215554p:plain
vrm選択画面
ができて、これを Load VRM するとキャラクターをはじまりのチュートリアル空間で動かせる状態

目的にたどりつけず、日が暮れてしまったので、次の記事へ繋げます。