20201204のUnityに関する記事は14件です。

【Unity(C#)】Mirrorで同期通信① マッチング機能

Unity #2 Advent Calendar 2020

こちらは Unity #2 Advent Calendar 2020 の 6日目の記事です。

Mirror

オープンソースのネットワークライブラリ(アセット)です。

プレイヤーのマッチングに公式サーバーが必要ないので
同一LAN内が担保されていれば接続が可能です。

当然、サーバーをゴリゴリ頑張れば自前運用も可能です。

【参考リンク】:無料で使えるネットライブラリMirrorのざっくり紹介

公式Discordに参加してみましたが、
アップデートが頻繁に行われているのもあってか、
実装上の質問も飛び交って賑やかでした。(全部英語です)

今回やること

①同一LAN内のサーバー(ホスト)を検索
②サーバーが見つかればクライアントとして接続、なければ自身がサーバー(ホスト)になる
③サーバー(ホスト)がマッチングを確認し、ゲームを開始する
④サーバー(ホスト)、各クライアント、共にシーン遷移する

一言でまとめるとオートマッチングシステムを作ります。

バージョン

Unity 2019.4.8f1
Mirror 26.2.2
UniTask.2.0.18

デモ

左上が自動でホストになり、残りの3画面がクライアントとして接続を試みます。
MirrorForQiita1.gif

同一LAN内が前提なのでIPアドレスの入力などは省略できます。

コード(CustomNetworkDiscovery )

まずはサーバーを検索し、接続するための処理を担うコードです。

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Mirror;
using Mirror.Discovery;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// サーバー検索、接続
/// </summary>
public class CustomNetworkDiscovery : NetworkDiscovery
{
    [SerializeField] private Button _multiPlayButton;
    [SerializeField] private Button _backButton;
    [SerializeField] private Button _playButton;
    [SerializeField] private Text _playerCountText;
    [SerializeField] private Text _connectionStateText;
    //SceneのアトリビュートはMirrorに用意されている便利機能
    //Inspectorでシーンを参照してコード内で文字列として使用できる
    [SerializeField,Scene] private string _gameSceneName;

    private ServerResponse _discoveredServer;
    private CancellationTokenSource _cancellationTokenSource;

    private const int CONNECT_INTERVAL_TIME = 2;
    private const int WAIT_TIME = 2;
    private const int CONNECT_TRY_COUNT = 1;

    private const string CONNECTION_STATUS_CLIENT_WAITING = "Waiting start...";
    private const string CONNECTION_STATUS_HOST_WAITING = "Waiting other player...";
    private const string CONNECTION_STATUS_SUCCESS = "Success!";

    private bool _isHostReady;
    private NetworkManager _networkManager;

    private void OnDestroy()
    {
        //シーン遷移などで破棄されたタイミングで検索をやめる
        StopDiscovery();
    }

    private void Awake()
    {
        //データ受信の準備
        NetworkClient.RegisterHandler<SendHostReadyData>(ReceivedReadyInfo);
        NetworkClient.RegisterHandler<SendPlayerCountData>(ReceivedPlayerCountInfo);

        //サーバー見つけたらこれが呼ばれる
        OnServerFound.AddListener(serverResponse =>
        {
            //見つけたサーバーを辞書に登録
            _discoveredServer = serverResponse;
            Debug.Log("ServerFound");
        });

        //サーバーの検索&接続開始
        _multiPlayButton.onClick.AddListener(() =>
        {
            Debug.Log("Search Connection");
            _backButton.transform.gameObject.SetActive(true);
            _multiPlayButton.transform.gameObject.SetActive(false);

            //接続を試みる
            _cancellationTokenSource = new CancellationTokenSource();
            CancellationToken token = _cancellationTokenSource.Token;
            TryConnectAsync(token).Forget();
        });

        //最初の画面に戻る
        _backButton.onClick.AddListener(() =>
        {
            Debug.Log("Cancel");

            //サーバーから抜ける
            //サーバーの検索停止
            StopDiscovery();
            NetworkManager.singleton.StopHost();

            //非同期処理止める
            _cancellationTokenSource.Cancel();
            _cancellationTokenSource.Dispose();

        });

        //ホスト側にのみ表示されるボタン プレイボタン押下で準備完了とする
        _playButton.onClick.AddListener(() =>
        {
            Debug.Log("Ready Ok");
            //各クライアントにフラグデータを送る
            SendHostReadyData sendData = new SendHostReadyData() {IsHostReady = true};
            NetworkServer.SendToAll(sendData);

            _playButton.transform.gameObject.SetActive(false);
        });
    }

    /// <summary>
    /// サーバーから受け取ったデータを各クライアントで使う
    /// </summary>
    /// <param name="conn">コネクション情報 関数内で使ってないけど必要みたい</param>
    /// <param name="receivedData">受け取ったデータ</param>
    private void ReceivedReadyInfo(NetworkConnection conn, SendHostReadyData receivedData)
    {
        //ローカルのフラグに反映
        _isHostReady = receivedData.IsHostReady;
    }

    /// <summary>
    /// サーバーから受け取ったデータを各クライアントで使う
    /// </summary>
    /// <param name="conn">コネクション情報 関数内で使ってないけど必要みたい</param>
    /// <param name="receivedData">受け取ったデータ</param>
    private void ReceivedPlayerCountInfo(NetworkConnection conn, SendPlayerCountData receivedData)
    {
        if (_playButton == null) return;

        _playerCountText.text = receivedData.PlayerCount + "/" + _networkManager.maxConnections;
    }


    /// <summary>
    /// 接続を試みる
    /// 非同期
    /// </summary>
    private async UniTaskVoid TryConnectAsync(CancellationToken token)
    {
        _networkManager = NetworkManager.singleton;

        int tryCount = 0;

        //サーバーの検索開始
        StartDiscovery();

        //サーバーに接続するまでループ
        while (!_networkManager.isNetworkActive)
        {
            //n秒間隔で実行
            await UniTask.Delay(TimeSpan.FromSeconds(CONNECT_INTERVAL_TIME), cancellationToken: token);

            //サーバー発見した場合
            if (_discoveredServer.uri != null)
            {
                Debug.Log("Start Client");
                //クライアントとして接続開始
                _networkManager.StartClient(_discoveredServer.uri);
                //接続ステータスの文言変更
                _connectionStateText.text =CONNECTION_STATUS_CLIENT_WAITING;
                //サーバーの検索停止
                StopDiscovery();
                //ここでホストの開始フラグを待つ
                await UniTask.WaitUntil(() => _isHostReady, cancellationToken: token);
                //接続ステータスの文言変更
                _connectionStateText.text = CONNECTION_STATUS_SUCCESS;
            }
            //サーバー見つからない場合
            else
            {
                Debug.Log("Try Connect...");

                //接続を試みた回数をカウントアップ
                tryCount++;

                //任意の回数以上接続に試みて失敗した場合は自身がホストになる
                if (tryCount > CONNECT_TRY_COUNT)
                {
                    Debug.Log("Start Host");

                    //ホストになる(サーバー)
                    _networkManager.StartHost();
                    //サーバーあるよーってお知らせする
                    AdvertiseServer();

                    //接続ステータスの文言変更
                    _connectionStateText.text = CONNECTION_STATUS_HOST_WAITING;

                    //プレイボタン表示
                    _playButton.gameObject.SetActive(true);

                    //ここでホストの開始フラグを待つ
                    await UniTask.WaitUntil(() => _isHostReady, cancellationToken: token);
                    //接続ステータスの文言変更
                    _connectionStateText.text = CONNECTION_STATUS_SUCCESS;
                    //n秒待つ
                    await UniTask.Delay(TimeSpan.FromSeconds(WAIT_TIME), cancellationToken: token);
                    //シーン遷移
                    _networkManager.ServerChangeScene(_gameSceneName);
                }
            }
        }
    }
}

NetworkDiscovery

サーバーを検索、もしくはサーバーが自身の存在を通知する機能を持ちます。

NetworkDiscoveryはそのまま使用することもできますが、
UIをカスタマイズしたかったり、
シーン遷移時のフェードアニメーションなどを追加したかったりする場合には
カスタムしないと難しいです。

そのために継承して利用しています。

StartDiscovery,StopDiscovery,AdvertiseServerなどは
NetworkDiscoveryの機能に当たります。

これらの機能は名前のまんまです。
ただし、シーン遷移時にしっかりとサーバーの検索、通知を停止させないと
サーバーは停止しているのにレスポンスだけは返ってくるという謎の減少が起きるので
OnDestroyで確実にStopDiscoveryするのが安全だと思います。


NetworkServer.SendToAll

サーバー内のすべてのクライアント(ホスト含む)に引数で指定したデータを送信します。

CustomNetworkDiscovery内ではホストがプレイボタンを押したことを各クライアントに通知しています。

   //ホスト側にのみ表示されるボタン プレイボタン押下で準備完了とする
   _playButton.onClick.AddListener(() =>
   {
        Debug.Log("Ready Ok");
        //各クライアントにフラグデータを送る
        SendHostReadyData sendData = new SendHostReadyData() {IsHostReady = true};
        NetworkServer.SendToAll(sendData);

        _playButton.transform.gameObject.SetActive(false);
   });

NetworkClient.RegisterHandler

先ほどのSendToAllでデータが送られてきたことを検知し、
各クライアントでデータの受信時に行いたい処理を登録できます。

(引数のNetworkConnectionは別になくても動きます。)

private void Start()
{
    //データ受信の準備
    NetworkClient.RegisterHandler<SendHostReadyData>(ReceivedReadyInfo);
}


/// <summary>
/// サーバーから受け取ったデータを各クライアントで使う
/// </summary>
/// <param name="conn">コネクション情報 関数内で使ってないけど必要みたい</param>
/// <param name="receivedData">受け取ったデータ</param>
private void ReceivedReadyInfo(NetworkConnection conn, SendHostReadyData receivedData)
{
    //ローカルのフラグに反映
    _isHostReady = receivedData.IsHostReady;
}

やり取りするデータも別途定義が必要となります。
NetworkMessageというインターフェースを実装することで
やり取りが可能なデータとなります。

using System;
using Mirror;

/// <summary>
/// 送信するデータ
/// </summary>
[Serializable]
public struct SendHostReadyData : NetworkMessage
{
    /// <summary>
    /// ホストが準備できたかどうか
    /// </summary>
    public bool IsHostReady;
}

コード(CustomNetworkManager)

次に接続にまつわるコードです。

using Mirror;
using UnityEngine;
using UnityEngine.SceneManagement;

/// <summary>
/// 接続にまつわるいろいろ
/// </summary>
public class CustomNetworkManager : NetworkManager
{
    [SerializeField,Scene] private string _titleScene;
    [SerializeField,Scene] private string _mainScene;

    private Transform _playerTransform;
    private Material _playerMaterial;

    /// <summary>
    /// プレイヤー入室時にサーバー側が実行
    /// </summary>
    /// <param name="conn">接続されたプレイヤーのコネクション</param>
    public override void OnServerAddPlayer(NetworkConnection conn)
    {
        Debug.Log("Add Player");

        //タイトルシーンでのみ実行
        if (_titleScene.Contains(SceneManager.GetActiveScene().name))
        {
            //接続中の人数表記を変える
            SendPlayerCountData sendData = new SendPlayerCountData() {PlayerCount = NetworkServer.connections.Count};
            NetworkServer.SendToAll(sendData);
        }

        //メインシーンでのみ実行
        if (_mainScene.Contains(SceneManager.GetActiveScene().name))
        {
            Debug.Log("Spawn Player");
            //プレイヤー生成
            GameObject player = Instantiate(playerPrefab);
            //今立ち上げているサーバーにプレイヤーを追加登録
            NetworkServer.AddPlayerForConnection(conn, player);
        }
    }

    /// <summary>
    /// 各プレイヤー退室時にサーバー側が実行
    /// </summary>
    /// <param name="conn">切れたコネクション</param>
    public override void OnServerDisconnect(NetworkConnection conn)
    {
        //接続中の人数表記を変える
        SendPlayerCountData sendData = new SendPlayerCountData() {PlayerCount = NetworkServer.connections.Count};
        NetworkServer.SendToAll(sendData);
        Debug.Log("Anyone Disconnect");
        base.OnServerDisconnect(conn);
    }

    /// <summary>
    /// サーバーとの接続が切れた時にクライアント側で呼ばれる
    /// </summary>
    public override void OnStopClient()
    {
        SceneManager.LoadScene(_titleScene);
        Debug.Log("Disconnect");
        base.OnStopClient();
    }
}

NetworkManager

文字通りネットワークにまつわるいろいろを担います。

The Network Manager is a component for managing the networking aspects of a multiplayer game.

引用:Network Manager

これもあまりそのまま使う想定のものではないので、
継承してメソッドをオーバーライドしてカスタムします。

コールバック含め、大量に機能があるので今回使ったものだけ解説します。


StartHost, StartClient, StopHost

接続にまつわる関数です。
StartHostを実行した場合、サーバーとクライアントの両方の役割を持つことになります。

StartClientは引数に指定したアドレスのサーバーにクライアントとして接続します。

StopHostは自身がサーバーならサーバーの接続を中断し、
クライアントならサーバーから抜けます。

NetworkManagerはシングルトンとなっており、
インスタンスをどこからでも呼び出せます。

StartHost, StartClient, StopHostは全てPublicな関数なので、
これらもどこからでも呼び出せるってことです。

今回はサーバーの検索を担う、CustomNetworkDiscoveryで接続にまつわる関数を呼び出しています。

そうすることで、
・LAN内にサーバーが見つかったら→StartClient
・LAN内にサーバーが見つからなかったら→StartHost

のように同一LAN内で自動でマッチングする仕組みを作れます。


OnStopClient

クライアントがサーバーから切断された場合に各クライアントで呼び出されます。

このコールバックの中でシーン遷移を呼び出すことで
切断→シーン遷移 という処理が可能となります。

すなわち、接続状態にあるクライアントでStopHostを呼び出せば
下記処理が呼ばれるということです。

 /// <summary>
 /// サーバーとの接続が切れた時にクライアント側で呼ばれる
 /// </summary>
 public override void OnStopClient()
 {
     SceneManager.LoadScene(_titleScene);
     Debug.Log("Disconnect");
     base.OnStopClient();
 }

OnServerAddPlayer

Mirrorにはプレイヤーという概念があります。
誤解を恐れずに簡単にまとめると
サーバーに接続したクライアントのことをプレイヤーと呼び、接続時にサーバーに追加されます。

このOnServerAddPlayerはプレイヤーが追加された際に呼び出される処理です。

デモにおける接続された人数の表記の変更の通知(プレイヤー増加時)はOnServerAddPlayerで行っています。

    /// <summary>
    /// プレイヤー入室時にサーバー側が実行
    /// </summary>
    /// <param name="conn">接続されたプレイヤーのコネクション</param>
    public override void OnServerAddPlayer(NetworkConnection conn)
    {
        Debug.Log("Add Player");

        //タイトルシーンでのみ実行
        if (_titleScene.Contains(SceneManager.GetActiveScene().name))
        {
            //接続中の人数表記を変える
            SendPlayerCountData sendData = new SendPlayerCountData() {PlayerCount = NetworkServer.connections.Count};
            NetworkServer.SendToAll(sendData);
        }

        //メインシーンでのみ実行
        if (_mainScene.Contains(SceneManager.GetActiveScene().name))
        {
            Debug.Log("Spawn Player");
            //プレイヤー生成
            GameObject player = Instantiate(playerPrefab);
            //今立ち上げているサーバーにプレイヤーを追加登録
            NetworkServer.AddPlayerForConnection(conn, player);
        }
    }

また、プレイヤーを概念ではなく、実体として生成する場合もあるかと思います。

その場合、OnServerAddPlayerでInstantiateしてあげれば
各クライアントにプレイヤーが生成されます。

ただし、この機能を利用するには
InspectorのPlayerPrefabNetworkIdentityが付与されたPrefabを
事前に登録しておく必要があります。

PlayerPrefabSS.PNG

最後に

詳しくは知りませんがUNETという機能?がひと昔前にあったそうで、
それを改良したのがMirrorのようです。

結構なビッグタイトルに採用されているようですが、
ドキュメント以外の情報がなかなか無いので苦労しました。

私の今の力では及びませんがサーバー側の実装とかもいずれできるようになりたいです。

(UniTaskの実装は見よう見真似でやったので間違ってたら教えてください。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラッピーチャレンジ 2020に作ったもの

クラッピーチャレンジ Advent Calendar 2020

https://adventar.org/calendars/5187
4日目の記事です。

だいたい #クラッピーチャレンジ したことはFacebookにあげてるだろうということでたどってみます。
まずは2020年じゃないけど去年のクラッピーチャレンジ Advent Calendar 以降ということで
2019/12/28
image.png
田中さんにレーザーカッター借りたので試しに…。

2/2
image.png
こんな感じに見える車を買った。

次はいきなり真打!
image.png
バイバイワールドの高橋さんからクラッピーのグループに
「クラッピーの骨格が余ってる。ほしい人いる?」
って投稿にいち早く反応し、コロナ第1波の最中買ったばかりの車の初仕事になりました。

ちなみにこの骨格はいろいろ100均の材料を中心に最終的にはこんな感じになりました。
image.png
この辺りはまた別の記事で…。

4/9
image.png
これは口も動くようにした ビッグクラッピーナノ? ちゃんです。

6/21 
image.png
あっ、これ クラッピーウクレレ を作ろうとして切り出したところで止まってる…。

8/8
image.png
このちょっと前ぐらいから CLUSTER のワールド作りにはまってて
エッシャーのだまし絵のワールドを作って中の人を全部クラッピーにw

8/10
image.png
突然始まった。クラッピー飯ブームw

8/11
image.png

8/12
image.png

8/13
image.png

8/14
image.png

8/15
image.png

8/16
image.png
あれ?だんだんクラッピーから離れてきているような…。

8/17
image.png

8/23
image.png
CLUSTER のワールド作りのついでにUnity ProBuilderにもはまって、
とりあえずビッグクラッピーのローポリ3Dモデルを作ってみた。

8/25
image.png
そして、祝!!! クラッピー万博の開催決定!!!

9/13
image.png
あれ?これまたクラッピーとちと違うか???

9/15
image.png
広島のHMCN主催のレッドハッカソンで広島市民球場作ったら思わず優勝してしまい、
そのネタ発表にあわせて作ったクラッピーっぽいアバター

9/17
image.png
これもw

9/18
image.png
はやりに乗って作ってみた。

image.png
これも CLUSTER のアバター

9/19
image.png
M5Stackハッカソンで作ったつんつんの金髪が生えたクラッピー!!!
image.png
このクラッピー金髪にM5Atom指すとUnityの中のデジタルツインがちゃんと同じ動きするんです!!!

11/24
image.png
またまたCLUSTERワールドです。ライブエイドのWembley Stadium会場をカラオケのハッカソンで作ったのでそのあと無理やりでかいクラッピーもステージ脇に配置ww

あれ?ここまでかな?

もうちょいあったような…。
見つけたら適宜足しますwww

おしまい!

いやぁ、これで今年の#クラッピーチャレンジ Advent Calendar に書くネタがもうなくなってしまったか???

追記)100円ロボット部に投稿してたのがあった!
3/15
image.png
クラッピーの手で歩く毛玉取りロボット!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Cloud Artifact RegistryでUnity Package Managerのプライベートレジストリを作成する

「Google Cloud + Gaming Advent Calendar 2020」の4日目を担当させていただくことになりました、株式会社グレンジの村田です。

Artifact Registry

Google CloudのArtifact Registryは、11月17日(現地時間)に一般提供を開始した新しいサービスです。Google Container Registryの進化形でもあり、コンテナイメージだけでなく言語パッケージなども管理できます。

Artifact Registryはnpmをサポートしてます。UnityのPackage Managerはnpmをベースにして動作しており、社内でUnity Package Managerのプライベートレジストリのニーズがあったため、Artifact Registryのnpmが利用できないか試しました。結果、設定方法を少し注意すれば使えることがわかったので、本記事ではUnityから利用するための手順を説明しています。

※執筆時点ではnpmがアルファ版のため、アンケートに答えて申請しないと使えません。申請してから約5日間ほどで使えるようになりました。

npmリポジトリの作成(npmレジストリ)

まずはnpmのリポジトリを作成します。

スクリーンショット 2020-11-29 23.52.16.png

リポジトリの名前は「advent-calendar」とします。

スクリーンショット 2020-11-28 16.44.22.png

リポジトリ「advent-calendar」が作成されました。

スクリーンショット 2020-11-30 0.13.43.png

advent-calendarをクリックして、リポジトリの中身を確認してみます。作成直後なのでパッケージは1つもありません。とりあえずリポジトリは作成完了です。

スクリーンショット 2020-11-28 16.49.03.png

リポジトリにログインする

リポジトリにパッケージを追加すること、またUnityでそのパッケージを取得するには、リポジトリにログインする必要があります。以下が一連の手順です。

  1. gcloudコマンドの初期化でGoogleアカウント認証
  2. .npmrcにリポジトリ設定を追加
  3. npxコマンドでnpmリポジトリにログイン(npm publishでパッケージの追加ができるようになる)
  4. Unityの設定ファイルにトークンをセット(Unityからパッケージを取得できるようになる)

Artifact Registryのコンソールの「設定の手順」にnpxコマンドまでの手順は書いてありますが、「npmの構成」にある「_password」だと、Unityの設定ファイルに記載するトークンが取れないので、少し変更を加えて.npmrcに設定する必要があります(後述)。

gcloud の初期化

「Google Cloud SDK のインストール」に従い、gcloud initを実行してください。
https://cloud.google.com/sdk/docs/install?hl=JA#top_of_page

続いて、次のコマンドでも認証しておきます。

gcloud auth application-default login

.npmrcにリポジトリ設定を追加

.npmrcはどのディレクトリで作成しても構いません。本記事ではホームディレクトリとします。すでに.npmrcが存在していれば、追記します。

先述の「npmの構成」を参考にしつつ、次の3行のような構成にしてください。

@adventcalendar:registry=https://asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/
//asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/:_authToken=""
//asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/:always-auth=true

「_password」は「_authToken」に変更しており、「username」と「email」の行は削除しています。「@SCOPE」は「@adventcalendar」にしました。
「asia-northeast1-npm」や「YOUR-PROJECT-NAME」は指定したリージョンやお使いのGCPプロジェクト名で変わります。

※この3行の形式はコンソールに表示される「npmの構成」とは別に、次のドキュメントに記載がありました
https://cloud.google.com/artifact-registry/docs/nodejs/quickstart#config

npxコマンドでリポジトリにログインする

ホームディレクトリに.npmrcがある場合、次のコマンドを実行します。

npx google-artifactregistry-auth ~/.npmrc

これでnpm publishでパッケージ登録ができる状態になります。

その後、.npmrcファイルを確認すると、_authTokenに値がセットされているはずです(YOUR_TOKENの部分)。この値をメモしておいてください。

@adventcalendar:registry=https://asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/
//asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/:_authToken="YOUR_TOKEN"
//asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/:always-auth=true

※この「authToken」が必要なため、先ほどの手順で「_password」から「authToken」に変更しました。

Unityの設定ファイル.upmconfig.tomlを作成

ホームディレクトリにUnityの設定ファイル「.upmconfig.toml」を作成します。設定例です。

upmconfig.toml
[npmAuth."https://asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/"]
token = "YOUR_TOKEN"
email = "not.valid@email.com"

npmAuthの後ろは.npmrcのregistryのURLです。YOUR_TOKENは先ほどメモした値をセットしてください。emailはnot.valid@email.comで良いです。これでUnityからもリポジトリにアクセスできるようになりました。

※.upmconfig.tomlについて
https://docs.unity3d.com/Manual/upm-config.html
https://docs.unity3d.com/Manual/upm-config-scoped.html

リポジトリにパッケージを追加する

パッケージとして登録するUnityプロジェクトに、package.jsonを作成します。作成例。

package.json
{
  "name": "jp.grenge.sample",
  "displayName": "Advent Calendar Sample",
  "version": "1.0.0",
  "unity": "2020.1",
  "description": "AdventCalendar記事用のサンプルパッケージです。",
  "author": {
    "name": "Grenge"
  }
}

ファイル構成例。

├── Runtime
│   ├── Prototype.Sample.asmdef
│   ├── Prototype.Sample.asmdef.meta
│   ├── Sample.cs
│   └── Sample.cs.meta
├── Runtime.meta
├── package.json

npm publishでリポジトリにパッケージを追加します。--registryオプションで明示的にリポジトリURLを指定します。

npm publish --registry https://asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/

ハマった点ですが、パッケージ名を「@adventcalendar/sample」とすれば、npm publishのみで--registryオプション無しでも追加可能です。しかし、Unityでは「@adventcalendar」で始まるパッケージ名が、指定のリポジトリURLに変更するという動作が機能してくれませんでした。

今回はパッケージ名を「jp.grenge.sample」として、Unityにて「jp.grenge」で始まるパッケージ名が指定のリポジトリURLにアクセスされることを確認しています(後述)。パッケージ名がスラッシュ区切りか(@adventcalendar/sample)、ピリオド区切りか(jp.grenge.sample)でUnityの処理が異なっていそうです。

Unityからパッケージを取得する

Packages/manifest.jsonの「dependencies」に「jp.grenge.sample」を追加し、「scopedRegistries」にリポジトリの設定を追加します。「scopes」に「jp.grenge」を指定することにより、パッケージ名「jp.grenge」で始まるパッケージは指定したリポジトリURLから取得するようになります。

Packages/manifest.json
{
  "dependencies": {
    "com.unity.collab-proxy": "1.3.9",
   (省略)
    "jp.grenge.sample": "1.0.0"
  },
  "scopedRegistries": [
    {
      "name": "advent-calendar",
      "url": "https://asia-northeast1-npm.pkg.dev/YOUR-PROJECT-NAME/advent-calendar/",
      "scopes": [
        "jp.grenge"
      ]
    }
  ]
}

UnityのPackage Managerで確認。無事取得できました(Unity 2020.1.13f)

スクリーンショット 2020-11-29 21.47.35.png

リポジトリにアクセスするユーザーの管理

IAMでリポジトリにアクセスするユーザーを管理できます。任意のGoogleアカウントを、ロールを選択して追加するだけです。

スクリーンショット 2020-11-30 0.35.28.png

おわりに

verdaccioを使い、グローバルIPでアクセス制限を設けてプライベートレジストリを構築することを検討していたのですが、Artifact Registryの登場により選択肢が増えました。IAMでユーザーの権限管理できるのも良いです。
しばらくはArtifact Registryで運用して、使い勝手を把握しようと思います。

※参考にさせて頂いた記事
https://monry.hatenablog.com/entry/2020/03/07/015041

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ラージシーンの最適化とロード戦略

今回の主な話題:ラージシーンの最適化とロード戦略、Physics2D.ConvertCollision2DForScript最適化、Shaderパフォーマンスコストの検出、NGUIの物理的な衝突検出をオフする。


制作

Q1: ラージシーン(SECTR + Terrain)に関して、いくつか質問があります。

1)SECTRとTerrainを使用してラージシーン問題を解決するモバイルゲーム(PUBGなど)は市場にありますか?その効果はどうですか?または、何かラージシーンに適切な他のプラグインありませんか?

2)Terrainの Draw Callに何か最適化提案はありますか?Terrain自体の木と草のコストはちょっと高いと気付いました。さらに、水平方向視角のときにより多いシーン内の内容が見えますが、何かレンダリングの負担を軽減するための一般的な戦略はありませんか?

3)SECTRの枠組みの下では、マップをブロックに分割するのは簡単ですが、表面の階層化(地面、建築、装飾など)の問題をどのように解決するのですか?ここでのレイヤーは、SECTRにあるSectorの概念と合いません、別次元みたいです。

4)ラージシーンでどちらの方法はLOD問題を解決するために適していますか?SECTRは一つ簡単なLOD案を提供しましたが、Boundに従ってコンポーネント(特にRenderer)のみの表示および非表示をコントロールします。SECTRはGC方面で注意すべき点はありませんか?

5)パフォーマンスから、AssetBundleを使ってサブシナリオを頻繁にロードおよびアンロードします(これにはSECTR_Chunkの変更が必要です)。どうすれば重い状況を減らせますか?

最初に声明します。ブログでSECTRプラグインを推奨しましたが、実際にプロジェクトに使うことはありませんでした。このプラグインへの理解は「プレイしてみました」レベルでしかなく、以下の内容に経験に基づいて推測するものは多いで、ただの参照にしてください。

1)現時点に、モバイルゲームでSECTR + Terrainを使用しているプロジェクトはあまりないようです。とにかく実際にそうしているプロジェクトは私には知ってありません。その一つ、SECTRがモバイル端末に対して特定の最適化を行っていない可能性があることです。シームレスな世界を構築するほとんどのプロジェクトでは、プラグインを直接使用する代わりに、プラグインを参照して自分で一セットを実現するかもしれません。その二、個人的な経験から言うと、Terrainのコストはモバイル端末で比較的に高いであり、特にUnityのような自分で変更し難い商用エンジンの場合、後期に最適化のプレッシャーが大きくなる可能性があり、ソースコードのない小規模なチームにとっては調整できるのは設定のみです。他のプラグインに関して、私が了解したのはワールドストリーマーです。原理はSECTRと少し違いで、これも参照のみにしてください。これらのプラグインはPCまたはホストプラットフォーム用に実現されていると思います。モバイル端末でも使用できるになりましたが、モバイル端末用にあまり最適化されていない可能性があります。

2)Terrainの使用に対して、前に私たちのプロジェクトが設立されたときには携帯端末でTerrainを使用することはまだ推奨していませんでしたが、Pubg類ゲームの普及やハードウェアパフォーマンスの向上によって、たくさんのプロジェクトはもう大量にTerrainを使っていますだろう。Guidelineの最適化はこれらの経験豊富な友達に任せて答えましょう。前の考えは、Meshに変更し、そして面を減らすことでありました。植物はSpeed Treeで生成できますが、これのコストは、携帯端末でより高くなります。特にアーティストがコントロールしない場合。GPU Instanceなどの技術を使用してDrawCallを最適化することを試せますが、この分野での経験はあまりないため、これ以上は説明しません。

水平方向の自由視角はプログラムにとって大事な挑戦です。もちろん、アーティストの作業も非常に多いです。一般的な方法は皆さんもう知っていると思います。より全般的なLOD、ビューイングコーン遠平面の裁断、低性能の効率を確保するために高性能と分けることなど。(LOD方面にもたくさんの巧妙な方法があり、ある状況で遠いところのオブジェクトLODは面を減らすやブランキングためだけではありません。時には遠くの景色の不足を補うために木などのカバーを作ります、近づくとこれは逆に消えます。)

3)マルチレイヤーの概念は、上記のWorld Streamerを参照できます。これは、距離に基づいてブランキングを行います。layer/tagで地面、大きなオブジェクト、詳しいオブジェクトなどを区別でき、異なるオブジェクトのブランキング距離は異なります。私の印象では、SECTRにも距離ベースのLoaderがあります。満足できない場合は、自分で改造する必要があるかもしれません。

4)LODは非常に重要なテクノロジーです。うまくいけば、レンダリングだけでなく、実行効率も大幅に向上させられます。うまくいかない場合は、逆最適化することもよく見られます。UnityはLODのためにデフォルトでLodGroupなどのコンポーネントを提供します。原理は君が言うSECTRの方法と同じです。単純な距離と比べて、バウンディングボックスが視野内のレンダリング比に基づいてLODを作るのはより科学的な方法です。ただ、実際な使用中に、距離ほど直感的ではなく、設定やすくない場合があります。LODをうまく実現した場合は、ラージシーンに対して、Streamと組み合わせることができれば、メモリコストやロードおよびアンロードにより適用であります。例えば、遠距離シーンに低LODモデルのみロードすることもできます。一方で、結果の良いLODはアーティストの繊細な調整または設置する必要があり、この中に多くの作業があります。

LODに対して、技術的な方面はそれほど深くではないかもしれませんが、逆に幅の部分を最適化する時に様々の面が考えすべきです。例えば、Render、Shadow、Animation、ParticleSystem、Material、PostEffectなど、そして高、中、低性能と掛けて一つのマトリックスを生成し、このマトリックスに対してカスタマイズおよび最適化させます。これは、LOD効果が優れているほど作業量が多いことも意味します。

5)GC方面の欠点、使うことないのでわかりません、回答は使用経験のある友達に任せ、本当に使う前に実際テストをすることを問題主にお勧めします。ただし、GCの領域では、Profile可能なものは基本的に自分で最適化できます。改善できない場合、良い方法もないため、個人的には特に重要ではありません。 (個人的な経験では、ほとんどのプラグインにはGCでの最適化余地があります。特にこのようなより多い要件にサポートしたいもの、プラグインレベルには少し最適化しにくいものがあります。)

6)重い状況に対して、AssetBundleのロードに基づく私たちの主な最適化方法は、LRUのようなキャッシュを実行することです。メモリの多い設備は多くやりますが、少ない方は少なくやります。プリロードはないため、予測も難しいです。ここで考えすべきのはchunkのセグメンテーションを細かくするか粗くするかです。それぞれに長所と短所があり、プロジェクトの実際状況次第で選択する必要があります。

たくさん話しましたが、一般的な方法ばかりで、多くの内容は問題主も知っていましたかもしれません。全体から言うと、問題主はSECTRによく知っていますから、プロジェクトの要件と組み合わせて改善および再実現することは大きな問題ではないはずです。原理から言うと、ラージシーンの動的ロードは非常に簡単です(Unityは自分で詳細なシーン管理を行わない場合に基づく)が、実際操作には多くの問題にあう可能性があります。3Dフリーパースペクティブの技術的な挑戦はまだ大きいですから、問題主に応援します。

今までにUWAが最適化した超大地形(8kx8k)のモバイルゲームから見ると、基本的にSECTR + Terrainソリューションを使用するものはありませんが、シームレスなスプライシングのためにすべてMeshに変更します。 SECTRプラグインを使用したことがないため、その分割メカニズムと構成を理解していません。 しかし、Terrainについては、一般的に開発チームは代わりにメッシュを使用する傾向があります。

Terrainの利点は、編集が非常に便利なことです。一部のプラグイン(Terrain Composerなど)を介して基本的な地形などをすばやく生成できますが、そのDraw Callを制御するのは簡単ではなく、少なくとも直感的ではありません。メモリも同じ複雑さのMeshよりも大きくなります。TerrainDataのロード効率も高くありません。したがって、初期段階でTerrainを使用して地形を編集したチームは、後期によくこれをMeshにエクスポートし、動的にブロックしてロードします。これは現在推奨しているソリューションです。

シーンのロードでは、AssetBundleの内容は多いではなく、サイズも大きくないなら、今のLoadFromFile+LZ4を介してロードする方法はもう十分早いです。現在の重い状況は通常、AssetBundleのロードではなく、AB.Loadとインスタンス化に発生します。これに対して、有効的な方法は、プリロード、キャッシング、および自分で作るストリーミングロードを行うことによって問題を解決します。前の二つは、他の人はもう詳しく説明しましたから、ここで話しません。ストリーミングロードとは、各フレームでロードおよびインスタンス化されるアセットの数を制御することです。これには、具体的な内容や状況次第で、具体的に分析する必要があり、統一できる規則はありません。

それ以外、あと何点説明します(これからは話題に離れる話)。

(1)地形を細かく切りすぎないでください。そうしないと、Cullingとそれに続くエンジンシーンの計画(CreateSharedRenderScene)のコストが増加します。

(2)超大地形ゲーム(Pubg、Sandboxなど)で、UIモジュールのコストは他のゲーム(MMO、ARPG、カードなど)よりも大幅に低く、これは他のモジュールにより多くの計算スペースを提供できます。

(3)物理モジュールの時間コストが大幅に増加しています。車などを追加すると、うっかり物理コストが多く増加します。これは開発チームがラージシーンを開発する時によくある新しい問題です。


物理

Q2: Physics2D.ConvertCollision2DForScriptが引き起こしたGCはどうやって最適化できますか?
1.png
設置は下記のようです。
2.png
3.png

OnCollisionEnter2Dをコールバックする時、ProfilerにPhysics2D.ConvertCollision2DForScriptのGCが見られます。コールバックする必要がない場合は、スクリプトからこの関数を削除してください。 OnCollisionEnter2Dを使用する場合、このコールバックによって生成されるGCは避けられないようです。公式フォーラムにもこれに関するいくつかの議論があります:https://forum.unity.com/threads/physics-contacts-gc-activity.369625

もちろん、問題主は自分の衝突検出モジュールを作成することもできます。こちらにPhysics2D.GetContactsインターフェースを使用できます。https://docs.unity3d.com/ScriptReference/Physics2D.GetContacts.html


UGUI

Q3: OnBecameVisibleのコールバックをUGUIで使用できますか?

UGUIのレンダリング規則から見ると、Canvasを単位にして大きなMeshを合併します(異なるDrawCallは異なるSubmeshに対応します)、レンダリングする時もSubmeshに従ってDrawCallを提出します。だから、CanvasRenderコンポーネントは実際に本当のレンダリング単位を表していません。理論的には、バウンディングボックスの概念はなく、OnBecameVisibleは動かないはずです。

まず、UIコントロールの4つの極限値ポイントのワールド座標を計算します。そして、次の方法でCameraのビューイングコーンにあるかどうかを判断できます。

bool IsVisible(Camera camera, Vector3[] worldPositions)
{
    Matrix4x4 vp = camera.cullingMatrix;
    foreach (var wp in worldPositions)
    {
        Vector4 v = wp;
        v.w = 1;
        Vector4 p = vp * v;
        if (p.w > p.x && -p.w < p.x && p.w > p.y && -p.w < p.y && p.w > p.z && -p.w < p.z )
        {
            return true;
        }
    }
    return false;
}

Shader

Q4: Shaderを作成した後、そのコストを正確にテストするにはどうすればよいですか?

Shaderの消費統計に関しては、一つ方法はツールで詳細な分析を行うことです。例えば、2つのShaderのコストを水平比較しやすいために、明確な指令数を出して比較します。もう一つの考えは、実機で実際の効率の影響によってテストし、本当のシーンに対して最適化効果を検証することです。 簡単な方法は、テストしたい部分にLODを区別し、Debugボタンを配置してLODを切り替え、直接に設備でフレームレートの変化とCamera.Render関数コストの変化を統合します。ただし、実機テストでは、GPU Boundではない場合にはフレームレートの変化を確認できないため、ボトルネックでなければ最適化の優先度を下げることができます。もちろん、バッテリー消費などの方面でまだ向上できます。


物理

Q5: 私のNGUI時間コストは非常に高いです。チェックしたら、NGUIはデフォルトで何か衝突検出機能がつけてあることがわかりました。これはどちらで設定できますか?感謝。

1)Start方法にあるUIPanelはUICameraのタイプに応じてリジッドボディコンポーネントを自動的に追加するかどうかを判断します。自動追加を回避するように試みることができます。
2)UnityメニューのEdit/Project Settings/Physicsに衝突検出を実行するレイヤーを設定できます。
4.png


UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析最適化ソリューション及びコンサルティングサービスを提供している会社でございます。

UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity 2018バージョンでテクスチャが導くアプリの強制終了

今回の主な話題:Unity 2018バージョンでテクスチャが導くアプリの強制終了、ゲーム実行中にMipmapを開くとアセットへの影響、UIでアニメーション効果を遷移する方法、サウンドの再生に多くのMonoメモリが消費されます。


アセット管理

Q1: Unity 2018.1.4f1でTerrainを使って地形を作りましたが、テクスチャを使用している限り、一枚しかない場合でも、Huawei P9で実行するとCrashされます。Terrainにテクスチャを削除したらCrashしなくなりました。原因はわかりません。地形テクスチャのサイズは512x 512です。

設置は以下となります。
1 .png
以下は私のAndroidStudioクラッシュスタックです。
2.png

テストしたところ、確かにcrashが発生しました。TerrainとTexture設置を確認しても、何か特別な状況はありません。しかし、圧縮フォーマットをASTCからETC2に変更しようとすると、次の図に示すように、crashはもう発生しなくなりました。したがって、ASTCテクスチャをロードした後の解凍過程に問題があると判断します。問題主にETC2形式で圧縮することをお勧めします。
3.png


アセット管理

Q2:UWA公式サイトのDemoから、プロジェクト「キワメ」はMipmapを使っていないことがわかりました。実行中にMipmapをオンにするかどうかが何か影響ありませんか?
4-1.png

Lightmapテクスチャの機能は他のカラーテクスチャと同じで、サンプリングされた情報はライトとシャドウとしてシェーダーで計算されるため、Mipmapをオンにするかどうかは他のテクスチャと同じように処理できます。たとえば、2.5D視点で、視野範囲が比較的近いゲームでは、メモリ占用を減少するためにMipmapを使わなくても大丈夫ですが、視野範囲の遠い状況では、Mipmapを使わないとフラッシュ現象があり、GPUサンプリングのコストも比較的に多くなります。
ですから、オンにするかどうかは、問題主のゲームタイプ次第で決めます。テクスチャとMipmapの役割を理解していれば、問題主は具体的な情報に基づいて自分で決定できます。

Mipmapに関しては、Lightmapは他のテクスチャマップと同じです。Mipmapをオンにすると、メモリが3分の1増加しますが、ビデオメモリの帯域幅を減少できます。
一般的に、Lightmapを使うのはシーンオブジェクト、基本的にはMipmapをオンにすることをお勧めします。90度(に近い)の俯角であれば、Mipmapを開く必要はありません。


制作

Q3: ある一枚の黒い画像を他の画像の図案によって、この部分の透明を切り抜く方法はありませんか?つまり、この黒い背景にこの丸い部分を切り抜いて、空き部分は透明であり、丸の移動によってこの部分も移動します。これはどうやって実現できますか?

逆にMask、二つ方法あります。

一、Shaderを変更し、UI-Default.shaderをコピーして、次のコード:

Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

を次のように

Stencil
        {
            Ref [_Stencil]
            Comp NotEqual
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

変更して背景のShaderを取り替えます。

二、Imageから類別を派生させ、GetModifiedMaterialその方法をOverrideします。CompareFunction.NotEqualをCompareFunction.NotEqualに変更します。


制作

Q4: UIを切り替える時に、画面全体がゆっくりと黒く(周辺から中央に)併呑され、そしてゆっくりと透明になって、切り替えられたUIを顕示するようにしたいですか、どうやって実現できますか?

UIで実現:黒い画像を描きます中央から周辺までalphaは徐々に増加し、9つの正方形のSpriteを作成します。移動する時に、UIの最上層に画面サイズを超える画像を追加してサイズを徐々に小さくします。最初は透明部分が画面全体を覆い、最後は黒い部分が画面全体を覆います。
5.png
もちろん、皆さんはShaderでこの効果を実現することにお気に入りようです。実現も非常に簡単です。簡単に言うと、一つの丸を掘ることであります。UVと(0.5,0.5)の間の距離を計算します。この距離がRより大きい場合は黒、それ以外の場合はtex2dがサンプリングした色を返します。Rはパラメータとして渡さればいいです(さらに、エッジ遷移を追加することも難しくない)。ただし、そうせれば結果は楕円であり、円が必要な場合は、テクセルサイズを計算し、それに_MainTex_TexelSizeを掛ける必要があります。


Monoメモリ

Q5: UWAプラグインを使って、サウンドの再生がMonoメモリはいっぱい消費していることは観察されたが、どうすればよいですか?
6.jpg

スタックから見ると、Audio Play時にたくさんのLogが作成され、Monoメモリの割り当てを増加しました。ErrorまたはWarning Logがあるかどうかを確認することを問題主にお勧めします。


UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析最適化ソリューション及びコンサルティングサービスを提供している会社でございます。

UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UniRxをミニゲームを作りながら学ぶ

はじめに

本記事はLife is Tech!Advent Calendar 6日目の記事です!
ぜひ最後まで目を通していってください〜よろしくお願いします!><

さて、タイトルにもある通り今回はUnityの便利ライブラリ、UniRxについての入門解説記事を書いていこうと思います。

ですが、僕自身もUniRxを初めて知った頃は、

  • 何から学べば良いのか分からない。
  • ネットの解説記事やコードを見ていても何が書いてあるかが分からない。

などなど何度も挫折しました。
ただやはりそんな中でも、無理やりでも書いていくと何となく分かるようになったので、今回は

  • Unityでゲームを少しでも作ったことがある人
  • UniRx学んでみたい・書いてみたいけど何すれば良いか分からない人

などの人でも読み進めていけて、書き方が何となくでも身につくことを目指す記事にしたいなと思います。
(事前にLINQなどの知識があるとより学びやすいです。)

ということで今回は簡単なミニゲームを作りながらUniRxを使用しない記法・使用した記法を並べ、主にUniRxでの書き方を身につけていきましょう。
細かい部分まで詳しく説明していくとキリがないので、本記事で詳しく触れていないよく分からない部分に関しては、自分でも随時調べながら進めていくと尚良しです!

UniRxは使えるようになるととても便利だと思うので、初心者や学びたての方でも、後々のために存在だけでも知っておいて欲しいなと思います。
それではいってみましょう。

今回作るゲーム

アドベントカレンダーっぽくサンタを登場させたかったので、
サンタがプレゼントを集めたらクリアのシンプルなゲームにしましょう。

last.gif

サンタがプレゼントを集める……非常に謎ですがそこは触れないでください。

実装

0. プロジェクトの準備

0-1. プロジェクトのダウンロード

もし今回の記事を自分でも書きながら進めたい人がいましたら、下のリンクからまずプロジェクトをダウンロードしてください。
サンタやステージのアセット(無料)は事前に入れてある & 今回触れないコードは少しですがすでに事前に書いてあります。
Unityのバージョンは 2019.1.14f を使用しています!

プロジェクトURL : https://github.com/kaku710/unirx_learn_project

0-2. Mainシーンを開く

Scenes/Mainを開いて、画像のようになっていたら大丈夫です!
スクリーンショット 2020-12-04 19.32.50.png

ここから実際に機能をつけていきます!

1. プレイヤーが動く処理

それではまずはサンタを動かしていきましょう。
Create→C# Scriptから、SantaController.csを作ります。

UniRx無しでシンプルに実装すると以下の感じでしょうか。

実装例 (UniRx無し)

SantaController.cs
using UnityEngine;

public class SantaController : MonoBehaviour
{
    float speed = 5;

    void Update()
    {
        Vector3 v = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        Move(v);
    }

    void Move(Vector3 v)
    {
        transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime);
    }
}

これをUniRxを用いて実装すると以下のように実装することができます。

実装例 (UniRx使用)

SantaController.cs
using UnityEngine;
using UniRx; // UniRx使用時は忘れずに
using UniRx.Triggers; // UpdateAsObservable使用に必要

public class SantaController : MonoBehaviour
{
    float speed = 5;

    void Start()
    {
        this.UpdateAsObservable()
            .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
            .Subscribe(v => Move(v));
    }

    void Move(Vector3 v)
    {
        transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime);
    }
}

軽く解説

ここやっていることとしては
1. UnityのUpdate関数をStart内でUpdateAsObservable()を利用してストリーム (一連の処理の流れのようなもの) というものに変換
( ストリームについて詳しく知りたい人はこちら )
2. Select()を利用して入力の値を取得
3. SubScribe()で関数を登録する

というような流れです。

この実装だけでは何でストリームに変換する必要があるの?など、メリットがパッとしない人も多いと思いますが、これをしておくことで例えば今後

  • bool型のisGameOverがfalseの時だけ動かしたい

などの時には、

this.UpdateAsObservable()
    .Where(_ => !isGameOver) // この1行でMove関数を呼ぶ条件をフィルタリングできる
    .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
    .Subscribe(v => Move(v));

Where()を利用してこんな感じで簡単に記述できたり、更に条件としてよくあるような

  • 入力がない時はMove関数を実行したくない

などの時には、

this.UpdateAsObservable()
    .Where(_ => !isGameOver) 
    .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
    .Where(v => v.magnitude > 0f) // この1行でさらに入力があるかどうかをフィルタリングする
    .Subscribe(v => Move(v));

もう1度Where()を利用してこのようにフィルタリングすることで実装することができます。
Where()if()のような役割ですね。

ちなみにこのようなフィルタリングはUniRxにはとても便利なものが多く用意されています。ちなみに後でもいくつか出てくる。先に色々見たい方はこちら↓
逆引き記事 : UniRx オペレータ逆引き メッセージのフィルタ

UpdateAsObservable()を使用するメリット

個人的に感じる、UpdateAsObservable()を使用するメリットとしては、

  • Update内のネストが深くなるのを防げる
  • 複雑になればなるほど可読性が上がる

こんなところです。
メリットなどについてもっと詳しく知りたい!という方は、こちらの記事にもより詳しく書いてあるので、興味のある方はぜひ読んでみてください。

ここら辺はよく分からない方というも、今はとりあえずこんな感じで書き換える方法もあるんだ!くらいに思ってもらえればOKです。どんどん使ってけば慣れていくはず!

(コードが書けたらサンタにアタッチするのを忘れずに)
santa_walk.gif
ゲームを再生するとサンタが無事動いてくれました!(進行方向に向くコードは事前にSantaRotator.csに書いてあります。)
それでは次いきましょう。

2. プレゼントに当たった時の処理

サンタがプレゼントに当たったら、プレゼントを削除 → カウント用の変数を増やす、の流れでいきましょう。

Prefabsフォルダ配下に"Present"のプレハブがあるので、3つ程ステージ上に適当に配置してください。
また、タグを事前に"Present"に設定しています。

そしたら先ほど作成したSantaController.csにコードを追加していきます。

まずUniRx無しだと以下のような感じでしょうか。(追記分のみ記載)

実装例 (UniRx無し)

SantaController.cs
public class SantaController : MonoBehaviour
{
    int presentCount = 0; // プレゼントを数える変数

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Present"))
        {
            Destroy(other.gameObject);
            presentCount++;
        }
    }
}

これをUniRxを用いて以下のように実装します。

実装例 (UniRx使用)

public class SantaController : MonoBehaviour
{
    int presentCount = 0; // プレゼントを数える変数

    void Start()
    {
        this.OnTriggerEnterAsObservable()
            .Where(o => o.CompareTag("Present"))
            .Subscribe(o =>
            {
                Destroy(o.gameObject);
                presentCount++;
            });
    }
}

軽く解説

こちらもStart内でOnTriggerEnter関数OnTriggerEnterAsObservable()を利用してストリームに変換しています。
このようにUniRx.Triggersを使うとUnityが用意しているコールバックをストリームにし、全てをAwake/Start内にまとめて記述することが可能になります。

ちなみにUnityで用意されているコールバックイベントはほぼ全て用意されているそう。すごい。
Wiki : https://github.com/neuecc/UniRx/wiki/UniRx.Triggers

補足 : ストリームを終了したい時

例えばこの衝突関係のストリームをゲームの途中で停止したい時があったとします。
そんな時は以下のように実装することで終了することができます。

IDisposable disposable;

void Start()
{
    // ストリームをIDisposableに格納
    disposable = this.OnTriggerEnterAsObservable()
                     .Where(o => o.CompareTag("Present"))
                     .Subscribe(o =>
                     {
                         Destroy(o.gameObject);
                         presentCount++;
                     });
}

void StopSubscribeStream()
{
    disposable.Dispose(); // 停止したいタイミングでこのようにDispose()を実行
}

このような感じでいつでも停止することもできます。めっちゃ便利。補足でした。

それではゲームがきちんと動くか確かめましょう!
collect_present.gif
このようにサンタが当たったらプレゼントが消えていればOKです!

続いては、ここで用意した変数 presentCount を用いてゲームクリア条件をつけていきましょう。

3. ゲームクリア条件をつける

サンタがプレゼントを一定数(今回は3つ)集めたらクリアにしたいのですが、
UniRx無しで愚直に書くとこんな感じでしょうか。Clearシーンは事前に用意しています。

実装例 (UniRx無し)

SantaController.cs
public class SantaController : MonoBehaviour
{
    const int CLEAR_PRESENT_COUNT = 3;

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Present"))
        {
            Destroy(other.gameObject);
            presentCount++;
            // ここから追記
            if(presentCount >= CLEAR_PRESENT_COUNT)
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Clear");
            }
        }
    }
}

ReactiveProperty

上記のクリア条件をUniRxを用いて実装したいのですが、UniRxを用いると、

変数の監視→値の変更を検知してActionを実行

といったようなことが簡単に行えるようになります。例えば、HPを監視して変更を検知したらHPバーに反映させたりとか。
それを実現してくれるのがUniRxのReactivePropertyです。
これは本当に便利なので、使えるようになるとよりレベルアップできると思います。

とりあえず手を動かして動くことを確認してみましょう。

まず先ほどint型で宣言したpresentCountReactiveProperty<int>型で宣言しなおします。

ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);

またそれに伴って、presentCount++の部分がエラーになると思うので、以下のように書き換えてあげます。

presentCount++; → presentCount.Value++;

これで変数が監視できるようになったので、Start内で値の変更を監視するようにします!以下が実装例です。

実装例 (UniRx使用)

SantaController.cs
public class SantaController : MonoBehaviour
{
    const int CLEAR_PRESENT_COUNT = 3;
    ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);

    void Start()
    {
        presentCount
            .Where(x => x >= CLEAR_PRESENT_COUNT)
            .Subscribe(_ =>
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Clear");
            });
    }
}

こんな感じで実装することができます!とても便利ですね。

ReactivePropertyのメリット

これも色々あるかとは思いますが、個人的には

  • MVPパターンに非常に有用なこと

が1番のメリットかなと思います。(あくまで個人的意見です)
MVPパターンとはデザインパターンの1つで、この設計はゲームの設計 (だけじゃ無く他のプロダクトでも) をしていく上でとても便利なのでぜひ知っておくと良いと思います。
この記事内では長くなるので解説しませんが、もしUnityでのMVPパターンについて詳しく知りたい方は以下の神記事を読むと良いです。(マジで僕もめちゃくちゃ読んだ)

MVPパターンに関して今はあまり分からなくても、ReactivePropertyに関しては非常に便利なので、少しずつでも慣れていくと良いと思います。

それではゲームを確かめてみましょう!
to_clear.gif
このようにプレゼントを3つ集めたらClearシーンに遷移すればOKです!

4. リトライ機能をつける

最後はボタンを押した時のイベントについて。ゲーム的にもせっかくなのでClearシーンからMainシーンに戻れるようにしましょう。
ボタンは事前に置いていますが、クリックした時のイベントの設定はしていませんのでそれをUniRxを用いて行ってみましょう。

UniRx無しの実装は
- publicで関数を定義してUGUIで紐付け
- onClick.AddListner()を使用
このあたりかなと。こちらの実装例は省略します。

適当にc#ファイルを新規で作成して、

実装例 (UniRx使用)

ClearPresenter.cs
using UniRx;
using UnityEngine;

public class ClearPresenter : MonoBehaviour
{
    public UnityEngine.UI.Button retryButton; // Inspectorで設定

    void Start()
    {
        retryButton.OnClickAsObservable()
            .Subscribe(_ =>
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Main");
            });
    }
}

こんな感じで書いてあげれば実装できます。

UniRxを用いてボタンのイベントを実装すると色々と便利で、例えば、1回押したら1秒は入力を受け付けないボタン、なども

button.OnClickAsObservable()
            .ThrottleFirst(System.TimeSpan.FromSeconds(1)) // 1秒間入力を受け付けない
            .Subscribe(_ =>
            {
               // 押した時の処理
            });

こんな感じで1行追加してあげるだけで実装することができます!UniRx使わないとなると結構めんどくさそうなので、とても便利ですね。他にも色々あるので気になる人は調べてみてください。

では最後にゲームが動くかの確認をしましょう!
last.gif
このようにボタンを押してMainシーンに戻ることができていたらOKです!

最後に念のため、実装した最低限のSantaController.csの全文も載せておきます。

SantaController.cs
using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class SantaController : MonoBehaviour
{
    float speed = 5;
    const int CLEAR_PRESENT_COUNT = 3;
    ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);

    void Start()
    {
        this.UpdateAsObservable()
            .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
            .Subscribe(v => Move(v));

        this.OnTriggerEnterAsObservable()
            .Where(o => o.CompareTag("Present"))
            .Subscribe(o =>
            {
                Destroy(o.gameObject);
                presentCount.Value++;
            });

        presentCount
            .Where(x => x >= CLEAR_PRESENT_COUNT)
            .Subscribe(_ =>
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Clear");
            });
    }

    void Move(Vector3 v)
    {
        transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime);
    }
}

さいごに

UniRxについて初めて記事を書いてみましたが、いかがだったでしょうか。
今回紹介したUniRxの機能は本当にほんの一部で、まだまだ便利な機能がいっぱいあります!自分もまだまだ勉強中です。

もっとUniRxについて詳しく知りたい・学びたい方は、

などなど読んでみると良いと思います。ちなみに本記事の参考もここです。(toRisouP様様ですね。)

ぜひ今後のゲーム開発に少しでも取り入れるきっかけになれば幸いです。

ではでは明日以降のLife is Tech!Advent Calendarもお楽しみに!ありがとうございました〜!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# .net、Unityアプリケーション間でMemoryMappedFileを用いてデータを共有する

やりたいこと

弊社の業務では、なんらかのセンサーを用いて情報を読み取り、その状況によって演出を行うといったことがよくあります。
演出をするアプリケーションはUnityで作成するとして、センサーを読み取る部分をどのように実装するかが問題になります。

ネイティブプラグインもしくはライブラリとして実装する手もありますが、センサーの調整や制御が少し面倒になるので、
- アプリケーションをC#.NETのWindowsフォームアプリケーションで作成
- 演出アプリケーションをUnityで作成
というように、2つのアプリケーションで分けて行うことにしました。

次のような恩恵がありました。
- センサー機器の担当者と演出の担当者の作業を分けることが出来る
- センサーの種類が変わっても対応できる

やりかた

MemoryMappedFileを用いて、メモリ内でファイルのようなものを扱い、アプリ間でデータのやりとりを行います。
MSDNのMemoryMappedFileクラスドキュメント
構造体を直接バイナリファイルに書き込んでポインタで参照したり、BinaryFormatterで書き込む手もあると思います。それだとパフォーマンスは間違いなく出ると思いますが怖いので……
今回は安全にxmlにシリアライズしたデータをやりとりします。

仕組みは単純で、センサーアプリ側ではデータを1つのクラスにまとめてxmlの形式にシリアライズし、メモリマップトファイルに書き込みます。演出アプリ側ではそれを読み取り、デシリアライズして元のクラスのインスタンスを復元することが出来ます。
ただし注意点として、2つのアプリでメモリを同時に読み書きしようとすると何らかの不具合が出るのは必至ですので、排他制御を確実に行います。

共通(データ定義)

以下が受け渡しを行うデータの例です。ここではSensorDataクラスがシリアライズするクラスです。
SerializableAttribute属性を付与していますが、実はXmlSerializerではSerializableAttribute属性は不要でした。
念のため残しております。

CensorData.cs
[Serializable]
struct SensorPoint {
    public float x;
    public float y;
}
[Serializable]
class SensorData {
    public float time;
    public List<SensorPoint> points;
}

書き込み側

Censor.cs
class hogeClass{
    private Mutex mutex; // 排他制御に使用
    MemoryMappedFile mmfile = null;
    // Formの作成時などに呼ばれる
    public void Initialize(){ 
        string mutexName = "SensorAppMutex";
        bool createdNew = false;
        mutex = new Mutex(false, mutexName, out createdNew);
    }
    // アプリ終了時に呼ばれる
    public void Finalize(){
        mmfile?.Dispose();
        mutex?.Dispose();
    }

    // 共有するデータを保存したいタイミングで呼ぶ
    public void SaveSensorData(const SensorData data){
        // シリアライザーの作成
        System.Xml.Serialization.XmlSerializer serializer =
            new System.Xml.Serialization.XmlSerializer(typeof(SensorData));

        bool getMutex = false;
        try {
            if (getMutex = mutex.WaitOne(5000)) {
                mmfile?.Dispose();
                mmfile = MemoryMappedFile.CreateNew("SensorAppData", 1024 * 1024 * 1);
                using (MemoryMappedViewStream stream = mmfile.CreateViewStream())
                {
                    serializer.Serialize(stream, data);
                }
            }
        }
        finally{
            if (getMutex) mutex.ReleaseMutex();
        }
    }
}

読み込み側

Unityアプリ側の処理です。
センサーアプリから渡された座標に、オブジェクトを生成しています。

Spawner.cs
// センサーアプリから渡された座標に、オブジェクトを生成する
public class Spawner : MonoBehaviour
{
    Mutex mutex; // 排他制御
    // 下記のSampleObjectコンポーネントを取り付けたゲームオブジェクトのプレハブ
    [SerializeField] GameObject sampleObject; 

    void Start() {
        string mutexName = "SensorAppMutex";
        bool createdNew = false;
        mutex = new Mutex(false, mutexName, out createdNew);
    }
    void OnDestroy() {
        mutex?.Dispose();
    }

    void Update() {
        System.Xml.Serialization.XmlSerializer serializer =
            new System.Xml.Serialization.XmlSerializer(typeof(SensorData));
        // センサーから取得した結果
        SensorData data = null;
        bool getMutex = false;
        try {
            if (getMutex = mutex.WaitOne(3000)) {
                using (MemoryMappedFile mmfile = MemoryMappedFile.OpenExisting("SensorAppData"))
                using (MemoryMappedViewStream stream = mmfile.CreateViewStream()) {
                    result = (SensorData)serializer.Deserialize(stream);
                }
            }
            catch (Exception ex) {
                // なにかまずいことが起こった場合、ログに残す
                Debug.LogError($"exception type: {ex.GetType()} msg: {ex.Message}"); 
                throw ex;
            }
            finally {
                if (getMutex) mutex.ReleaseMutex();
            }
        }

        // データが取得できたので、その位置にオブジェクトを生成する
        if (data != null) {
            foreach (var point in data.points) {
                Vector3 pos = new Vector3(point.x, point.y, 0);
                GameObject obj = Instantiate<GameObject>(samplePrefab, pos, Quaternion.identity, this.transform);
            }
        }
    }
}
SampleObject.cs
// 生成されて一定時間で消えるゲームオブジェクトの例
public class SampleObject : MonoBehaviour
{
    const float lifeTimeMax = 0.5f;
    float lifeTime = lifeTimeMax;

    void Update()
    {
        lifeTime -= Time.deltaTime;
        if (lifeTime < 0)
        {
            Destroy(this.gameObject);
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パニシング:グレイレイヴンのUI設定が好き

はじめに

みなさん、「パニシング:グレイレイヴン」はもうプレイしましたでしょうか?

私は崩壊3rd推しだったので、もちろん注目していました。

皆さんも是非プレイしましょう!!!!

公式サイトはこちらです!
https://grayraven.jp/ (『パニシング:グレイレイヴン』公式サイト)

image.png

満を持して今日の正午にサービスが開始したこのゲームですが、設定の中のある項目に驚きました。

それがこちらです↓
ご覧ください。

iOS の画像.gif

はーーー天才!

と、言うわけで、コレを作ってみましょう。

作り方

分からないけど、どうせUnityを使ってるのでUnityで作成します。

シーン

まず、シーンを用意します。空で良いです。
image.png

何もありません。
ここに、Canvasを生成しましょう。
image.png

そしてその中にImageを投入します。
アンカーは縦横ともにストレッチにしておきます。
image.png

コレが背景になりました。
image.png

そうしたらまずは適当に右側についてくるモノを生成しましょう。
こんな感じです。
image.png

ヒエラルキーはこんな感じ。

LeftSideとRightSideに項目を分け、その二つのアンカーも縦横をストレッチにします。
image.png

image.png

そしてその項目のアンカーを下記のように設定します。
image.png
image.png

後はスクリプトを書くだけです。

スクリプト

UIPositionController.cs
using UnityEngine;
using UnityEngine.UI;

public class UIPositionController : MonoBehaviour
{
    public RectTransform leftTransform;
    public RectTransform rightTransform;
    public Slider slider;


    private void Update()
    {
        // スライダーの値を取得する。(0~1)
        float value = slider.value;

        // スライダーの値を元に位置を動かす。
        float move = 10;
        leftTransform.offsetMin = new Vector2(value * move, 0);
        rightTransform.offsetMax = new Vector2(value * move * -1, 0);
    }
}

適当なObjectにつけて(canvasに付けた)、インスペクターにしっかりと設定する。

image.png

そして実行すると見事成功です!!

iOS の画像_1.gif

端のボタンが押しづらい!!手が短くて届かない!!などの理由がある人にはありがたい設定ですね!

おしまい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ml-agentsのstepとframeの関係がよく分からなかったので調べてみた。

結論

  • Update => 可変
  • OnActionReceived => 50/s (Fixed Timestep依存) = step
  • CollectObservations, Heuristic => 10/s = Desicion Period * step

ML-Agentsのセンサーに、加速度と重力方向を渡そうと思ったら、自分がフレームとステップの関係をよく理解できていなかったことが原因でうまく行かなかったのでまとめてみました。

Update関数の呼ばれる頻度

可変。Updateは画面のリフレッシュレートに応じて、1秒あたりに呼ばれる回数が決まっていない。これはUnityの基本。

OnActionReceivedの呼ばれる頻度

固定。画面のリフレッシュレートに関係なく、1秒当たり固定回呼ばれ、これ1回が1stepに相当する。
FixedUpdate関数の呼ばれる頻度で処理しているようで、プロジェクトの設定からFixed Timestepを変更すると変わってしまうため注意が必要。規定だと1秒に50回実行。

CollectObservations, Heuristicの呼ばれる頻度

コンポーネント「Decision Requester」で設定している「Decision Period」の数だけstepが進むと実行される。
既定値の5が設定されている場合、Fixed Timestepが規定値の「0.02」であれば、5/50で1秒当たり10回呼び出される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity 2019.4〜】4K超の360動画を実用的なパフォーマンスで再生する(8Kまで対応)

概要

Unityで360度動画を再生するとき,4Kの解像度を境界に動画コンテナに工夫が必要でした。

360度動画をUnity上で再生するための操作は,公式ドキュメントに手順が掲載されていますが,Unityにインポートするまでの工夫や,そうならざるを得ない背景事情を取り扱います。

環境

ソフトウェア

  • Windows 10
  • Unity 2019.4.x
  • SteamVR
  • HTC Vive

ハードウェア

  • Intel Core i7 10700
  • NVIDIA RTX 2070 Super
  • DDR4-2133 32GB

カメラ

  • Insta360 Pro 2

背景

一般的にUnityに動画をインポートする際には,H.264 (typically in a .mp4, .m4v, or .mov format)を選択するが一般的です。

しかし,H.264形式は4K30p(4,096×2,048)までが標準仕様で,それを超える解像度(例. H.264形式の8K動画)をUnityで扱う際には,iGPU(CPU内蔵グラフィックス)やdGPU(グラフィックボード)のデコード支援が利用できませんでした。
その結果,デコード処理がCPUで行われるため,動画の再生がカクカクになります。

H.264の次世代規格であるH.265は,8K120p(8192×4320)まで標準仕様です。UnityでもUnity 2019.1からH.265をサポートしています。

Unityにインポートするまでの準備

  1. H.265形式で動画をエンコード
  2. HEVC ビデオ拡張機能の導入(120円)

1. H.265形式で動画をエンコード

お好みのエンコーダーで,動画をH.265形式でエンコードします。筆者の環境では以下の設定で運用しています。H.265形式のCPUエンコードは非常に時間がかかるため,Intel QSVやNVIDIA NVENCやAMD VCEによるハードウェアエンコードをおすすめします。
エンコードについては,私はいつもこちらの方のブログを参考にしています。

形式 2D, 3D プロファイル 生成動画のビットレート(参考程度)
8K30p(7680x3840) 単眼視 Main@L6.1@High 約150Mbps
8K30p(7680x7680) 両眼立体視 Main@L6.2@High 約300Mbps

2. HEVC ビデオ拡張機能の導入

現在VRヘッドマウントディスプレイを扱うには,Windows一択のため,Windowsで動かしている方が大半だと思います。

Unityは,Unity 2019.1からH.265をサポートしましたが,Windows上でのUnity
では,動画のインポートと再生にHEVC ビデオ拡張機能を導入する必要があります。

これは,UnityがMicrosoft Media Foundationを利用しているためです。

この拡張機能は実質有償化しましたが,OSレベルでH.265デコード機能が扱えるようになり,UnityでH.265が扱えるほか,エクスプローラのサムネイルでの表示やプレビューに対応するようになります。120円です。将来的にOS標準サポートになるといいですね。

こぼれ話:カメラRAWやAV1の拡張機能は無償提供されています。

Unityにインポートしてからの手順

公式ドキュメントに手順が掲載されています。2Dも3Dもどんとこい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでの経路探索とAIに何を使うか

はじめに

Unityで経路探索やAIを使うにあたって、
どんなものがあるのか?
どんな違いがあるのか?
どうやって使うのか?
を以下に絞って比較し、それぞれの簡単な使用方法を紹介します。

  • 経路探索
    • Unity標準のNavMesh
    • A* Pathfinding Project
  • AI
    • StateMachine(ImtStateMachine)
    • BehaviourTree(FluidBehaviorTree)

どんなものがあるのか

経路探索

主に見かけるのは以下の2つです。

  • Unity標準のNavMesh
    • Unityの標準機能なのでそのまま使えます。
    • キャラが通れる経路をメッシュ状に表現したものです。
  • A* Pathfinding Project
    • NavMeshの他にも、GridGraph や PointGraph での経路探索もできるアセットです。
    • Pro版とFree版があります。

AI

簡易なAIの手法として2つピックアップします。
(オープンワールドやMMOなどのスケールの大きいゲームでの複雑なAIでは、これらを組み合わせたり、別の手法にしたりといったアプローチが必要になると思われます。)

  • StateMachine
    • ステート(状態)に応じて行動させて、ステートを切り替えていくことで様々な行動を表現します。
      StateMachine簡易図.png
  • BehaviourTree
    • 状況に応じた行動をTree状に表現して、Tree全体を都度走査して行動させます。
      BehaviourTree簡易図.png

どんな違いがあるのか

経路探索の比較

項目 Unity標準のNavMesh A* Pathfinding Project
導入 マップとなるオブジェクトをStatic(もしくはNavigation-Static)に設定。 アセットのインポート。
Pathfinderコンポーネント追加。
Editorでの経路生成 Window → AI → Navigation から設定してBake。 空オブジェクトにPathfinderコンポーネントを追加して、NavMesh/Grid/PointどれかのGraphを登録してScan。
動的な経路生成 NavMeshSurface.BuildNavMesh()
(オブジェクトをStaticにする必要はない)
別途GitHubからコードを拾ってくる必要がある。
AstarPath.Scan()
事前にPathfinderコンポーネントに空Graphの追加が必要。
移動経路のパス取得 NavMeshAgent に計算させるか、パスのみ計算する。 Seeker に計算させる。
Modifier コンポーネントを追加しておくとパスが自動補正される。
移動 取得したパスを辿るか、NavMeshAgentで移動。
NavMeshAgentでspeed類を0にして自動移動させないようにした場合でも、updatePositionをtrueにしておくとNavMesh範囲から外れないように移動させられる。
取得したパスを辿る。
障害物 NavMeshObstacleを配置するとNavMeshが更新される。 Collider.boundsを使用してGraphUpdateObjectを生成・反映すると更新される。
NavMesh用のNavmeshCut機能はPro版のみ。
経路設定のセーブ・ロード BakeすることでNavMeshDataファイルを生成。
NavMesh.AddNavMeshData()でロード。
NavMesh.RemoveNavMeshData()で破棄。
Pathfinderコンポーネントからセーブしてファイル生成。
AstarPath.data.DeserializeGraphs()でロード。
AstarPath.data.RemoveGraph()で破棄。

AIの比較

項目 StateMachine BehaviourTree
全体把握 ステートが増えると全体像を把握しにくくなる。
特に遷移関係がややこしくなる。
全体像を把握しやすい。
実行時の状態把握 現在の状態をContextに保持しておくなどして、CurrentStateTypeといった単一の情報で把握できる。 現在の状態を把握しづらい場合がある。
(各種状況が組み合ってTreeを走査するので、Stateなどの単一の情報で表現しづらい)
GUIでTree状態を見られれば全体像と同じく把握は容易。
実装 ステートの追加にクラス自体の追加が必要となり、単純なステートだった場合は BehaviourTree と比較してコード記述が多くなる。 行動の追加に必要なコード量が、StateMachine と比較して少ない。
ただし、書き方によってはスパゲティになりやすい側面もある。
追加・修正 行動の追加・修正時に全体像を把握せずに、関連するステートのみ把握していれば十分な場合がある。 行動の追加・修正時に全体像を把握しないといけない場合がある。
(内部まで詳細に把握する必要は無いが、関連しない行動であっても、Tree上の優先度やどの状況での行動なのかの把握が必要になる場合がある)

アセットの比較

AIに後述するアセット(ImtStateMachine, FluidBehaviorTree)を使用した場合における比較です。
(AIの比較と重複する部分もあります。)

項目 ImtStateMachine FluidBehaviorTree
実装方法 ステートごとにクラスを実装。 単一コードでツリーを実装。
Treeを部分的にパーツとして作成し、別のTreeに追加することも可能。
行動の切り替え 各ステートごとの遷移可否関係を設定しておく必要あり。
ステートを増やしたらその都度遷移関係の記述も必要。
毎フレームTree全体を走査して、都度現状に対応した行動を処理させる。
実行 現在のステートのクラスのみが実行される。 毎フレームTree全体を走査するため、不要なチェックが発生する場合がある。
Wait や Continue で Tree全体の走査ではなく行動を継続させることも可能。
全体把握 GUI表示機能なし。
ステート遷移を矢印で表現するとしたら、ステートが増えるのに応じて矢印も増えるため全体の把握が難しくなる。
遷移可否の関係性は ImtStateMachine.AddTransition() をまとめて記載することで把握しやすくはできる。
ただし、ステートクラス側で遷移ガードができるため把握しづらくなる場合がある。
実行中にTreeをGUI表示可能。
(実行中のみ表示可能で、編集は不可。)
Treeを辿ることで、どの状況ならどの行動をするかを把握できる。
コード記述自体がTree状になるためGUIでなくても俯瞰での把握は難しくない。
実行時の状態把握 CurrentStateType などを用意して現在のステートを把握できるようにすれば、エディタでも本番環境でもログ等ですぐ判別できる。
単一の状態として表現するため、今どの行動をしているのかは把握しやすい。
エディタでの確認はTreeのGUI表示を見ることで可能。
実機環境用の確認機能はない。
実装するとしても、単一の状態としての表現ではなく全体の状況に応じて行動を選択するため、複数情報が必要。
キャッチアップ 行動をクラスごとに切り替える作りであることが分かれば、理解は容易。
遷移関係の把握はステートが増えるごとにややこしくなるが、行動の実装・修正は単一ステートや関連ステートの一部のみの把握で済む場合もある。
Sequence, Selector の組み合わせ方や優先順位の考慮が必要。
状況に応じて都度行動を変化させるため、全体像を把握しておかないと意図しない行動をさせてしまう場合がある。

どうやって使うのか

Unity標準のNavMesh

Window → AI → Navigation を開きます。

Objectタブを選択した状態で、Hierarchyからマップとなる床や壁のオブジェクトを選択すると、対象オブジェクトの設定を変更できます。
Navigation Static にチェックして、Navigation Area を床であれば Walkable, 壁であれば Not Walkable にします。

すべてのマップ用オブジェクトを設定したら、Bakeタブの中にあるBakeボタンを押してNavMeshを生成します。
SceneビューでBakeした結果を確認できます。

実行中にNavMeshを生成・更新するには、NavMeshComponentsという追加機能群が必要です。
https://github.com/Unity-Technologies/NavMeshComponents
NavMeshSurfaceコンポーネントを使用することで、動的生成が可能になります。

キャラクターなどの移動させたいオブジェクトに、NavMeshAgentコンポーネントを追加します。
SetDestination() で移動先座標を設定すると、自動で移動し始めます。

navMeshAgent.SetDestination(targetPosition);

または、移動経路となるパスのみを取得することもできます。

var navMeshPath = new NavMeshPath();
NavMesh.CalculatePath(myPosition, targetPosition, -1, navMeshPath);
// navMeshPath.corners に移動経路が入っています

A* Pathfinding Project

Free版があるので、ダウンロードしてみて採用するかどうか検討できます。
https://arongranberg.com/astar/download
(Pro版の機能が不要であればFree版を採用することもできます。)

空オブジェクトを生成して、 Component → Pathfinding → Pathfinder でコンポーネントを追加します。

InspectorでAdd Mesh Graphから設定したいタイプを選択して追加し、Scanボタンで経路を生成します。
なお、NavMeshを使用する場合、Meshデータが必要となります。(アセット内にサンプルが入っています。)
GridGraphの場合は、NodeSize, Width, Depth等の数値設定のみで生成できます。
SceneビューでScanした結果を確認できます。

実行中にNavMeshやGridGraphを生成・更新するには、Inspectorで設定済みのNavGraphを取得して、設定を更新するなどしてからScanします。

var graph = AstarPath.active.graphs[0] as NavMeshGraph;
graph.sourceMesh = mesh;

AstarPath.active.Scan();

キャラクターなどの移動させたいオブジェクトに、Seekerコンポーネントを追加して、移動経路を取得できます。

astarSeeker.StartPath(myPosition, targetPosition, path =>
{
    if (!path.error)
    {
        // path.vectorPath に移動経路が入っています
    }
});

ImtStateMachine (StateMachine)

IceMilkTeaというフレームワークの一部として公開されている、ImtStateMachineを使用する例です。
https://github.com/Sinoa/IceMilkTea/blob/develop/Packages/IceMilkTea/Runtime/Core/UnitCode/PureCsharp/StateMachine.cs

参照用のメンバを用意しておきます。

public class StateMachineAi : MonoBehaviour
{
    // 各ステートクラスでの参照用
    Character character;

    // ステート遷移用ID
    enum TransitionEventId
    {
        Move,
        Attack,
    }
}

ステートごとの挙動を記述したクラスを作成します。

// 移動ステート
class MoveState : ImtStateMachine<StateMachineAi, TransitionEventId>.State
{
    protected override void Update()
    {
        Context.character.Move();

        // ターゲットに近づいたら攻撃ステートに遷移
        if (Context.character.IsNearTarget())
        {
            Context.stateMachine.SendEvent(TransitionEventId.Attack);
        }
    }
}
// 攻撃ステート
class AttackState : ImtStateMachine<StateMachineAi, TransitionEventId>.State
{
    protected override void Enter()
    {
        // ステート遷移時に攻撃開始
        Context.character.Attack();
    }

    protected override void Update()
    {
        // 攻撃が終わったら移動ステートに遷移
        if (!Context.character.IsAttacking())
        {
            Context.stateMachine.SendEvent(TransitionEventId.Move);
        }
    }
}

Enter(), Update() 以外にも、終了処理・特定時の遷移ガードといった機能もあります。

初期化として、StateMachineを生成して、ステートの遷移条件を設定します。

void Start()
{
    stateMachine = new ImtStateMachine<CasualEnemyStateMachineAi, TransitionEventId>(this);
    // 遷移条件の設定
    stateMachine.AddTransition<MoveState, AttackState>(TransitionEventId.Attack);
    stateMachine.AddTransition<AttackState, MoveState>(TransitionEventId.Move);
    stateMachine.SetStartState<MoveState>();
}

後は、ImtStateMachine.Update()を呼ぶことで、現在のステートのクラスのUpdateが動作します。

void Update()
{
    stateMachine.Update();
}

FluidBehaviorTree (BehaviourTree)

GitHubで公開されているFluidBehaviorTreeを使用する例です。
https://github.com/ashblue/fluid-behavior-tree
コードからTreeを作成し、Editorでの実行中にTreeの状態をGUIで確認することができます。
(蛇足ですが、Behavior / Bihaviour のuが入るかどうかはアメリカ式かイギリス式かの違いで、意味は同じです。)

Treeを生成します。

void Start()
{
    tree = new BehaviorTreeBuilder(gameObject)
        .Selector()
            .Sequence("攻撃")
                .Condition(("攻撃していないか") => !character.IsAttaking())
                .Condition(("ターゲットとの距離判定") => character.IsNearTarget())
                .Do(() =>
                {
                    character.Attack();
                    return TaskStatus.Success;
                })
            .End()
            .Sequence("移動")
                .Condition(("攻撃していないか") => !character.IsAttaking())
                .Do(() =>
                {
                    character.Move();
                    return TaskStatus.Success;
                })
            .End()
        .End()
        .Build();
}

Selector(), Sequence()は子をすべて処理するけれど途中の成否によって処理を終了する仕組みで、
Condition()は条件判定、Do()は内包する処理を実行する仕組みです。
他にも、処理の待機/継続・成否の反転・Treeのパーツ化といった機能もあります。

後は、BehaviorTree.Tick()により、Treeを走査します。
MonoBehaviour.Update()で呼べば、毎フレーム走査することになります。

void Update()
{
    tree.Tick();
}

終わりに

結局の所どれを使えば良いのかは、ゲームの規模やジャンルによっても変わります。
いずれも一長一短あるため、GridGraphが使いたい、複雑なAIは不要、などの要望・状況に応じて選択できると良いかと思います。
Unityで経路探索・AIを実装したいけれどとりあえずどうしたらいいのか、といった方へのとっかかりとなることができれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity と Vue3 と GitHub Pages と GitHub Actions のお話

初めに

こんにちは、Bugfire です。
クラウドワークス Advent Calendar 2020 の12日目になりました。

タイトルはてんこ盛りですが、内容は薄いです!

結果だけ知りたい人に

なぜこんなふわっとした物に

当初 Project Tiny (Unity のインスタントアプリ用のフレームワーク) で小さくなった Unity をいっちょ試してみるか!と思っていたのですが、自分が試した時点ではサンプルすらビルドできないので諦めました。

すごい勢いで変化しているので、古いもの試してもしょうがないですし(言い訳)

というわけで、使い古されたネタの Unity WebGL ビルド + Vue.js を試してみます。Vue3 の Composition API を使ったことがないので、それもついでに試しつつ、GitHub Pages w/ GitHub Acitons でリリースをします。

Unity

環境

Unity のバージョンは 2019.4.15f1 LTS を使いました。弱気ですね。
安定バージョンだけあって、WebGL のビルドはすんなりいきます。

プロジェクトファイル

中身作るの大変なので、CubeWorld をベースに作業を行なってみます。
というか、すごいですねこれ。MineCraft クローンですが、よくできています。

CubeWorld ScreenShot

UnityLoader.js の変更

このバージョンでは、mac OS Big Sur (Version 11)の UserAgent に対応していないので、少し手を加えましょう。

UnityLoader.js は難読化が行われているので、扱いくいです。読みやすく変更しましょう。
自分は js-beautify を使いました。

npx js-beautify -s 2 < original/UnityLoader.js > new/UnityLoader.js
@@ -1996,7 +1996,7 @@
     var p = n;
     switch (/Windows/.test(u) && (p = /Windows (.*)/.exec(u)[1], u = "Windows"), u) {
       case "Mac OS X":
-        p = /Mac OS X (10[\.\_\d]+)/.exec(i)[1];
+        p = /Mac OS X (1\d[\.\_\d]+)/.exec(i)[1];
         break;
       case "Android":
         p = /Android ([\.\_\d]+)/.exec(i)[1];

こんなしょうもない差分です。

もう一つあります、スマートフォンの場合は、

  compatibilityCheck: function(e, t, r) {
    UnityLoader.SystemInfo.hasWebGL ? UnityLoader.SystemInfo.mobile ? e.popup("Please note that Unity WebGL is not currently supported on mobiles. Press OK if you wish to continue anyway.", [{
      text: "OK",
      callback: t
    }]) : ["Edge", "Firefox", "Chrome", "Safari"].indexOf(UnityLoader.SystemInfo.browser) == -1 ? e.popup("Please note that your browser is not currently supported for this Unity WebGL content. Press OK if you wish to continue anyway.", [{
      text: "OK",
      callback: t
    }]) : t() : e.popup("Your browser does not support WebGL", [{
      text: "OK",
      callback: r
    }])
  },

ここでダイアログが表示されてしまうので、コードから削除を行うか

UnityLoader.SystemInfo.mobile = false;

を行うことで無理矢理チェックを回避します。

ビルド環境

私は UnityCloudBuild (以下UCB) に課金していないので、手動でビルドして、生成物をレポジトリに Commit していますが、通常であれば、UCB でビルドを行うのが良いでしょう。

その場合は C# で Build PostProcess で Upload なりすると良いと思います。

Vue.js

環境構築

$ npm init -y
$ npm install -L -S -D @vue/cli
$ npx vue create app
  • ❯ Manually select features で細かく指定
  • Choose Vue version, TypeScript, Router, Linter をアリに
  • ❯ 3.x (Preview)
  • ? Use class-style component syntax? (y/N)NO

あとは適当に

vue-unity-webgl のような便利コンポーネントもありますが、ここは自分でコンポーネントを作ります。

UnityLoader.js のローダー

まず最初にできなかったことから...。最初 UnityLoader.js をモジュールとして実行しようと思いましたが、global スコープを前提としているところがあり、諦めました。

export interface UnityInstance {
  SetFullscreen(mode: number): void;
  SendMessage(gameObject: string, method: string, param: string): void;
}

export interface UnityLoader {
  instantiate(
    container: string,
    configUrl: string,
    options: { onProgress: (instance: UnityInstance, progress: number) => void }
  ): UnityInstance;
}

declare let UnityLoader: UnityLoader | undefined;

export function GetUnityLoader(url: string): Promise<UnityLoader> {
  return new Promise((resolve, reject) => {
    if (typeof UnityLoader !== "undefined") {
      return resolve(UnityLoader);
    }
    const s = document.createElement("script");
    s.type = "text/javascript";
    s.src = url;
    s.async = true;
    s.defer = true;
    s.onload = () => {
      if (typeof UnityLoader !== "undefined") {
        return resolve(UnityLoader);
      } else {
        return reject(`Load error on UnityLoader (${url})`);
      }
    };
    s.onerror = () => reject(`Load error on UnityLoader (${url})`);
    document.head.appendChild(s);
  });
}

script 要素を作って無理矢理呼び出します。形として getter はありますが、グローバルに読み込まれます。

Vue コンポーネント

Composition API を使ってみました。Composition API の説明自体は各所にあるので略。

<template>
  <div :id="unityContainerId"></div>
</template>

<script lang="ts">
import { defineComponent, onMounted, reactive, PropType } from "vue";
import { GetUnityLoader, UnityInstance } from "./UnityLoaderUtil";

export default defineComponent({
  name: "Unity",
  props: {
    loaderUrl: {
      type: String,
      required: true
    },
    configUrl: {
      type: String,
      required: true
    },
    onLoad: {
      type: Function as PropType<null | ((instance: UnityInstance) => void)>,
      required: false,
      default: null
    },
    onProgressChanged: {
      type: Function as PropType<null | ((progress: number) => void)>,
      required: false,
      default: null
    }
  },
  setup(props) {
    const unityContainerId = `unityContainer-${Math.random()
      .toFixed(10)
      .toString()
      .substr(2)}`;
    const state = reactive<{ hasError: boolean }>({
      hasError: false
    });

    onMounted(async () => {
      try {
        const unityLoader = await GetUnityLoader(props.loaderUrl);
        unityLoader.instantiate(unityContainerId, props.configUrl, {
          onProgress: (instance, progress) => {
            if (props.onProgressChanged !== null) {
              props.onProgressChanged(progress);
            }
            if (progress >= 1 && props.onLoad !== null) {
              props.onLoad(instance);
            }
          }
        });
      } catch (e) {
        state.hasError = true;
      }
    });

    return { unityContainerId };
  }
});
</script>

emit で型の付け方がわからなかったので、コールバック関数を Props に渡すようにしました。自分に型は必要です。(そのあたり論争があるようですね)

Unity と Vue.js 間での通信

まったく Unity と Vue.js の間で通信をしないのであれば良いですが、普通はそうはいきませんね。

Vue.js から Unity への通信は簡単です。初期化時、もしくは Progress イベントで伝わる UnityInstance の SendMessage メソッドで、GameObject のメソッドを呼び出すことができます。文字列の引数が渡せるので、必要があれば JSON などを使うのも良いです。

逆方向はちょっとだけ面倒です。

  • JS 側は Unity 側のプロジェクトで .jslib ファイルの JS で記述された mergeInto() で export する
  • Unity 側は System.Runtime.InteropServices.DllImport で上の関数の import する。
  • Unity 公式サイトのドキュメント に詳しく書いてあります。

自分は Unity(jslib) 側の責務を最小にするため、

mergeInto(LibraryManager.library, {
  GameToWebNative: function(tag, message) {
    if (typeof window.UnityGameToWebHandler === 'function') {
      window.UnityGameToWebHandler(Pointer_stringify(tag), Pointer_stringify(message));
    }
  }
});

のように行いました。window にUnity をロードする前から変なのをはやしておくことで、実態を Vue 側の実装に委譲しています。
一般的には postMessage を用いるのが良いかと思います。

余談

当初 UI は HTML/CSS で、ゲーム内容は Unity/WebGL とし、オーバレイ表示することで、ブラウザの機能を生かして最高の体験を!!!、と作業を進めましたが、面倒すぎました。

特に Unity/WebGL のビルドは死ぬほど遅いのと、環境が分散するため、とても作業し辛いです。
業務であれば、開発中は UnityEditor 側に mock を作って、環境を Unity に閉じることはできそうです。片手間ではちょっと...。

なので、Vue らしさは全然ないです。中途半端な記事で申し訳ありません。

デプロイ

ついでに GitHub Actions から GitHub Pages にデプロイしましょう。

name: github pages

on:
  push:
    branches:
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    env:
      TARGET_PATH: CubeWorld/Web
      CACHE_VERSION: v1
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2.1.2
        with:
          node-version: 12.x
          check-latest: true
      - uses: actions/cache@v2
        with:
          path: ${{ env.TARGET_PATH }}/node_modules
          key: ${{ env.CACHE_VERSION }}-${{ runner.os }}-v12-${{ hashFiles(format('{0}/package-lock.json', env.TARGET_PATH)) }}
          restore-keys: |
            ${{ env.CACHE_VERSION }}-${{ runner.os }}-v12-
      - name: Install
        run: |
          cd ${{ env.TARGET_PATH }}
          npm install
      - name: Build
        run: |
          cd ${{ env.TARGET_PATH }}
          npm run build
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ${{ env.TARGET_PATH }}/dist

取り立てて書くことはないですね。peaceiris/actions-gh-pages は便利。他レポジトリへの deploy もできるので、公開用レポジトリと非公開レポジトリに分けるのも良いでしょう。

Unity Project での変更

ぶっちゃけ、ここが一番時間かかりました。疲れました。

Unity WebGL での問題点

マルチタッチがバグっています

マルチタッチでのコントロールがうまくいかなくて TouchScript を試していたりしましたが、そもそも Input.GetTouch() で取りこぼしがあります。(Unity 2019.4.15f1時点)
(TouchScript の issue でも取り沙汰されていますが、TouchScript の問題点ではありません)

https://github.com/Bugfire/CubeWorld/blob/develop/CubeWorld/Assets/QubeWorld/Shared/Components/WebGLInputWorkaround.cs

かなり無理矢理な対処をしたので、興味がある方はここのソースを読んでみてください。

Cube World の変更

もともとのプロジェクトは、

  • PC 用でキーボード中心の操作系
  • IMGUI ベースのメニュー

でした。そりゃ、8年前のプロジェクトですからね!

操作系の変更

  • タッチ中心の操作系に変更
  • 画面中心のみターゲットから、タッチした場所をターゲットに変更

メニューの変更

  • ある程度を uGUI ベースに変更しました。
    • アイテムウィンドウ関連がまだ残っています (組み合わせでバグっています)

追加機能

  • Web/Unity 間通信のサンプルとして、チャットボタンを作りました
    • 押下すると Web 側でテキスト入力を行い、Native 側に結果を投げ、表示します。
  • 追記
    • Photon REALTIME の無料プランを使って簡易的なチャットを実装しました。便利、かんたん。

動いていないところ

  • Network/Multiplay
    • 動きません(実装していません)

Cube World の感想

本当によくできています。
エンジン部分が Unity に非依存に作ってあるので、Unity に限らずお好きな C# ランタイムでサーバ検証ができそうです。

逆に Physics や raycast は Unity の便利な機能が使えませんが、エフェクトやキャラクタの表示のみに使用を限るのも良いというか、よくあるMMOの実装ではそうするでしょう。

おそらく高速化のために、ある程度大きなの粒度で動的に mesh を作成しています。
(聞いた話、本家もライティングの結果をランタイムで mesh にベイクしているみたいですね)

時間があればもう少し改造してみたいです。

参考

以下のサイトを使用してリソースを作成しました。ありがたや。

終わりに

しくじり一覧

  • Project Tiny 使わなかった
  • UnityLoader.js をモジュール化できなかった
  • UI を Vue.js に寄せることができなかった
  • 時間のほとんどを Unity のプロジェクト変更に費やしてしまった
  • テキスト入力を Web 側の form で行おうとしたら、iOS でフォーカスがうまく戻ってこなくて諦めた

Unity も Vue.js も GitHub Actions もみんな楽しいですね!
Unity のプロジェクトの CubeWorld を UI の世代更新や、スマートフォン対応にするのに一番時間がかかりました。久しぶりの Unity 楽しかったです。

We're hiring!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hololens2 備忘録

Hololens2 備忘録

UnityEditorとHololens2の乖離
UnityEditor上では動作するが、いざhololensにビルドしてみると想定した動作が起こらない現象がありますよね。そこで実際に困ったことを共有していきたい

VRMファイルのモデルが出力されない(確認できない)
この問題に対して、
・しっかりと配置できていない。(遠い位置に配置してしまっているなど)
・ビルドの設定?
・Solvers(ソルバー)を使っていて何かのミスでどこかに配置されていて見えない
など複数の原因が存在していると考えられる

1つ目しっかりと配置できていない
image.png
こんな簡単な事分かるわ!と思うかもしれませんが説明します

MixedRealityPlayspaceのオブジェクトをクリックするとSceneタブで、オブジェクトが認識される空間が確認できます。
この空間でカメラのポイントよりも遠かったり、X方向から見た時に上過ぎたりすれば、HoloLensでそのオブジェクトの位置を確認することは難しいです。
そして、オブジェクトのスケールは大きいよりも小さいほうが扱いやすい気がします。

2つ目はビルドの設定
image.png
これはVRMファイルのモデルが出力されない時に、調査して分かったことなのですが、
Stereo Rendering Mode がSingle Pass Instancedになっていると右目だけしか表示されないようです。なのでMulti Passに変更しましょう。
HoloLensで見えているが、動画を取るとVRMのモデルが消えてしまうという場合にもMulti Passの設定が有効です。こちらを参考にした

3つ目はSolvers(ソルバー)を利用している場合
ところでSolvers(ソルバー)
とは、簡単に説明するとMRTKで用意されている機能で、オブジェクトを特定の位置に固定させたり、距離を指定して手に追従させたりすることが出来ます。
HoloLens2の実機でHandMenuやSolverなどが動作しない場合の対処方法についてこちらの記事を参考にするとよい。
MRTK2.5にすることによってSolverの不具合を解決できる様だ
MRTK2.5をインポートする分かりやすい記事

Hololen2で撮影した動画をPCへ送る方法

これは、調査してもなぜか簡単な方法があまり出てこなかった、検索するとアプリをインストールする方法などが出てくるが他にも方法はあります。
多分簡単すぎて、載せる必要が無いのかな

PCでエクスプローラーからHoloLensにアクセスしてPictureフォルダ内にある動画をコピーしてくれば送れます
image.png
非常に簡単です(笑)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityで「シリアライズするフィールド」と「プロパティー」を簡潔に書きたくて、SourceGeneratorを作ってみた

※ この投稿で紹介するSourceGeneratorは、まだUnityじゃ使えません。将来的にこうできればいいなーって話

さてまずは、C#のプロパティーの話をします。
C#では、フィールドをpublicにするのは良くなくて、プロパティーを使ってフィールドにアクセスするのが良いとされています。

↓のコードは良くなくて

public class Launcher {
    // publicなフィールドだからこれは良くない
    public float speed;
}

↓な感じでプロパティーにするのがGoodです。

public class Launcher {
    float speed;

    // ちょっと古い書き方のプロパティー
    public float Speed {
        get { return speed; } 
        set { speed = value; }
    }
}

さて、↑みたいなコードはわざわざフィールドを作らなくても、↓のような自動実装プロパティーで実現ができます。

public class Launcher {
    public float Speed { get; set; }
}

自動実装プロパティーを使うことで、C#のコードからは見えない次のようなフィールドとアクセッサが、"自動"で生成されます。

  • <Speed>k__BackingFieldというフィールド
  • get_Speedというメソッド
  • set_Speedというメソッド

自動実装プロパティー、便利ですね!


さて次はUnityのシリアライズとフィールドとプロパティーの話。

UnityにおいてMonoBehaviourのサブクラスやScriptableObjectのサブクラスなどで、次のようなpublicなフィールドに設定した値や参照は、シーンやプレファブにシリアライズされることがあります。

using UnityEngine;

public class Launcher : MonoBehaviour {
    // publicなフィールドだからこれは良くない
    public float speed;
}

ですが先にも説明したとおり、C#として↑のようなpublicなフィールドは良くなかったですね。
こういう場合は、SerializeField属性の出番です。SerializeField属性をつけたフィールドはpublicでなくても、シリアライズされます。

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField] float speed;
}

さて、このフィールドに外部からアクセスするために、プロパティーを付けてあげましょう。(とりあえずゲッタープロパティーだけ)

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField] float speed;
    public float Speed => speed;
}

うん、よくあるパターンです。 このUnityで「シリアライズするフィールド」と「プロパティー」というよくあるパターンをもっと簡潔にしたいな!というのがこの投稿の趣旨です。


先ほど説明した自動実装プロパティ、あれは使えないのでしょうか?

自動で実装される内部的なフィールド(バッキングフィールド)に、SerializeField属性をつけることができたら、実現できそうです。なんと、これ実現できます。

C# 7.3から自動実装プロパティの自動で実装される内部的なフィールド(バッキングフィールド)に属性を付与できるようになりました。

using UnityEngine;

public class Launcher : MonoBehaviour {
    [field:SerializeField]
    public float Speed => speed;
}

うん、簡潔!めでたしめでたし!
とは、ならないいんです・・・

自動実装プロパティのフィールド名は、「<Speed>k__BackingField」という名前です。困ったことに、この名前でシリアライズされてしまいます。次の画像みたいな感じで。

スクリーンショット 2020-12-04 1.06.25.png
スクリーンショット 2020-12-04 1.06.21.png

うーん、困った。


さて、ここでC# 9.0から加わったSourceGeneratorの出番です。(まだUnityじゃ使えません。将来的にこうできればいいなーって話)

C# Source Generatorは、ビルド時にC#のソースコードを生成する仕組みです。

  • メインのプロジェクトがビルドされる前にコード生成
  • コード生成するために必要な入力値はコンパイル時に必要
  • 出力結果は、プロジェクトの一部となる
  • IDEにおいて、生成したコードの宣言にジャンプもできる
  • ILではなくC#を生成するので、デバックがすごい楽
  • 既存のソースコードを上書きしたりけしたりすることはできない

このC# Source Generatorを使うことで、プログラマティカルにコード生成をすることができます。C# Source Generatorを使うことで、ボイラープレートのコードは非常に簡単になります。

さて、自分はC# Source Generatorをつかって、「フィールドに付与すると、そのフィールドのゲッタープロパティーを生成してくれるSource Generator」を作ってみました!

ソースコードはこちら! RyotaMurohoshi/PropertyGenerator

using System;
using PropertyGenerator;

public partial class Product
{
    [GetterProperty(PropertyName = "Identifier")]
    private readonly int id;

    [GetterProperty] private readonly string name;

    public Product(string name, int id)
    {
        this.name = name;
        this.id = id;
    }
}

↑みたいな感じで、idやnameというフィールドにGetterPropertyという属性をつけると、↓のようなプロパティが生成されます。

public partial class Product
{
    public int Identifier => this.id;
    public string Name => this.name;
}

このGetterPropertyを使って、次のようなボイラープレートなコードもC# Source Generator使ってすっきりさせてみましょう。

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField] float speed;
    public float Speed => speed;
}

↑が、↓こうなります!プロパティーがいらなくなりました!すっきり!

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField, GetterProperty] float speed;
}

あんまり嬉しくない?確かに1行だとそうですね。

これが↓みたいにたくさんあったらどうでしょう?

using UnityEngine;

public class Monster : ScriptableObject {
    [SerializeField] int maxHp;
    public int MaxHp => maxHp;

    [SerializeField] int maxMp;
    public int MaxMp => maxHp;

    [SerializeField] int attack;
    public int Attack => attack;

    [SerializeField] int defense
    public int Defense => defense;

    [SerializeField] int speed
    public int Speed => speed;
}

これが↑、↓こうなる!すっきりしましたね!
ちなみに↑は、一部実装が間違っている箇所にきがつきましたか!↑はうっかりミスをしてます。↓ならこういううっかりミスも防げますね!

using UnityEngine;

public class Monster : ScriptableObject {
    [SerializeField, GetterProperty] int maxHp;
    [SerializeField, GetterProperty] int maxMp;
    [SerializeField, GetterProperty] int attack;
    [SerializeField, GetterProperty] int defense
    [SerializeField, GetterProperty] int speed
}

Unityで「シリアライズするフィールド」と「プロパティー」ですが、残念ながら自動実装プロパティはつかえません。そこで、Unityで「シリアライズするフィールド」と「プロパティー」を簡潔に書きたくて、SourceGeneratorを作ってみました。フィールドにつけるとゲッタープロパティーを生成するという非常に簡潔なものです!

よかったらコードを見てみてください! : RyotaMurohoshi/PropertyGenerator

これがUnityで使えるようになるのが楽しみです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む