simplestarの技術ブログ

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

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

■前書き
今回でこの連番タイトルもラストになりそうな進捗でてます。

前回の記事はこちら
simplestar-tech.hatenablog.com
要約すると
1.送信するタイミングとデータ構造を知るための調査
2.送受信のRPCインタフェースを設計
3.実際に送受信できていることを確認
でした。

今回は最後の繋ぎこみ部分、これがうまくいけばキャラクターはレイテンシが小さい状態で同期してアニメーションしてくれるはず。

■キャラクターの移動と回転
速度と角速度の適用

送られてくるデータを適用するには何かしらのキーでゲームオブジェクトを見分けないといけません。
プレイヤー名は自由に決められてしまうので、GUID を生成してから、それを整数値にすることで、これをプレイヤーIDとしました。
重なったら奇跡としましょう。

GUID の生成と文字列にするフォーマットまではここを参考にし
C#でGUIDをToStringするときに使用できる書式 - PG日誌

文字列から int 値を作る書式はここを参考にしました。
C# GUIDをシードにして整数の乱数を作る - 備忘録

ついでに Unity 2019.1.1f1 に更新して作業再開

最初はキーマップに VRM インスタンスを登録して、これを参照しつつ、受け取った位置と回転を適用するサンプルの動作を見てみます。
動きは完璧でした。

補間をどうするか考える必要があります。

■キャラクターのモーションの再生
CrossFace できることは知っているので、これを実装して動きを見てみましょう。
送受信のインタフェースは完成しているので、実際に送受信するコードだけは書いてしまいましょう。
書きました。

受け取ったモーションパラメータの適用を書いていきます。(ローカルで動作確認済みなので、期待できる)
書きました。

動作確認してみます。
念のため、受信しているかのログも記載しておきます。

動作の同期は完璧でした。
あとは、ログを取り除いて、送受信する頻度を state 変化は毎フレーム適用するようにしてみます。

悲しいことにログ吹っ飛んじゃった…

なんやかんやで、リアルタイム通信同期できるようになりました。

作業ログはここまでとします。

■補間
これは記録すべきだなぁと後から思い出したのでここに書くことにします。

using UnityEngine;

public class TransformInterpolation : MonoBehaviour
{
    internal Vector3 position;
    internal Quaternion rotation;

    [SerializeField] float acceleration = 0.2f;
    [SerializeField] float convergence = 0.9f;
    [SerializeField] float warpDistance = 1.5f;
    [SerializeField] float rotateSpeed = 500f;

    float velocity = 0;

    void Update()
    {
        transform.position = Vector3.MoveTowards(transform.position, position, velocity * Time.deltaTime);
        var distance = Vector3.Distance(position, transform.position);
        if (0.001f > distance)
        {
            velocity *= convergence;
        }
        else
        {
            velocity += acceleration;
        }
        velocity = Mathf.Clamp(velocity, 0, 5);
        if (warpDistance < Vector3.Distance(transform.position, position))
        {
            transform.position = position;
        }        
        transform.rotation = Quaternion.RotateTowards(transform.rotation, rotation, rotateSpeed * Time.deltaTime);
    }
}

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

■前書き
前回の記事はこちら
simplestar-tech.hatenablog.com

前回の内容をまとめると
1.扉のあるチュートリアル閉鎖空間を用意し、VRMファイル選択で動的に VRM キャラクターを読み込める
2.扉の前でアクションを行うとAWS Cognito のサインアップ・サインインが行える
3.サインイン後は S3 に VRM をアップロードし、プレイヤー名を決めて Magic Onion のサーバーに入室する
4.二人目のプレイヤーが入室した通知で S3 オブジェクトキーを取得し、これを S3 からダウンロードしてシーン内に二人目のプレイヤーの姿が表示される
5.プレイヤーに外部から Animator パラメータを与えることで自由に操作できることを確認
6.Animator から同期するべき情報を取得できそうな関数を前調査、報告

といったものでした。

今回は、前調査でわかっている Unity の Animator のパラメータの変化を観察して送受信するインタフェースを設計します。
観察→理解→想像→試行→観察 のループを高速で回して、機械が完成する感じのことします。

■横道
本筋とは関係ないけど、気になるのは S3 アップロードは即時完了しないので、可能なら Magic Onion サーバーに入ることを決めたら
そのときはログイン処理中とかのメッセージを出し、S3 アップロード完了と同時にサーバールームに入室を決めたい

あとは、S3 アップロード権限が無いとかで失敗したり、サーバーに入れなかった時などはその旨を表示して、ローカルのゲームに戻ってこれるつくりも入れてあげたい

一般公開したときは、ルーム最大数を越えたら、その旨で弾かれるつくりとかもいいね
あと、チャット機能と表情やエモーションアクションを送れるメニューと機能も欲しいところ

■本題
Animator のパラメータの変化を検知して、これを報告するプログラムを書いてみます。
期待通り動くかな

layer は 5つ、そのうち、アクションをしている最中は State の nameHash 値が -1903714832 のときローリング
直立ロコモーション時は 1905792070 の値になることを確認

hash 値を与えて CrossFade できるか確認します。
できました。

気づいたことに、Invector のコンポーネントの割当たっている VRM キャラに CrossFade させると移動し、そうでない VRM キャラはその場でアニメーションしました。
Root Motion の違い?
yes

ただ、単に apply root motion にチェックを入れると、アイドル時に振動してしまうので、何らかのスクリプト編集が必要そう

そうだ、 Animator.applyRootMotion も同期するステートにしてしまおう
動的に切り替えて、期待通りのモーションとなることは確認できた。

パラメータ、レイヤーごとのステート、あとは位置と回転と速度などですね
これも Animator から取れるのでしょうか

取れますが、OnAnimatorIK とか OnState とかの関数内で呼べと怒られました。

Debug.Log($"bodyPosition = {this.debugAnimator.bodyPosition}");
Debug.Log($"rootRotation = {this.debugAnimator.bodyRotation}");
Debug.Log($"velocity = {this.debugAnimator.velocity}");
Debug.Log($"angularVelocity = {this.debugAnimator.angularVelocity}");

速度や角速度が取れるのはいいですね、これなら毎フレーム送らなくても滑らかな補間が行える?

この関数はいったい
yutakaseda3216.hatenablog.com

無知だったので、落とし穴に気付かせてもらい、助かりました。

OnAnimatorIK 関数はどうでしょう。
Animator に IK レイヤーを一つ加えると、呼び出してもらえるようになり警告が消えました。

小ゴールとして、この関数でモニタリングしつつ、頻度を調整できるようにしつつ、前回ブロードキャストした状態からの差異をまとめて列挙できる仕組みを書いてみます。

定期的ってのは FPS とかで調べると解決
OnAnimatorIK は 50 FPS であることもわかった。

アニメーションが設定されていると Body の位置が移動する
Distance 計算して余白を求めればゆるやかに更新

ところで BodyPosition の更新は意味あるのか
意味なかった、なんと、ではこのオブジェクトの transform を利用することにします。

これなら、適用後
移動することを確認

アニメーターパラメータも差分をチェックする機構を作って動作を見てみます。
問題なさそうですね

アニメーションステートを見ます。
こちらも差分の検出は問題なし

animator.applyRootMotion は変化を受け取れなかったので、無視します。

これで小ゴール達成です。

■スナップショット差分が発生したデータだけを送信するインタフェース

値に変更があったものを瞬時に送信できるような仕組みとするので
差異があったパラメータを配列にして送信できたらいいなと

動的に増減するので List 型にしてみる

まずはリストが変かあったときに要素を持ち、これを送信する
そんなリストが作られる様子を確認してから

それを送信できるインタフェースを Magic Onion 側に用意してコードジェネレートしてビルド通してみましょう

リストになれない
値が float, int, bool, Vector3, Quaternion に分かれます。

name hash を送信すれば float, int, bool はそれぞれリスト化できそう
インタフェースは三つになる形か

レイヤーごとのアニメーションステートは一つインタフェースを呼ぶ形

Vector3 とかはそれぞれインタフェースを用意してしまう形で x 4 ってことで

となるとインタフェースは全部で 8 つ増えることに
List に関してだけ namehash と value のセットを送信する形になりますね。

作って、送受信できているか、試してみます。

サーバー側でも UnityEngine.dllを選んでAddをクリックして参照追加すれば Vector3 を引数に指定して処理できるようになります。
C:\Program Files\Unity\Hub\Editor\2019.1.0f2\Editor\Data\Managed\UnityEngine.dll
これです。

あれ、サーバーとして起動できない。
プロジェクトに含めないといけない様子だが
ビルドは通るけど、ランタイムエラーという

Magic Onion で Unity Vecotr 3 とか使えた気がしたけど、気のせい?
全部 float にして送受信するように書き直して Magic Onion のコード生成を実行

ミスマッチで作れないとエラー

追加した行をすべて元に戻して、同じエラー

内容が読めないけど、これは?
もう見落としている箇所が見つけられないのだけど…

入念にチェックをしてみます。

エラーが発生するのはコードを書いていた部分ではなく Unity アセンブリからの MessagePack コード自動生成の部分でした。
なぞ、解決方法がわからん。

簡易なプロジェクトで Magic Onion だけのプロジェクトを作ってコードジェネレートだけするというのがいいのかな…
それで解決を試みていきましょう。

通信はできるようになった、けど
同じアカウントで複数ログインを試行すると、プレイヤーIDが重なってしまう。

ここはプレイヤーごとに異なる、ログインハッシュ値を一時的に作成してユーザーの重なりを除去するのが良いか
NameToHash を使わせてもらうのがよいか

リアルタイムに情報を送受信しあっているので、小ゴールはクリアしていた

■日が暮れた…
続きは、次の記事に書きます。

改竄防止しつつ、コードを変えずにパラメータを再利用する案

ゲーム内コードで自らの dll の hash 値を計算して、それと公開鍵によって解錠する暗号化されたファイルを設定ファイルとして外部に配置し
ゲーム外コードから dll の hash 値と秘密鍵で設定ファイルを暗号化しておけば、ゲームロジックに変更を加えると解錠できなくなってゲームに接続できない
& 設定ファイルを複合化できないから、コードに直接データを埋め込むよりは良いかも

だめか

編集前の hash 値は割れるので、複合化のコードが割られれば、hash 値計算部分を改竄して編集前の hash 値を利用するように変えてあげれば、あとはコードの改竄し放題じゃん

でも、ガードは固くなったので
コードを難読化して、hash 値計算している箇所を隠すようにできれば
大分、ライトな層のクラッカーをあきらめさせられるかな

暗号化とかはこれで

追記:
ゲームのロジックとは別のアセンブリから、そのゲームロジックのアセンブリハッシュ値を計算してユーザー管理サーバーに送り
サーバーが期待するハッシュ値と比較、違っていたらロジックの改ざんが確定するので、然るべきチーターのための処理フラグをサーバー側で立ててあげる
それとなくリーダーボードから外されていて、報酬がもらえないなど、チート判定されたことの発見が遅れるとか、本人から不具合報告の連絡をさせて、犯罪者をあぶり出すとか
ハッシュ値の送信箇所まで改ざんされたら終わりだけど、ただ、ゲームロジックをどんなに眺めても解放にたどり着けないはず(予想が的中されることはありそうだけど)
これもライトな層のクラッカーをあきらめさせる程度のものでしかないか

あ、このページを見られて予想されるという線もある

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

■前書き
https://simplestar-tech.hatenablog.com/entry/2019/05/02/073820 の続きです
前回を要約すると

具体的には、チュートリアルルームを作りこんで、出口があり
出口にキーカードをかざすようにして、プレイヤー名、サインインするフローを用意し
プログラムを繋ぎこみます。

■横道
Unity プロジェクト単体によるリアルタイム通信と数十MB単位の巨大なデータの即時受け渡しができるようになったわけで
これってかなり自由にオンラインゲーム作れるってことじゃないですか
土台として固めて、なんとか形にしたい

■本題
まずは出口付近でアクションするとカードキーをかざす動作をするというものを作りますか

一度もそんなアニメーションを再生したことないな、どうすれば?
あと、ボタンの関連づけをより自然に直します。

いい感じの扉のあるチュートリアルルームのアセット買ったのでライトベイクはじめたら、永遠に終わりそうにない…

あと5分して進捗なければ消します。
進捗あったので残しました。

ボタン配置はどこで変えられる?
これで変えられました

var vInput = vrmRoot.AddComponent<vThirdPersonInput>();
vInput.jumpInput = new GenericInput("Space", "A", "A");
vInput.rollInput = new GenericInput("Q", "Y", "Y");
vInput.crouchInput = new GenericInput("C", "X", "X");
var vAction = vrmRoot.AddComponent<vGenericAction>();
vAction.actionInput = new GenericInput("E", "B", "B");

ライトベイクはよくわからないが、扉の前でカードキーイベントを発生させたい。
次に、カードキーアクションを発生させる environment があるか調べてみます。

vTriggerGenericAction クラスをアタッチした Collider が指定したアニメを再生させる仕組みの様子
動きを見たら実装を追ってみます。

Animation名を指定する欄で Press_Lever を記入したら、ドアのレバーアクションが行われることを確認できました。

■ドアアクションで、ルームログインのパネルを表示

具体的に
ユーザー名、パスワード→初めての方はWebを開く
ログイン完了と同時に S3 へ現在の vrm データをアップロード
プレイヤー名を記入する項目が出てきて、ディフォルト値があれば利用
オンラインルームに入る、ボタンを押すと
アップロード完了時のイベントで vrm のダウンロード用のオブジェクトキーを作成
オブジェクトキーが作成されるのを待ち、 Magic Onion のリアルタイム通信の規定ルームに入る

をやってみます。

Sign Up ボタンを押すと所定の url に飛ぶような Unity 機能ありますかね
これかな
Unity - Scripting API: Application.OpenURL

using UnityEngine;

public class OpenURL : MonoBehaviour
{
    [SerializeField] string url;

    public void OnClick()
    {
        Application.OpenURL(url);
    }
}

期待通り機能しました。
アクションが発生したときのイベントもあるので、これを利用します。

S3 アップロードのサンプル一式をコピーします。

コピーしたのは
Assets/AWS_packages をまるごとコピー
AWSS3 サンプルフォルダを切って、サンプルのスクリプトファイルをコピーしました。

UI が出てもマウスカーソルが表示されません。
誰がどこで制御しているのでしょうか

■UI表示時にのみ、マウスカーソルを表示

参考:
kan-kikuchi.hatenablog.com

たぶんどこかでCursor.lockState を制御しているコードがあるんじゃないかな
発見

vThirdPersonInput.cs

        public virtual void LockCursor(bool value)
        {
            if (!value)
                Cursor.lockState = CursorLockMode.Locked;
            else
                Cursor.lockState = CursorLockMode.None;
        }

外部から制御するため、この関数を呼ばせてもらいますか
正常に機能しました。

■UI操作中はキャラクターアクションしないでほしい
インプットを切るとかあるんですかね

Invector の Trigger アクションの With GameObject の使い方を知りたい
実装を追うと UnityEvent の使い方になった
www.urablog.xyz

あれ、UnityEvent call 対象のコールバック関数に問題あるのかなー?呼ばれるけど引数が null になってしまう

Invoke するときは GameObject 渡しているのに

どういうこと?
ここを読みます。
UnityEvent - Unity マニュアル

足りない!次
unitygeek.hatenablog.com

設定可能な関数一覧リストは、'Dynamic'と'Static'の2つに分けられている
Dynamic/Staticの両方に表示されている。引数を動的に指定したい場合は、'Dynamic'の方を使う。

この説明で理解しました。
私はいつも通り静的な関数を設定していました。
ここを動的な関数に選択しなおします。

その後に lockInput フラグを変更すれば、ロックはできるはず
できました。

イベントの動作だけブロックできなかった。
GenericAction についてもこんな対応を入れて解決する?

this.lockInputTarget = vrmRoot.GetComponent<vThirdPersonInput>();            
this.lockInputTarget.lockInput = true;
this.lockActionTarget = vrmRoot.GetComponent<vGenericAction>();
this.lockActionTarget.actionInput.useInput = false;

解決しました。

■プレイヤー名記入とチャットルームへのジョイン

今の進捗を確認すると、VRM 選択→チュートリアルルームで自由行動
扉の前で入力モードが切り替わり、Amazon Cognito の認証フロー完了 + S3 のクライアント有効化まで

パネルは引き続き、プレイヤー名とチャットルームへのジョインについて切り替わるべきで、入力ロックもまだ続けておく必要があった
そうした変更を反映させます。

S3 アップロードが完了できていること
ジョインすることができ、ジョイン完了の通知が届くことを確認しました。

■S3アップロード完了の通知とオブジェクトキーの送信

C# のコールバックの書式は…調べます。
Action なんちゃらだったと思ったけど

UnityAction callback

っていうのが使えた
オブジェクトキーの受信まで確認できました。

■別プレイヤーがログオンした際のオブジェクトキーによるダウンロード

別のプレイヤーが入ってきたときに、そのプレイヤーをインスタンス化する処理を書きます。
他のプレイヤーが参加してきたときにオブジェクトキーを取得できることを確認したので、これをダウンロードして byte 配列にします。
そしてこれを VRM インスタンスとして配置し、username ごとの配列

さて、ダウンロードしたデータから GameObject が作成されることをデバッグ実行して確認したが、シーン内にいない
どういうこと?

追記:vThirdPersonController がシングルトンとして、二つ目を消すコードになってたからでした。

あと、ビルドしたクライアントを終了するとフリーズする

プレイヤータグを付けないようにしてみるなど
現れた
相変わらず終了時にフリーズする
仕方ない、アプリ終了時に何かするコードを仕込みます。

それでもダメ Esc で必ずログアウトとCleanUp するようにしてみたがどうか
フリーズは避けられたけど、サーバー側で例外が発生していた

Leave を無理に呼ばないようにするとか
いえ、Leave イベントこなくなり悪化しました。

Quit するのをずっと先にしてみるとか

結局解決せず、最低限、フリーズしないようにするワークアラウンドのコードがこちら

void Start()
{
    this.InitializeClient();
    Application.wantsToQuit += WantsToQuit;
}

bool WantsToQuit()
{
    CleanUpMagicOnion();
    return false;
}
        
private async void CleanUpMagicOnion()
{
    if (this.isJoin)
    {
        await this.streamingClient.LeaveAsync();
        this.isJoin = false;
    }
    // Clean up Hub and channel
    await this.streamingClient.DisposeAsync();
    await this.channel.ShutdownAsync();

    Application.wantsToQuit -= WantsToQuit;
    Application.Quit();
}

エラー内容はサーバーからのステータス送信に失敗というものなので、クライアントは正しく切断処理をしていそう
サーバー側に流れるエラーは、ここまでの作業で無視することにしました。

エラー内容

W0503 19:36:35.140967 Grpc.Core.Server Exception while handling RPC. System.InvalidOperationException: Operation is not valid due to the current state of the object.

小ゴールとして、正しくダウンロードしてキャラクタがゲーム内に表示されているのでよしとします。

デバッグ機能:ダウンロードしたキャラクターを何かしらのインプットで動かす

通信する内容を精査していきます。
現在、他プレイヤーのデータをダウンロードして VRM キャラがゲーム内に出現している状態です。
これを操作するように出来る機能をローカルで確認していきます。

まずは、ファイルからデータを取り出してゲーム内に出現させるフローを確立します
そのあとにいじっていきましょう。

Animator は割り当たっているから、ステートマシンの再生でアクションできるはずなので、外から強制的に動かす信号を送れるようにしてみます。
具体的には

Animator のステートとしてディフォルトでは LocoMotion が選択されている
Animator パラメータの Inut Magnitude を 0~1.5 で操作し
各種 InputHorizontal , InputVertical などを増減させると歩きモーションを変えられる

ステートの変化はどうやっているか調べてみると

animator.CrossFadeInFixedTime("StateName", 0.1f);

試しにローリングするか調べてみます。
位置に変化がない状態でその場でローリングを行いました。

なるほど、アクションが実行されたら、そのタイミングでアクションをブロードキャストすると同期は可能ですね。
Animator パラメータは入力に変化があったときにスナップショットを送り付ける
定期的に絶対座標と回転をブロードキャストして補間するという仕組みがぱっと浮かびました。

ひとまず小ゴールクリアですね。考えていきます。

■キャラクターのステートの変化をキャッチしてブロードキャストする

アクションの実行など、アニメーションパラメータやステートに変化が生まれた時にこれを察知するスマートな方法は議論されていないのでしょうか。
調べます。

コード見る感じ AnimatorControllerParameter[] parameters { get; } でステートを逐一チェックして
ローカルで大きな変化があったパラメータをリストアップして、変化したパラメータをブロードキャストするとか?

全部ハッシュ値でアクセスしつつ、これ毎フレームチェックするべきなのかな
計算コスと大きいかな

でも通信量を極力抑えることの方が大事だと思われる

アニメーションパラメータは値が前フレームの状態より変化したらとしますか、これなら実装が見えるので作業するだけかな
アクションはどうする、これもステートとしてとれる?

レイヤーごとにこれで取ってこれそう
AnimatorStateInfo GetCurrentAnimatorStateInfo(int layerIndex);

関連
int layerCount { get; }


StringToHash でステート名を hash にして、hasy 値で StateInfo は構築されている様子、効率的に処理できそう

bodyPositionとかRotationとか
deltaRotation, deltaPosition ってのはアニメだけの話?

ちょっとモニタリングしてみます。

日が暮れたので、次の記事につなげます。
やっと試行錯誤できてますね。GWの目的達成です。

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 するとキャラクターをはじまりのチュートリアル空間で動かせる状態

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