e.blog

主にUnity/UE周りのことについてまとめていきます

シンプルなBehavior Treeを実装してみる

概要

今回は、AIの中でも比較的スタンダードな、「Behavior Tree」について書きたいと思います。
(内容は、いろいろな記事を拾い読みしながら、自分の解釈で実装したものになるので、多少の誤解や間違いがあるかもしれません)

実装したサンプルはGithubで公開しています。

GitHub - edom18/SimpleBehaviorTree

スクエニの以下の記事にも、

現在のゲームのキャラクターの人工知能の約7割程度がビヘイビア・ツリーで作られていると推測されます。

と記載があるので、それなりに大きな比率を占めているものだと思います。

thinkit.co.jp

ちなみに、Unityのアセットストアにも、GUIで簡単に実装が行えるアセットが売られています。


Behavior Designer

Behavior Treeとは

Behavior Treeとは、ビヘイビア、つまり振る舞いをツリー構造状に定義し、それを逐一実行してく形のものです。
AIに対して、なにかしらの判断と、一連の流れを記述した行動をとらせたい場合に重宝する手法です。

Behavior Treeを構成するのは「ノード」です。
いくつかの基本ノードを様々に組み合わせながら、AIとしての処理を作っていきます。

ノードの種類

  • ActionNode
  • Decorator
  • ConditionNode
  • Sequencer
  • Selector
  • Repeater

などがあります。

特に、ActionNodeConditionalNodeについてはリーフノードになっていて、分岐処理や、実際のアクションを実行するノードとなります。
それ以外のSequencerSelectorは、いわゆるコンポジットパターンによる、子ノードを持てるノードになっています。

名前からなんとなく分かるかと思いますが、Sequencerは一連の流れを処理し、Selectorは、子ノードのうちのどれかを選択します。

以下で少し細かく見ていきます。

ActionNode

アクションノード。一番末端にあるノードで、ゲームオブジェクトの実際の振る舞い(アニメータを起動してアクションさせたり、1フレーム分移動させたり、など具体的な行動)を記述します。

Decorator

デコレータ。アクションノードなどの結果をデコレート、つまり装飾して返します。
例えば、アクション自体は成功しているけど、強制的に失敗にする、など用途は様々です。

ConditionNode

条件分岐用のノード。
プレイヤーが近くにいあるか、HPは足りているか、など様々な分岐を行うのに利用されるノードです。
以下で説明するシーケンサの中のひとつのノードに指定することで、例えば「プレイヤーが近くにいたら攻撃する」などの分岐が可能になります。

また、条件分岐ノードは「条件が更新された」場合に、それまで実行していた処理を中断して分岐がtrueになったあとの処理を再度実行するかを評価することができます。(詳細は後述します)

Sequencer

シーケンサ。つまり一連の流れを処理するノードです。
Selectorとの大きな違いは、登録された子ノードすべてを実行し、もし途中のノードで「失敗」が返された場合は、その時点でシーケンサの処理自体を「失敗」として親に戻します。
いわゆる「AND」的な振る舞いをするのがシーケンサの役割です。

主な利用シーンとしては、プレイヤーの位置を特定し(1)、その場所まで移動(2)、そして一定距離まで近づいたら攻撃(3)というような一連の流れを実行します。
もし仮に、途中の「プレイヤーの位置まで移動」が困難になった場合、(高台に逃げられた、とか)この処理は「失敗」扱いとなり、結果として「近づいて攻撃する」という一連の流れ自体が失敗になるわけです。

Selector

セレクタ。どれかひとつを選択します。イメージ的には「OR」ですね。
そしてこれは、「どんな行動をすべきか」を選択する場合に一番用いられるノードでしょう。

前述のシーケンサと組み合わせることで、「一定の体力がある場合は、近づいて攻撃」のパターンと、「体力が少なくなったから離れて攻撃」の選択肢があった場合に、ふたつのパターン自体はシーケンサにより一連の流れとして実装し、その「どちらかを選択する」ことを、セレクタが行う、という感じになります。

Repeater

設定されたノードを「リピート」するノードです。
ルートノードの下において処理をループすることで、常に思考を繰り返すAIが作れたりします。

ノードの状態

ノードにはいくつかの状態があります。

  • 非アクティブ(Inactive)
  • 成功(Success)
  • 失敗(failure)
  • 実行中(Running)
  • 完了(Completed)

該当の処理が成功した場合は「成功」、失敗した場合は「失敗」、そして移動処理中など、まだ処理結果が分からず、処理中を表す状態が「実行中」となります。
また、まだタスクの実行が開始されていない状態を「非アクティブ」とし、非アクティブ状態で活性化された場合にOnStartなどの処理を実行します。

そして最終的に、全タスクが完了状態になった場合に、Behavior Tree全体が完了状態となるようになっています。

ノードのサイクル

各ノードはサイクルを持っていて、OnAwakeOnStartOnUpdateOnEndなどがあります。
今回実装したのはこの4つの状態です。

これを見てピンと来る方もいるかもしれませんが、いわゆるステートパターンにある状態遷移に近いイメージですね。
各ノードが活性化され、アクティブなノードが常にOnUpdateが呼ばれる形で実装しています。

冒頭のスクエニの記事でも触れられていますが、ステートパターンとは相性がよく、スクエニFF15の各キャラクターは全体の大きな状態はステートパターンで実装され、各状態ごとの細かい処理はBehavior Treeで実装されているようです。

Conditionalノードの場合は、必要に応じて再評価する

Conditionalノードについてはやや特殊で、条件分岐を行う関係上、「再評価する必要がある場合」があります。
例えば、敵が近くにきた場合は攻撃する、というAIを作るとしましょう。

当然、最初は敵は近くにいません。
そしてその分岐は「失敗」に終わります。敵が近くにいないからですね。

その後、時間が経過して、敵が近づいてきたとしましょう。
しかし、再評価する仕組みがない場合、すでにその分岐は評価が終わってしまっているため、敵が近くにいる、という分岐処理は二度と実行されません。
つまり、敵が近づいてきたにもかかわらず、「敵が近づいてきたら攻撃する」という項目が一切実行されなくなってしまいます。

AIとしてこれでは困りますね。
そのため、再評価が必要な分岐の場合は、親ノード側でそれを知る必要があるわけです。

UnityのアセットであるBehavior Designerを見てみたところ、どうやら再評価がある分岐ノードについては常にOnUpdateを呼び続け、仮に状態が変化した場合にそれを検知するような仕組みになっていました。
実装方法は様々でしょうが、(オブザーバパターンなど)とにかく、分岐ノードの再評価が必要な場合は、常にチェックするように実装する必要がある、というわけです。
(じゃないと状態変化に気づけませんからね)

直感的にも再評価してくれるほうがいいと思うので、今回自分で自作してみた実装には再評価の仕組みを入れてあります。

実装方針

今回の実装は、比較的シンプルな形で、「ちょっとした分岐と再評価、そして行動」ができるAIが作れるレベルのものを目指しました。
(あくまでビヘイビアツリーの内容把握が目的なので)

まず、全体を管理するためのクラスとしてSimpleBehaviorTreeクラスを用意します。
このクラスがマネージャとして振る舞い、全体のタスクの更新と現在の状況の把握を行います。

ノードはNodeクラスがベースクラス

各ノードに関しては、Nodeクラスをベースクラスとして、前述したノードを実装しました。
これをそれぞれツリー構造になるように親子構造を構築し、それのルートノードをSimpleBehaviorTreeの実行する最初のノードとして設定します。

using UnityEngine;

namespace BehaviorTreeSample
{
    /// <summary>
    /// Simple Behavior Treeのノードベースクラス
    /// </summary>
    public abstract class Node
    {
        protected GameObject _owner;
        public GameObject Owner
        {
            get { return _owner; }
            set { _owner = value; }
        }

        private int _index = -1;
        public int Index
        {
            get { return _index; }
            set { _index = value; }
        }

        protected Node _parentNode;
        public Node ParentNode
        {
            get { return _parentNode; }
            set { _parentNode = value; }
        }

        protected string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        // 現在のステータス
        protected BehaviorStatus _status = BehaviorStatus.Inactive;
        public BehaviorStatus Status
        {
            get { return _status; }
        }

        public Node()
        {
            _name = GetType().ToString();
        }

        /// <summary>
        /// Behavior Tree起動時に一度だけ呼ばれる
        /// </summary>
        public virtual void OnAwake()
        {
            // do nothing.
        }

        /// <summary>
        /// ノードが実行されたら呼ばれる
        /// </summary>
        public virtual void OnStart()
        {
            Debug.Log("[OnStart] " + Name);
            _status = BehaviorStatus.Running;
        }

        /// <summary>
        /// ノード実行中(Running)に毎フレーム呼ばれる
        /// </summary>
        public virtual BehaviorStatus OnUpdate()
        {
            if (_status == BehaviorStatus.Completed)
            {
                Debug.Log("This task already has been completed.");
                return _status;
            }

            if (_status == BehaviorStatus.Inactive)
            {
                OnStart();
            }

            return _status;
        }

        /// <summary>
        /// ノードの実行が終了したら呼ばれる
        /// </summary>
        public virtual void OnEnd()
        {
            if (_status == BehaviorStatus.Completed)
            {
                return;
            }

            _status = BehaviorStatus.Inactive;
        }

        /// <summary>
        /// ノードが中断された際に呼び出される
        /// </summary>
        public virtual void OnAbort()
        {
            OnEnd();
        }

        /// <summary>
        /// 子ノードを追加する
        /// </summary>
        /// <param name="child">追加する子ノード</param>
        public virtual void AddNode(Node child)
        {
            // do nothing.
        }

        /// <summary>
        /// 子ノードを複数追加する
        /// </summary>
        /// <param name="nodes">追加する子ノード郡</param>
        public virtual void AddNodes(params Node[] nodes)
        {
            // do nothing.
        }
    }
}

見てもらうと分かるように、OnStartOnUpdateOnEndメソッドをvirtualで定義し、派生クラス側でそれをoverrideして利用する想定です。
OnAwakeだけは少し特殊で、SimpleBehaviorTreeクラスが起動された際に、ツリー全体を走査して、すべてのノードを「起こし」ます。そのときに実行されるのがOnAwakeです。
なので、このメソッドは実行後一度だけ呼ばれるメソッドとなります。初期化処理などをここで記述する想定です。

SimpleBehaviorTreeクラスは全体を管理

前述したように、SimpleBehaviorTreeクラスが全体を管理するマネージャクラスとなります。
そして管理方法としてはシンプルに、ツリー構造を持ったタスクすべてにユニークなインデックスを割り振り、インデックス番号からすぐに該当タスクを取り出せるようにします。

インデックス割り振りは、起動時に全タスクを起こすOnAwakeを呼び出すタイミングで行います。その際に、順にツリーを辿ってインデックスを割り振ります。

処理としては以下のようになります。

/// <summary>
/// 対象ノードを起動(Awake)する
/// </summary>
/// <param name="node">起動するノード</param>
private void CallOnAwake(Node node)
{
    // ノードに全体のグラフの通し番号を設定する
    node.Index = _nodeList.Count;

    _nodeList.Add(node);

    // 対象ノードにオーナーを設定する
    node.Owner = _owner;

    // ノード起動
    node.OnAwake();

    // CompositeNodeの場合は再帰的に起動させる
    CompositeNode cnode = node as CompositeNode;
    if (cnode != null)
    {
        foreach (var child in cnode.Children)
        {
            CallOnAwake(child);
        }
    }
}

そして、全体が管理できるようになったら、ルートノードから処理を開始します。
実行はツリー構造を辿り、シーケンサセレクターなどから構成された部分ごとに実行を繰り返します。

最終的には必ずどれかのリーフノードにたどり着くので、それを実行し、それが「成功」か「失敗」か、あるいは「実行中」かを判断し、「実行中」だった場合は処理をいったんそこで停止します。

※ 今回の実装では、全タスクの走査は1フレーム内で行います。そのため、どこかに無限ループがあったり、実行内容が多すぎる場合はゲームが停止します。

Conditionalノードの監視

前述したように、条件分岐を行うノードに関しては必要に応じて「再評価」する仕組みを導入します。
具体的には、再評価するべきとフラグが立てられたノードに関しては再評価リストにノードを登録し、他のタスク実行中も常に条件分岐処理(OnUpdate内でそれを行う)を実行し、もし条件分岐の状態が変化した場合にそれを通知します。

再評価対象などの情報を保持する専用クラスを設ける

今回の設計方針では、全タスクに対してインデックスが割り振られているので、そのインデックスを保持して、どのタスクが再評価必要かを把握するにはインデックスだけがあれば十分です。
ただ、それに紐づく親のタスクインデックスや、前回のタスクの終了状態などを一緒に保持しておくことで、評価が切り替わったことを検知することができるようになっています。

/// <summary>
/// 再評価する際の諸々の情報を格納する
/// </summary>
public class ConditionalReevaluate
{
    public int Index { get; set; }
    public BehaviorStatus Status { get; set; }
    public int CompositeIndex { get; set; }
    public int StackIndex { get; set; }
    public void Initialize(int i, BehaviorStatus status, int stack, int composite)
    {
        Index = i;
        Status = status;
        StackIndex = stack;
        CompositeIndex = composite;
    }
}

Indexはタスクのインデックス、Statusは判断されたときの状態、StackIndexはスタック中どこにあるかのインデックス、CompositeIndexは親のインデックスを参照しています。

この「再評価情報」をリスト化し、再評価が必要なタスクを毎フレームチェックし、変化があった場合にそれを検知します。
そのために、毎フレームのUpdateごとに再評価リストをすべて評価します。

/// <summary>
/// 再評価が必要なノードを再評価する
/// </summary>
/// <returns>再評価して変化のあったノードのIndex</returns>
private int ReevaluateConditionalTasks()
{
    for (int i = 0; i < _reevaluateList.Count; i++)
    {
        ConditionalReevaluate cr = _reevaluateList[i];
        BehaviorStatus status = _nodeList[cr.Index].OnUpdate();

        // 前回の状態と変化していたら、祖先まで遡って処理を停止する
        if (cr.Status != status)
        {
            CompositeNode cnode = _nodeList[cr.CompositeIndex] as CompositeNode;
            if (cnode != null)
            {
                cnode.OnConditionalAbort(cr.Index);
            }
            _reevaluateList.Remove(cr);
            return cr.Index;
        }
    }

    return -1;
}

再評価に変化があったら、同じ祖先ノードまで遡って停止を伝える

もし仮に、どこかの再評価が変化した場合は、それまで実行していたタスクを終了させ、変化した分岐からやり直します。
例えば、シーケンサの最初のタスクに「敵が近くにいたら」というタスクがあった場合、大体の場合においては敵が近くにいないため「失敗」に終わります。
失敗した場合は「パトロール」などの別のタスクが実行されることになります。

そしてしばらくして敵が近づいてきた場合は分岐の結果が変わります。
このタイミングで、先に進んでいたタスクの「パトロール」を停止し、分岐結果が変わったところまでやり直し、以後の処理を継続します。

つまり、敵が近づいてきたら攻撃、などの「分岐が成功したあと」のタスクが実行される、というわけです。

↑最初の分岐が失敗になり、次のシーケンスが実行されている状態

↑最初の分岐状態が変更されたため、それまで実行されていたタスクを終了し、変化した分岐先の処理が開始される

以上のように、タスクを順次実行しながら、分岐が変化した際にそこまで戻って処理を行うことで、状況を把握し、適切に行動を起こすAIの仕組みを構築することができます。

実装してみて

SequencerSelectorが重要な要素になります。
これらが連携しながらAIとしてキャラクターを操作していきます。

なので、アクションノードなどは実際の実行処理を記述するのみです。

今回は簡単のため、インスタンス生成時にラムダ式を渡してそれを実行するようにしています。
よく使われるようなアクションは個別に新規で作成してもいいと思います。(例えば一定時間待つアクションとか)

今回実装したアクションノードクラス↓

/// <summary>
/// 実際のアクションを行うノード
/// </summary>
public class ActionNode : Node
{
    private Func<GameObject, BehaviorStatus> _action;

    public ActionNode(Func<GameObject, BehaviorStatus> action)
    {
        _action = action;
    }

    public override BehaviorStatus OnUpdate()
    {
        base.OnUpdate();
        _status = _action.Invoke(Owner);
        return _status;
    }
}

実装してみて思ったのは、こうしたグラフを持つものはやはり、GUIベースのツールがないと実装するのは困難だな、ということ。
仕組み自体はできたものの、これを、高度に入り組んだAIを実際に組むとなるとコードが膨大になり、かつ管理も困難になるので、実際のところは、上で紹介したようなアセットを使うなどしたほうがよさそうです。

が、仕組みや動作原理などを把握したかったので今回のサンプルを作成しました。
冒頭でも書きましたが、いろんな記事を拾い読みして想像で実装したので、若干勘違いや他で使われているものと異なっている部分があるかもしれません。
また、「Behavior Designer」を実際に使ってその使用感や、カスタムタスクを実際に作成して動作確認したりしながら内部を想像して実装したので、だいぶ「Behavior Designer」よりの実装になっていると思います。

ちなみに、(タイトル同じですが)GREEのEngineerブログで、PHPによる実装例が記載されていました。
こちらもだいぶシンプルな例になっているので、合わせて見てみるとより分かりやすいかもしれません。

シンプルなBehaviour Treeを実装してみる - GREE Engineers' Blog

その他のAI実装パターン

ちなみに、以前にこれとは別のAI作成パターンとして、「ゴール駆動型エージェント」の記事も書いているので、AI関連に興味がある方は読んでみてください。

edom18.hateblo.jp

edom18.hateblo.jp

Daydream開発はじめたのでメモ

概要

Daydream開発を始めてみたので、色々とメモしていきます。
基本的に気づきベースなので、随時更新していきます。

開発環境の整備

開発環境が整わないと色々と大変なので、Tips的な部分をメモ。

adbをWi-Fi経由で実行する

Daydream開発をしていると、実機インストール→Daydreamデバイスで見る、を繰り返すため、USB接続でやっているととてもめんどくさいです。
しかし、adbにはWi-Fi経由でコマンドを実行する方法が用意されています。

まず、USBで一度Android端末とPCを接続します。
コマンドプロンプトから以下のコマンドを実行することで、以後はWi-Fi経由でコマンドを実行し、ケーブルレスでapkのインストールなども可能になります。

$ adb tcpip 5555
$ adb connect xxx.xxx.xxx.xxx

xxx.xxx.xxx.xxxAndroid端末のIPアドレスです。

接続を解除するには以下のようにします。

$ adb disconnect

コントローラを使う

Daydream Viewには標準でコントローラが付属しています。
そしてDaydreamアプリはコントローラを認識しないと起動しないので、必然的にコントローラが必須となります。

アプリ内でコントローラを利用するにはGvrControllerクラスが提供するプロパティを利用します。
ドキュメントはこちら(Controller API Basics

コントローラの状態を知る

ドキュメントから引用すると、以下の状態を知ることができます。

Buttons

The controller reports the following button properties:

  • ClickButton: True if the Click button (a touchpad click) is currently being pressed.
  • ClickButtonDown: True for 1 frame after user starts pressing the Click button.
  • ClickButtonUp: True for 1 frame after user stops pressing the Click button.
  • AppButton: True if the App button is currently being pressed.
  • AppButtonDown: True for 1 frame after user starts pressing the App button.
  • AppButtonUp: True for 1 frame after user stops pressing the App button.

GvrEventSystemを使う

Google VR SDKから、イベントシステムを利用した、各種イベントの制御の方法が提供されています。
といっても、独自のものはIGvrPointerHoverHandlerのみで、それ以外は通常のuGUIで提供されているインターフェースを実装することでイベントをトラッキングすることができるようになっています。
(例: IPointerClickHandlerなど)

ハマったこと

いくつかハマったことをメモとして残しておきます。

Single Passレンダリングを利用すると色味がおかしくなる

色々な最適化方法を参考に設定を行っていたところ、Single Passレンダリングを有効化して実機確認したところ、色が反転するような現象が現れました。
結論から言うと、Use 32bit Display Bufferをオフにしていると起こるようです。

Daydreamコントローラの感度が悪い

開発をしていて、しばらくしたらどうも「Daydreamコントローラの感度が悪いな・・」と思うことが何度かありました。
いろいろ調べてみると以下の投稿を発見。どうやら、充電がなくなると精度が悪くなるようです。
なので、充電に気をつけて、できるだけフル充電状態で開発ができるようにしておくとよさそうです。

www.reddit.com

Playerにコライダを設定したらイベントが動作しない

GvrEventSystemに則ってイベントを処理していて、とあるタイミングでPlayerにコライダを設定したところ、コントローラのRayが自身にヒットしてしまって一向にオブジェクトに反応しない、ということがありました。

冷静に考えればRayCastなので当たり前ですが、仕組みがそもそもどうやっているのかを把握していなかったので若干ハマりました。
GvrPointerPhysicsRaycasterクラスに、eventMaskというプロパティがあるので、それにマスクを設定しておけば大丈夫です。

例:

GvrPointerPhysicsRaycaster raycaster = GetComponentInChildren<GvrPointerPhysicsRaycaster>();
int playerLayer = (1 << 8);
raycaster.eventMask = ~playerLayer;

ポイントした先にワープするとポインタが正常に動かなくなる

これも理由は上記と同じなのですが、ワープポイントみたいなオブジェクトを作り、それにコントローラで選択させて、決定時にその場所までワープする、みたいなことは比較的やる処理かなと思います。

ただ気をつけないとならないのは、ワープポイントの場所に移動させると、(実装の仕方によっては)コライダの中に入ってしまい、結果そのオブジェクトにコントローラのポインタが取られ続けてしまう、という問題があります。

自分の実装では、プレイヤーがワープポイントにいる間はRaycast向こうなレイヤーに変更し、別の場所に移動したら戻す、ということで対処しました。

AndroidアプリのDaydreamコントローラエミュレータで実機アプリを動かす

Daydreamは、専用コントローラを関連付けないと起動ができません。
(コントローラを認識してね画面から先に進まない)

とはいえ、現状は日本での発売がなく、せっかくDaydream対応端末を買ってもDaydreamコンテンツを楽しむことができません。
そこで、Android上で実行できる、コントローラのエミュレータを使います。

エミュレータはこちらからダウンロードすることができます。
基本的にはPC(開発機)に接続して開発用途として使うものになります。

が、やはり実機での確認は必須。
ということで、これを実機でも利用できるように設定します。

手順としては、

  1. Daydreamアプリを実行するAndroid(実機)と、コントローラ用AndroidBluetoothでペアリングしておく(これはDaydream関係なく普通に設定から行う)
  2. Daydreamアプリ側で、GoogleVR設定画面から「ビルド」を複数回タップして開発者メニューを表示する
  3. 開発者メニューから、コントローラのエミュレータを有効化する
  4. (3)のタイミングで、どのAndroid端末のエミュレータを利用するかリストが表示されるので、(1)で同期した端末を選択する

という流れで行います。

無事に接続が完了すると、エミュレータ画面で「Connected」表示になるので、あとは普通にDaydreamアプリを操作することが可能になります。

フレームレートが出ない

開発をしていて、Physicsを利用してすばやく動かしたオブジェクトがやたらブレて見える現象がありました。 野生の漢さんの記事で書かれている、FixedUpdate周りかなと思って色々やってみたものの変化なし。

そして色々調べていたところ、以下の記事を発見。

https://forum.unity3d.com/threads/best-practice-project-settings-for-daydream.438598/

ここで書かれている以下の項目にしたところだいぶ軽くなりました。

  • Enable 2x Anti Aliasing. (Unity will issue a warning about performance when you build, but it performs fine and looks bad without it.)
  • Disable real time shadows. (They look bad and kill performance.)
  • Disable "Use 32-bit Display Buffer"
  • Stick with Forward or Vertex rendering
  • Enable Multithreaded Rendering
  • Enable Static and Dynamic batching

HDRが有効にならない

Post Processing Stackを使っていて、Bloomが有効にならないなーと思ったら、そもそもHDRが有効になってなかったというオチ。
Graphics Settingsを見てみると、Androidの場合はuse HDRがデフォルトではオフになっているのが原因。それをオンにしたら普通に動きました。

このへんで言及されてるやつ。

github.com

プロファイリング

プロファイリングするにはいくつかのツールがあります。
一番シンプルなのは、ビルトインの内部プロファイラを利用することだと思います。(が、なぜかAndroidでこれが出力できなかった( ;´Д`))
ただ、色々試したのでメモしておきます。

adb logcatでログを出力

AndroidをPCに接続して、以下のようにコマンドプロンプトから実行すると、ログがだーっと出力されます。

$ adb logcat

ただ、これだとAnroid上で実行されているすべてのアプリやシステムのログがすべて出力されてしまうので、Unityだけに絞ります。
具体的には、

$ adb logcat Unity:V *:S

Unity部分がタグで、VVerboseの略、*:Sは、Unityとタグが付いたもの以外のログをすべてSilentにする、という指定です。
これで無事、Unityタグがついたログだけがフィルタリングして出力されるようになります。

ファイル保存

セーブデータなど、ファイルを保存するには保存先パスを指定する必要があります。
保存先パスとしてはApplicationクラスから取得できる以下のパスを利用します。

Application.dataPath

アプリケーションが保存されているパス。

環境 取得パス
Android(本体) /data/app/<アプリのID>.apk
Android(SDカード) /mnt/asec/<アプリのID>/pkg.apk
iOS /var/mobile/Applications/****/myappname.app/Data

Application.persistentDataPath

永続的なデータを保存するパス。 ユーザーデータなど、アプリが再生成できないファイルを配置する。

環境 取得パス
Android(本体) /data/data/<アプリのID>/files/
Android(SDカード) /mnt/sdcard/Android/data/<アプリのID>/files/
iOS /var/mobile/Applications/****/Documents

apkに含まれているManifestの内容を確認する

Daydream向けアプリでは、いくつかの機能の有効化などをManifestに記載する必要があります。
その際、実際にビルドしたapkに該当の項目がしっかり含まれているか、というのを確認したいケースがあります。

それを行うには、aaptというツールがあるので、それを利用します。
aaptは、apktoolsに含まれています)

ibotpeaches.github.io

コマンドは以下のように実行します。

$ aapt l -a hoge.apk

すると、ダーっと色々な情報が出力され、その中にマニフェストの内容も含まれているので、それで内容を確認することが出来ます。

詳細はこちら

stackoverflow.com

参考

logcat コマンドライン ツール  |  Android Developers

docs.unity3d.com

maimai-jp.hatenablog.com

有用リンク

八分木(モートンオーダー)を使ってエリアを分割して処理負荷を軽減する

概要

今回の記事に関しては以下の記事をほぼそのまま参考にさせてもらっています。
実装自体はUnityで行い、コードもC#で作りましたがベースとなる理論は記事をそのまま参考にさせていただきました。
マルペケさんの記事には毎度本当にお世話になっています。

ここでは、そこで得た知識を「自分なりに」理解した内容をまとめて解説したいと思います。

ちなみに今回の記事を書くのに、Unityで実装したサンプルがあります。(サンプルプロジェクトはGithubに上げてあります

実行イメージは以下のようになります。


赤いラインがセルを表現していて、どの空間に所属しているかによって衝突可能性のあるオブジェクト同士をラインで結ぶというデモです。

参考にさせていただいた記事

そしてこれを使って実際にUnity上で動くサンプルも作ってみたので、興味がある方は見てみてください。
https://github.com/edom18/MortonOrder

ちなみにどんな内容かと言うと、八分木(Octree(オクトツリー))という概念を使って、3D空間を格子状に分割し、衝突判定などを効率的に行うための理論です。

Wikipeidaから引用させてもらうと以下のようなイメージです。

八分木(オクトツリー)とは

八分木(英: Octree)とは、木構造の一種で、各ノードに最大8個の子ノードがある。3次元空間を8つのオクタント(八分空間)に再帰的に分割する場合によく使われる。四分木を3次元に拡張したものと見ることができる。英語の名称は oct + tree に由来するが "octtree" とは書かず "octree" と書く。

Wikipediaから引用

図を見てもらうと分かりますが、ひとつのBoxが8つに分割され、さらにその分割されたうちのひとつのBoxがまた8つに分割されていく、という構図になっています。
そしてこれを理解する上で重要なポイントは、2進数で考えること。

8つなので3bit単位で分割されていくわけですね。
以下からの解説に関してはこのbitを意識して読んでもらうと分かりやすいかと思います。

※ ただし、今回の記事のほぼすべての解説は2D平面による解説(4分木)となっています。(なので2bit単位)
が、基本的にX,Y軸に加えてZ軸を追加するだけで、基本的な考え方はあまり違いはありません。

モートンオーダー(モートン順序)

これ、最初に知ったときは驚きました。
空間に登録するための判定処理が、まさかオーダーO(1)にまで縮小できるなんてと。
世の中にはほんとに頭のいい人がいるんだなーと思わされました。

2進数(2bit)で空間を分解していく

まずは以下の図を見てください。
4つごとにどんどん分割されていく状況を示しています。

じーっと図を見ていると、徐々になにかが見えてきます。
冒頭で話した通り、それは「2進数」です。

ひとつの空間を4つに分割する。つまり2進数で言うと2桁ですね。コンピュータなら2bit単位です。
そしてそれをひとつの単位として、さらにそれを分割する場合は、また2bitで分解します。

つまり、ひとつ下のレベルに分解するに従って末尾に2bitずつ追加されていく、と見ることもできます。

上の図をベースに説明すると、ルート空間は当然ながら「0」ですね。これは分割してないので当然です。
これをひとまず2bitを使って00と表現しておきます。

さてでは次に。これを4分割します。すると2bitなので2桁の2進数を末尾に追加します。
すると合計4桁になるので00 00となりますね。

そして下位2bitは当然、0〜3の値を持つことができます。まさに分割された空間のindexですね。
ではさらに続けて、それを分解してみましょう。すると、00 00 00と6桁になります。
末尾2桁は前述と同じく0〜3となります。

つまり、親空間を示すのが(上記の例で言えば)中央の2bitに当たります。
以後は、親→子→孫→ひ孫と分解レベルが上っていくにつれて末尾にたされる2bitもそれに応じて増えていくことになります。
しかし今見たように、基本的なルールはなにも変わりません。

試しに45番を例にしていましょう。

まず、孫での45番は一番右の図の位置になります。
そして左にいくに連れて親空間での位置になります。

45という数字を2bit単位に分解して考えると、確かにそれぞれの空間のindexが隠れていることが分かるかと思います。
(ルート空間は0なので省略)

前述の説明を借りるなら、親空間の2、つまり10を分解して2bitを末尾に足し、さらにそのindexである3、つまり11を追加。
さらにそれを分割して1(01)を末尾に追加し、これを10進数に戻せば45が出て来る、というわけですね。
こう考えるとなんとなく数値と分割の関連が見えてこないでしょうか。

裏を返せば、示された番号を2進数に分解して、かつ2bit単位で分けていくと各空間のindexが顔出す、というわけです。
別の言い方をするなら、2進数の概念を整理してうまく視覚化したもの、と見ることができるかもしれません。
最初にこれを見たときはまさにこの感覚を持ちました。

つまり、空間の通しindex番号をひとつ与えられただけで、そのオブジェクトがどの空間のどの位置にいるか、ただのビットシフトのみで計算できるというわけなのです。
計算量として表せばO(1)のオーダーとなります。なんとすばらしい! 真面目にどの位置にいるか線形検索していては到底たどり着けない高速さです。

座標位置から、所属する空間番号を割り出す

空間分割をモートンオーダーで行うと、すばらしく簡単に空間番号を割り出すことができることが分かりました。

では実際に利用するシーンを仮定して見てみましょう。
以下の図を見てください。

空間分割している意味は、そもそも動くオブジェクトとのあたり判定など「総当り」で調べることをせず、簡単に「近くの」オブジェクトを探すことが目的です。
なので「このオブジェクトはどこに属しているのか?」を知ったり、「この点の近くにあるオブジェクトは?」といったことにも答えられなければなりません。

しかしそれもとても簡単に達成することができます。
上記の図は、孫空間まで分割された状態(8x8=64分割)を示しています。

仮にルート空間の一辺の長さを100とした場合、孫空間での単位距離は8({=2^3})で割ることで求められます。
そして(例えばユーザがクリックした位置などとして)35, 58という座標が与えられたとしましょう。
これを、求めた単位距離で割ると2.8, 4.64となります。そしてこれをさらに端数切り捨てて2, 4にします。

これは、64分割された空間の左から2番目、上から4番目の位置であることを示しています。(indexなので0番始まりな点に注意)
つまり「36」の位置になります。

さて、今までずっと2進数で話をしてきたので、こちらも同じく2進数で見比べてみましょう。
すると、最初の2, 4010, 100となります。 そして「36」を2進数で表すと100100となります。

ちなみに上では3bit単位で表現していますが、これは空間の分割回数(空間レベル)からです。(冒頭で書いた2D = 2bit、3D = 3bitの意味ではありません)
今回の例では3回分割を行っているため3bit表現になっています。空間分割が2進数の視覚化と考えれば、分割数がそのままbitとして出現するのはイメージができるかと思います。

実はこれ、2bit単位で見たときにxの値(=2)を右のbit、yの値(=4)を左のbitとして合成すると、100100となるのです。

|_0|_1|_0| // xの値は2bit単位で区切った際の右bit
|1_|0_|0_| // yの値は2bit単位で区切った際の左bit
----------
 10 01 00

なんということでしょう。
xの値は右のbit、yの値は左のbitとして機能しているわけですね。
そしてこれは、これに限らずすべての空間について成り立ちます。

前述のように、この数値から孫空間だけでなく、子空間、親空間のどの位置か、までが一発で決まってしまいます。やってみましょう。
まず、100100を2bit単位に分解します。すると、10 01 00と分けることができます。

左から順に、「親空間」「子空間」「孫空間」の位置を表していたのでしたね。
親空間は10です。つまり、2番の位置にあることがわかります。図にすると以下ですね。

孫空間の36番は確かに「親空間」の2番の位置にあるのが確認できます。
では「子空間」ではどうでしょうか。

子空間の番号は01、つまり1ですね。1は右上です。図で言うと、

こちらも確かに1番の位置にありました。

最後は孫空間です。
孫空間は00ですね。つまり0、左上です。

こちらも確かに左上にありました。

以上のように、bit演算だけで、座標からびしっと空間を特定する方法、それが「モートンオーダー」です。
これを考えた人、ほんと天才だと思います。

以上を踏まえた上で、x,yの値から孫空間の位置を割り出す関数(bitシフト)は以下のようになります。

int BitSeparate(n)
{
    n = (n | n << 8) & 0x00ff00ff;
    n = (n | n << 4) & 0x0f0f0f0f;
    n = (n | n << 2) & 0x33333333;
    return (n | n << 1) & 0x55555555;
}

「オブジェクト」の所属空間を求める

さて、前述のように「点」がどの位置にあるかを求める方法が分かりました。
しかし、ほとんどの場合、オブジェクトは点ではなく、ある程度の大きさを持った「ボリューム」となります。

そしてそのサイズによっては孫空間には収まらず、「どの空間分割数が適切か」を判断しないとなりません。
2Dの場合、大きさを考慮するには左上と右下の値を採用し、いわゆるAABB(軸並行バウンディングボックス)で考えることで解決できます。

つまり、左上の点と右下の点ふたつの所属空間をまず求め、その後にそのサイズに応じてどの空間に属するかを判断します。
とはいえこれは簡単ではありません。― 普通に考えては。
これまたすごい発想(というかどうしたらそんな発想が出るのかと思ってしまいますが)を利用します。

参考にさせていただいた記事から引用させてもらうと、

上の例だと左上は19番、右下は31番に含まれています。ここからが面白いところで、色々と試行錯誤し、またJagoon様からのご指摘によって修正されたうれしい法則です。右下の空間番号31と左上空間番号19の排他的論理和を取ると12となります。この12を2進数で表現するとこうなります:

実は、境界図形の所属する分割レベルはこの排他的論理和の結果ビットが0じゃない最上位の区切りに表現されます。上の例だと青色で示した区切りが[11]となっています。所属空間は、上の例のように一番下の区切りを一つ上のレベル(ここでは子になります)、そこから親、ルートと空間レベルを上げていきます。[11]のビットはこの法則に従うと「親空間に所属」と判断されます。

とのこと。ほんとbit演算さまさまです。
サイズに応じてどの空間に属するか、ほぼO(1)で処理が済んでしまいます。

XORでなぜ所属空間が求まる?

このXORで出てくる理由。自分の理解を添えておくと、以下のようなイメージです。

各番号には所属親空間の情報が残っている。つまり、同じ親空間の情報はXORで取り除かれる。
結果として、違う親空間にいる場合はそれが「フラグ」として抽出される。

という感じです。
ここで「フラグ」と書いたのは、抽出した値自体に意味はなく、「どの位置のbitが立っているか」という、まさにフラグ的な利用をするからです。
その利用については後述します。

試しにひとつ例題をやってみましょう。

以下の図を見てください。

左上を32、右下を47とします。まず、図を見て答えを見つけてしまうと「親空間」の2に所属することが分かりますね。これを計算で求めてみましょう。
32^46 = 15となります。2進数にすると1111ですね。4分割なので00 11 11と3つ分で表します。(子空間から始めるので4-1 = 3
そして「ルート 親 子」のどこに属しているかは、bitが立っている位置を参考にします。

具体的には「どの位置のbitが立っているか」です。
上記の例で言うと、3つに分解した範囲で考えると、左から2つめ、および3つめにbitが立っていますね。
そしてbitが立っている部分の、一番左側を採用します。

「ルート/親/子」の3つとして考えると「親」の部分のbitが一番左側のbitとなります。
つまり所属空間は「親」ということになりますね。

こうして手作業で探すのは簡単ですが、プログラムでもそれほどむずかしくはありません。
2bit単位で見ているだけなので、最初の2bitをチェックして値が1以上かをチェックし、そうであった場合に空間レベルを上げていく、というふうに実装すればよいのです。

実際に今回これを実装した箇所は以下のようになります。
(ちなみに下のコードでは3Dに拡張しているため、3bitごとチェックしている点に注意してください)

int xor = ltd ^ rbd; // 2点の位置のXOR
int i = 0;
int shift = 0;
int spaceIndex = 0;

while (xor != 0)
{
    // 下位3bitずつマスクして値をチェックする
    if ((xor & 0x7) != 0)
    {
        // 空間シフト数を採用
        spaceIndex = (i + 1);
        shift = spaceIndex * 3;
    }

    // 3bitシフトさせて再チェック
    xor >>= 3;
    i++;
}

計算結果の下位3bitの値をチェックし、0以外=bitが立っているので、その場合はいったんその空間レベルを記憶しておきます。
そして計算結果を3bitシフトして再チェック。以後、値が0になるまでこれを繰り返し、最終的に得られた結果が空間レベルと、空間レベルを得るために必要なシフト数が得られる、というわけです。

さて、実際にこれを前述のルールの沿って見てみると、親空間のindexを得るには右4シフトすればいいことが分かります。
なぜか?

そもそも求めた番号には常に、その親に当たる空間のindexは含まれているのでしたね。
なので、「どの親に所属するか」さえわかれば、あとは数値をその親空間までシフトしてやれば自然とそのindexが姿を表す、というわけです。

実際にやってみましょう。
右下の数字47を右に4シフトすると(47 >> 4)答えは2です。
(ちなみに、どちらも同じ親に属しているので、32のほうを4bit分シフトしても同じ値が算出できます)

つまりまとめると「親空間の2に属する」となりました。

図で見ていた通りの結果になりましたね。すごい!

線形4分木でアクセスを高速化

前述の話は、すべてツリー状(木構造)での話でした。
しかしそれでは再帰的に調べていなかければならず、せっかく空間への登録・確認がO(1)にできたのに意味がなくなってしまいます。
これもO(1)で取れるようにしてみましょう。

今まで出てきたものを線形、つまりひとつの配列に収めてしまいます。

上図のように、線形配列に直したものを線形4分木(Liner Quaternary Tree)と呼びます。
線形の配列(リスト)にすることで、これまた各空間へのアクセスをO(1)にします。

ただ当然ですが、各分割レベルの最初のindexは0です。
そのまま分割空間レベルごとのindexを使うわけにはいきません。
以下のように通し番号として表現する必要があります。

図を見てもらうと分かりますが、各分割空間レベルの最初のindexはその前の空間レベルの分割数の合計を足した数になっているのが分かるかと思います。
例えばレベル2なら、その前にはルート空間で1、その下の空間で4、合計5個の要素があります。
なのでレベル2は5から始まっているわけですね。

そしてこれを計算するには「等比級数の和」を利用し、以下のように式で表すことができます。

\( 足す数 = \frac{4^L - 1}{4 - 1} = \frac{4^L - 1}{3} \)

今回の例では以下のように実装しました。

int ToLinearSpace(int mortonNumber, int level)
{
    int denom = _divisionNumber - 1;
    int additveNum = (int)((Mathf.Pow(_divisionNumber, level) - 1) / denom);
    return mortonNumber + additveNum;
}

さぁ、これで各オブジェクトの所属空間の検索、更新、登録、操作すべてがO(1)で行えるようになりました。

衝突判定について

衝突判定についても最適化の方法があります。
ただ、今回の記事はあくまで8分木の解説なので、その後のリスト生成などはしません。
参考にしたマルペケさんの記事ではそのあたりについても解説しているので、そちらを参考にしてみてください。
いちおうサンプルとして上げたほうはそれを元に、衝突判定のリストを生成するところまでも実装済みです。

ちなみに衝突判定以外にも、近傍のオブジェクトを取得する、など他にも色々と使いみちはありそうです。
その場合でも、空間へのアクセスがO(1)で行えるので、「近傍」を定義してその空間へアクセスすればすぐにその空間に所属しているオブジェクトのリストを得ることができると思います。

SkinnedMeshとBoneWeightについてメモ

概要

とある実装をしていて、アニメーション周りをわりと触ったのでそのメモです。
具体的には、メッシュカットを利用してメッシュを分断したあと、分断したメッシュもボーンアニメーションさせたくて実装したメモです。

実際に切断してアニメーションさせた動画↓

見てもらうと分かりますが、カットされて分断されても同じようにアニメーションしているのが分かるかと思います。

ちなみに、ボーンアニメーション(スキニング)については、前にWebGLで実装した際に解説した記事があるので、興味がある人は読んでみてください。

qiita.com

また、いつもお世話になっているマルペケさんのところの記事を参考にしたので、こちらのほうがもっと詳細です。

その61 完全ホワイトボックスなスキンメッシュアニメーションの解説

原理

スキニング自体の実装や概念については、上記の記事を参考にしてみてください。
ここでは、あくまでメッシュカットをしたときに実装した内容を元に、Unity上でどうしたらいいか、という視点から書いていきます。

とはいえ、簡単なボーンアニメーションの仕組みについて触れておきます。

ボーンアニメーションとは、ボーンと呼ばれる頂点群を定義し、そのボーンの動きに応じてポリゴンの各頂点が動かされる、という仕組みです。
では、どのボーンが動いたらどの頂点が動くのか。それを定義するのがBoneWeightです。

名前の通り、ボーンのウェイト、つまり「影響度」の値を保持します。
なので、メッシュは頂点数と同じだけのBoneWeight配列を保持しています。

BoneWeightは最大で4本までのボーンが設定できます。
ここで勘違いしないでもらいたいのは、モデル全体で4本だけ、ではなく、あくまで「1頂点に影響を与えることができるボーンは最大で4本」ということです。
これはマルペケさんの記事を見てもらうといいと思いますが、とにかくボーンの数を増やしてしまうとGPUで扱えるメモリの領域(レジスタ)がすぐに枯渇してしまうためです。

また、頂点に着目すれば、そもそも4本以上のボーンから影響を受けるケースというのは稀でしょう。
つまり、逆を言えば4本で十分とも言えます。

なのでBoneWeight構造体の定義を見てもらうと分かりますが、boneIndex0boneIndex3weight0weight3まで、配列ではなくそのままの名称で定義が書かれています。

ここで、boneIndexは、各ボーンに振られたインデックス番号、wegithは、そのボーンからどれくらいの影響を受けるかを示したものになっています。
なお、4本のボーンの影響度の合計は必ず1になるように正規化されます。

ボーンの簡単な説明は以上です。

今回はこのボーンの影響度を、メッシュカット後の各メッシュに適用し、それをアニメーションさせる、という実装を行いました。

ちなみに、メッシュカットについては以前、公開されていたコードを読んで解説した記事を書いたので、興味がある方はそちらも合わせてご覧ください。

qiita.com

登場人物

今回の実装にあたって、必要なクラスやプロパティなどがいくつか絡み合っているのでそれらを明確にします。

  • Mesh
  • SkinnedMeshRenderer
  • Animator
  • BoneWeight

Meshクラス

まずはMeshクラス。

Mesh mesh = new Mesh();
mesh.name = "Split Mesh";

// _cuttedMeshesは独自で定義したメッシュ情報を持つクラスの配列
mesh.vertices = _cuttedMeshes[i].Vertices;
mesh.triangles = _cuttedMeshes[i].Triangles;
mesh.normals = _cuttedMeshes[i].Normals;
mesh.uv = _cuttedMeshes[i].UVs;
mesh.subMeshCount = _cuttedMeshes[i].SubMeshCount;
for (int j = 0; j < _cuttedMeshes[i].SubMeshCount; j++)
{
    mesh.SetIndices(_cuttedMeshes[i].GetIndices(j), MeshTopology.Triangles, j);
}

mesh.bindposes = oriRenderer.sharedMesh.bindposes;
mesh.boneWeights = _cuttedMeshes[i].BoneWeights;

Meshはポリゴンを形成するために必要な情報をまとめて持っているクラスです。
verticestrianglesnormalsuvsubMeshCountsubMeshIndicesなどがそれに当たります。

今回はメッシュを平面で切って分割するため、新しく計算した頂点群などを再設定しています。

そしてさらに、メッシュは(前述のように)ボーンの影響度についても知らなければなりません。(アニメーションさせたい場合)
そこで設定しているのがboneWeightbindposeです。

boneWeightは上で書いた通り、各頂点が影響を受けるボーンの情報です。
そしてbindposeは、「モデルのデフォルト位置としてのボーンの姿勢」です。

人型のモデルであれば、Tポーズ時の位置、と考えるといいと思います。
つまり、「動いていない状態のボーンの位置」ということですね。

なぜこれを定義しているかというと、ボーンというのは「動いた差分分だけ、頂点に影響を与える」ようになっているからです。
つまり、ボーンがまったく動かされないならTポーズのまま、ということですね。

そしてそこから、ボーンが曲がったり移動したりすることで、モデルは様々な形に変形されます。
さらにそれを連続して行うことでアニメーションが成立している、というわけです。

SkinnedMeshRenderer

アニメーションするために必要なSkinnedMeshRenderer
ボーンに対応した頂点を、ボーンの動きに追従して動かすことでアニメーションを行います。

そしてSkinnedMeshRendererはその名の通り、スキニングされるメッシュを表示するためのクラスになります。
そのため、いくつかのボーンなどの情報を持たせる必要があります。

// objは新規生成されたGameObject
SkinnedMeshRenderer renderer = obj.GetComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.materials = mats;
renderer.bones = newBones;
renderer.rootBone = newRootBone;

メッシュは頂点群や、それらのボーンの影響度を持つ「データ構造」と言っていいでしょう。
そしてSkinnedMeshRendererは、このデータ構造を用いて適切にボーンの影響を頂点に与えます。まさにレンダラですね。

ボーンアニメーションに必要な情報はここではふたつ。
ひとつはbones、そしてもうひとつがrootBoneです。

rootBoneは、どのボーンがルートになっているのかを示す、Trasnformへの参照です。
そしてbonesは、定義されているボーンへの参照の配列になっています。

ちなみに、なにかしらのモデルデータを開いてみると分かると思いますが、ボーンとして利用されるオブジェクトのツリー構造は、必ずしもボーンだけとは限りません。
例えばなにかをグルーピングするための目的で配置されているオブジェクトなどもあるでしょう。

つまり、ルートボーン以下にはボーンとして機能していないオブジェクトも当然ながら存在します。
そのため、ボーンとしてアサインされたもののみをbones配列が持っている、というわけですね。

Animator

メッシュやレンダラの設定を適切にしても、アニメーション自体を再生しないことには動きません。
ということで、Animatorクラスが必要になります。

今回の実装では、メッシュカット対象オブジェクトが持っていたアニメーションのデータをそのままコピーして使っています。

Animator anim = newObj.AddComponent<Animator>();
anim.runtimeAnimatorController = _target.transform.root.GetComponent<Animator>().runtimeAnimatorController;
anim.avatar = _target.transform.root.GetComponent<Animator>().avatar;

AnimatorAddComponentして、適切にアニメーションに必要なデータを設定(コピー)します。

AnimatorAnimatorControllerについては、Unityでアニメーションさせたことがある人には特に説明の必要はないでしょう。
今回はコピーしていますが、切断後のアニメーションを変える場合は単なるコピーではなく、新しいコントローラを設定してもいいと思います。

そして最後にAvatarをコピーして終了です。

まとめ

今回はあくまでボーンをどうアサインしていったらいいかのメモなので以上となります。
まとめると、

  • メッシュに頂点情報およびボーンウェイト情報とボーンのデフォルト位置を紐付ける
  • SkinnedMeshRendererに、ルートボーンと、アニメーションに使われるボーンの配列を登録する
  • Animatorを通じてアニメーションを行い、AnimatorControllerのステートマシンを用いて各アニメーションを制御する

という流れになります。
データ構造的にはとてもシンプルですね。

次回の記事では、メッシュカットにボーンアニメーションを設定したあたりを書こうと思います。

UNETを使ってネットワーク間で処理を同期する

概要

前に使っていたNetworkViewを使った処理はdeprecatedになり、新しく UNET という仕組みが登場しました。
今回はこれを使っていくにあたって色々新しい概念が出てくるのでそれをまとめておこうと思います。

用語整理

UNETを使う上で、(慣れるまでは)色々混乱する用語があるので、それをまずは整理しておきます。

サーバ

UNETはサーバとクライアントのコードを同じクラス上で記述することができます。
そのため、サーバ側のコードを別途用意する必要はありません。
ただ、実行されるのがサーバ側なのかクライアント側なのか、というのは常に意識しておかないとなりません。

リモートクライアント

サーバに対してのクライアントです。
つまりサーバに接続しているPCということですね。

ローカルクライアント

こちらもリモートクライアントと「ほぼ」同じものです。
サーバに接続し、リモートクライアントと同じように、クライアント側のコードが実行されます。
ただ一点違うのは、以下のホストに関係しています。

ホスト

UNETはサーバ/クライアント側のアーキテクチャなので、必ずサーバが必要となります。
ただ、サーバをどこかにホストして実行する以外の手段として、いずれかのPCがホストとなり、サーバとクライアントふたつを同時に起動する形のものがあります。それを「ホスト」と呼んでいます。

つまり、前述の「ローカルクライアント」は、サーバが実行されているPC上で実行されているクライアント、ということになります。
そして次の名前が似ている「ローカルプレイヤー」と最初は(自分は)とても混乱しました。

ローカルプレイヤー

ローカルクライアントとローカルプレイヤー。どちらもローカルとついていて、なんとなく似たようなイメージの両者。
ただ、ローカルクライアントはネットワーク周りのもになのに対して、ローカルプレイヤーはどちらかというとゲームよりの概念となっています。

誤解をおそれずに言うなら、ローカルプレイヤーとは、そのPC上でプレイヤーを操作しているユーザのこと、と考えてもいいと思います。
当然、マルチプレイゲームであればユーザはプレイしている人の数だけいることになりますが、実際に操作している人のPC上に映し出される、操作可能なキャラクターは大体の場合においてひとりでしょう。
そしてその「操作可能なキャラクター」こそがローカルプレイヤーと思っていていいと思います。

(実際はマルチプレイをする上で、権限だったり、といったことを示すものですが、あくまでイメージはそんな感じです)

ネットワークシステムの概要

詳細についてはドキュメントを見たほうが確かなのですが、ざっくりと自分の認識とポイントを書いておきたいと思います。
以下はドキュメントにある画像です。

https://docs.unity3d.com/jp/current/uploads/Main/NetworkHost.png

ポイントは、用語整理のところでも書いた通り「Host」と書かれた領域に「Local Client」と「Server」があることでしょう。
ドキュメントを読み進めていくにあたって地味に重要なので、違いはしっかり認識しておいたほうがいいと思います。

まずは同期を試してみる

さて、UNET、最近では色々と実装してだいぶ慣れてきましたが、最初の頃はなにをどうしたらいいんだ、という感じでした。
簡単に同期させるだけでもいくつかのクラスやコンポーネントを組み合わせないとならないからです。

なので、まずは簡単に「同期させるだけ」をベースに説明していこうと思います。

同期する上で中心となる「NetworkIdentity」コンポーネント

まず、ネットワーク間で同期を取るオブジェクトには必ずこの「NetworkIdentity」コンポーネントをつける必要があります。
ネットワークで同期を取るということは、それぞれ同期対象のオブジェクト(やキャラクター)を一意に識別し、それを適切に取り扱う必要があります。

そのためのIdentityを司るのが、まさにこの「NetworkIdentity」というわけです。
色々な同期用コンポーネントをつけると必ずこのコンポーネントが一緒にくっついてきます。

位置を同期する「NetworkTransform」コンポーネント

次に、位置を同期したい場合は「NetworkTransform」コンポーネントをアタッチします。
(位置以外にもアニメーションの同期などもあります)
このコンポーネントをアタッチするだけで簡単に位置の同期を行ってくれます。
位置の同期だけであればこれで十分です。

f:id:edo_m18:20170120164537p:plain

ネットワーク間で「オブジェクトの生成」を同期する

さて、位置の同期だけであれば前述のようにコンポーネントを付与するだけでOKでした。
しかし、オブジェクトを新しく生成する場合はそこまで簡単にはいきません。

必要な処理を列挙すると以下のようになります。

  • 生成したいオブジェクトをPrefab化する
  • 該当オブジェクトに「NetworkIdentity」コンポーネントを付与する
  • NetworkManagerに、ネットワーク間で生成を同期したいオブジェクトを登録する(ClientSceneのRegisterPrefabメソッドからも登録できる)
  • 生成したオブジェクトをNetworkServer.Spawnメソッドを利用して、接続している全クライアントに対してオブジェクトが生成されこと(Spawnされたこと)を通知する。

という流れです。
よくよく考えてみれば当たり前ですが、オブジェクトが生成されたことを、接続中のクライアントに通知しないと画面にそれが表示されません。
そのために、Spawnシステムがあるんですね。

コードで書くと以下のようになります。(実行はサーバ上で行う必要があります)

GameObject obj = Instantiate(_anyPrefab);

// これを実行することで、
// サーバで生成されたオブジェクトを全クライアントに通知することができる。
NetworkServer.Spawn(obj);

ネットワークで同期する処理を記述する(NetworkBehaviourを継承する)

オブジェクトの位置を同期したり、生成したり、といったことは大体はコンポーネントの付与で対処できました。
しかし、それだけでは当然ゲームは作れません。
ゲーム足り得るには、やはりユーザの操作を受け付けてなにかしらのフィードバックを行う必要があるでしょう。

そしてそれは当然、プログラムを書く必要があります。

通常、Unityは「MonoBehaviour」クラスを継承することで様々なことを記述していきます。
UNETもこれとコンセプトは変わりません。
ただ、代わりにNetworkBehaviourクラスを継承して処理を記述する必要があります。

NetworkBehaviourクラスはisServerisLocalPlayerなどのプロパティを持っており、今実行しているコードがサーバなのかクライアントなのか、ローカルプレイヤーなのかなどの状況を判別するためのフラグが用意されています。 (ひとつのコードで、サーバ側、クライアント側、あるいはその両方として実行させることができる)

サーバ or クライアント、どちらで実行するか

冒頭では、サーバとクライアントどちらも同一に書けると書きました。
基本的にはオフラインのゲームでの制作と考え方はあまり変わりません。

しかし、中にはサーバ側だけで実行してほしい処理、あるいは逆にクライアントだけで十分な処理などがあると思います。
そうした、「どちら側で実行するのか」を決める方法があります。

ServerServerCallbackClientClientCallback、この4つのAttributeを利用することでそれを実現します。
それぞれの意味は以下のようになります。

  • Server ... サーバ側でのみ実行される処理。クライアント側では空実装になる。
  • Client ... サーバの逆、クライアント側のみで実行される処理になる。
  • ServerCallback ... Serverと似ていますが、UpdateメソッドのようにUnityのシステムから呼ばれるメソッドに対して記述します。
  • ClientCallback ... こちらもServerCallbackの逆ですね。

例えば、サーバ側だけで処理してほしいメソッドの場合は、[Server] Attributeを付与します。

[Server]
void AnyMethod()
{
    // do anything.
}

「ローカルプレイヤー」であるかどうか

位置の同期、サーバ/クライアント双方での処理の記述について書きました。そして、実際に制作するにあたってとても重要なのが処理している対象が「ローカルプレイヤーであるか」という点です。
ローカルプレイヤーはその名の通り、コードを実行している対象が「プレイヤー」であるかどうかを考えることが大事なポイントとなります。
コードで言うと以下のようなコードで生成されたGameObjectインスタンス)のことです。(サーバ側のコードとして実行してやる必要があります)

var player = Instantiate(_playerPrefab);
NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);

ためしにプレイヤーキャラと敵キャラの2体だけが存在しているところをイメージしてください。

明らかに「敵キャラ」は「プレイヤー」ではありませんね。
敵キャラ(GameObject)にはきっとEnemy.csのようなスクリプトがアタッチされていると思います。

この場合のEnemy.csコード内で実行している処理は「プレイヤー」ではないコードとなります。

敵キャラなどは以下のようなコードで生成、同期されます。

GameObject obj = Instantiate(anyPrefab);
NetworkServer.Spawn(obj);

一方、プレイヤーキャラの場合は前述のような(AddPlayerForConnectionメソッド)コードで生成されたインスタンスです。
そしてそのGameObjectにはPlayer.csのようなスクリプトがアタッチされていることでしょう。
このコード内で実行されている処理は当然、「プレイヤー」として実行されます。

さて、オフラインゲームであれば「プレイヤーキャラ」はひとつだけですが、オンラインゲームとなるとこの「プレイヤーキャラ」は接続しているユーザの数だけ存在することになります。
大体のオンラインゲームは、自分の分身となるアバター的なキャラクターを操作してゲームを進めていくと思います。

では、その「アバター的キャラクター」と、「他人が操作しているキャラクター」を分けないとどうなるでしょうか。
自分の操作した内容がすべてのキャラクターに伝わってしまったらゲームどころではありませんね。

つまり、「自分のキャラクター」を識別した上で、コントロール可能なキャラクターをひとつに絞らないとなりません。(例えすべてのキャラクターの見栄えが同じであっても)

そしてこの「自分のキャラクター」を表すのが「ローカルプレイヤー」という概念なのです。

なので、ローカルプレイヤーとして動いているゲームオブジェクトの操作は「権限」がある状態になっていて、UNETでは他キャラクターと異なる意味を持ちます。

権限 - Local Client Authorityについて

NetworkIdentityにはLocal Client Authorityというフラグがあります。 これは、ローカルクライアントが権限を持つ可能性があるオブジェクトに対して設定します。

具体例をあげると、プレイヤーが、ゲームワールドに落ちているオブジェクトを拾ってなにかアクションをする、というような場合に利用します。
通常、こうしたオブジェクトの挙動については、該当オブジェクトにスクリプトファイルをアタッチして制御すると思います。
仮にオフラインのゲームであれば「拾った」ことを検知して、プレイヤーの手にそれを持たせるなどの処理をかけば目的が達成できます。
(もちろん、ゲーム的に意味を付けるならもっと色んなことを書かないとダメですが)

しかし、オンラインゲームの場合、その「拾ったオブジェクト」はサーバおよび全クライアントで表示されています。
そしてその位置を動かした場合。誰の位置を信頼したらいいでしょうか。
サーバ側? でも、位置のアップデートはたった今拾ったプレイヤーの位置や行動によって変更されます。

すべての処理をサーバ側だけで行っているのであればサーバ側だけで判断して、該当プレイヤーの状態に応じて位置を更新してやれば動きます。
しかし、全プレイヤーの描画や姿勢制御などをサーバだけで行うには限界があります。
そのあたりはクライアントでやってほしいところですよね。

ここで、見出しになっている「権限」の話が出てきます。
つまり、該当のオブジェクトを「操作」する「権限」を「誰が持っているのか」を表すのが、LocalClientAuthority、というわけです。

そしてこのフラグがtrueになっているローカルプレイヤーの操作を信頼し、サーバはその状態を各クライアントに伝える、という形で位置同期が行われています。

また、下のほうの「サーバ・クライアント間でメッセージを送信する」でも触れますが、UNETでは「コマンド」という形でサーバに対してメッセージ(メソッド呼び出し)を行うことができます。
ただこの「コマンド」、今説明したLocalClientAuthorityがないプレイヤーが実行しても無視されてしまいます。

位置の同期だけでなく、そうした様々な実行権限を表しているわけですね。
コードにするとこんな感じ↓

[Command]
void CmdAssignAuthority(NetworkIdentity targetID, NetworkIdentity playerID)
{
    targetID.AssignClientAuthority(playerID.connectionToClient);
}

[Command]
void CmdRemoveAuthority(NetworkIdentity targetID, NetworkIdentity playerID)
{
    targetID.RemoveClientAuthority(playerID.connectionToClient);
}

基本的な概念はこのような流れで制御されていきます。
あとは、サーバ・クライアント間でのメッセージのやり取りや、プレイヤーの生成など実際のコードを書いていくフェーズになります。

以下は、実際に開発をする上でハマった点や役に立った点などをざっくばらんにまとめています。

その他、開発中に必要になったことのメモ

NetworkInstanceIdを使ってオブジェクトを特定する

基本的に、サーバ/クライアント間でオブジェクトを特定して処理をさせる場合、GameObjectの参照をそのまま渡すことはできません。
(よくよく考えれば当たり前ですが、サーバ/クライアント間ではメモリも共有されていませんし、参照を渡したところでなんの意味もありません)

そこで利用するのがNetworkInstanceIdです。

NetworkInstanceId は、サーバ/クライアント間で同期する際にオブジェクトを識別するためのIDを表すクラスです。 これを利用して、同期対象となるオブジェクトを特定し、オブジェクトに対して色々と処理を行うことができます。

サーバとクライアントで利用するクラスが異なる

この NetworkInstanceId ですが、サーバ側とクライアント側で利用する方法が異なります。
サーバ側で処理を行う場合は以下のようにします。

// サーバ側
GameObject target = NetworkServer.FindLocalObject(netId);
クライアント側で処理を行う場合は以下のようにします。

// クライアント側
GameObject target = ClientScene.FindLocalObject(netId);

あとは、取得したGameObjectに対して通常時と同じように処理をしてやればOKです。

ただひとつ注意点として、ここで行った処理はあくまで「そのクライアント上でのみ」実行されるという点です。
つまり、処理は同期されません。同期させたい場合はRPCを使って処理する必要があります。

変数の同期

UNETにはフィールドの値を同期する仕組みが用意されています。
同期したいフィールドにSyncVarアトリビュートをつけることで、サーバを経由して値を同期することができます。
ただしいくつか制限があり、通常のプリミティブ型か Transform や Quaternion などの特定の型のみしか同期することができません。 当然、自分で定義したクラスなどは同期できないので注意が必要です。

プレイヤーオブジェクトの取得

シーンに配置されたオブジェクトのうち、プレイヤー(UNET上では若干特殊なものとして扱われる、いわゆる操作可能なプレイヤーオブジェクトのこと)を取得する場合は以下のようにすることで取得できます。

foreach(var player in ClientScene.localPlayers)
{
    // クライアントシーンに存在するローカルプレイヤーのうち、操作に必要なものを取り出して処理
}

ここで、 ClientScene はサーバと区別されている、いわゆるクライアント上のシーンに属するプレイヤーオブジェクトを取り出すことができます。 (localPlayers になっているのは、複数生成することが可能?)

その他の、サーバで管理されているオブジェクトの取得

上記はプレイヤーオブジェクトの取得でした。 今度は、プレイヤーオブジェクト以外の、サーバで管理されているオブジェクトの取得です。(Spawnしたやつ)

foreach(var dict in ClientScene.objects)
{
    // サーバで管理されているオブジェクトの辞書。
    // Dictionary<NetworkInstanceId, NetworkIdentity>型のオブジェクトで、サーバで生成された、プレイヤー以外のオブジェクトが格納されている
}

NetworkBehaviourで利用できるコールバック

  • OnStartServer
  • OnStartClient
  • OnSerialize
  • OnDeSerialize
  • OnNetworkDestroy
  • OnStartLocalPlayer
  • OnRebuildObservers
  • OnSetLocalVisibility
  • OnCheckObserver

手動でプレイヤーオブジェクトを生成、登録する

色々調べてもなかなか情報が出てこず、だいぶ苦戦しました・・。 が、実に簡単なことでした。 (なんかコネクション張ってそのあとになにしてかにして、みたいのを想像していたので・・)

void Update()
{
    if (!IsClientConnected())
    {
        return;
    }
    
    // サーバに追加を通知し、成功したら `true` が返る
    if (ClientScene.AddPlayer(0))
    {
        Debug.Log("Success to add a player.");
        
        // ローカルプレイヤーの情報にアクセス。
        // ただし、サーバとの通信が挟まるため、非同期で`gameObject`が設定されるため、
        // すぐにアクセスするとNullになるので注意。
        ClientScene.localPlayers[0].gameObject.transform.position = new Vector3(0, 10f, 0);
    }
}

プレイヤーの生成(スポーン)を出し分ける

オンラインゲームは、ほとんどの場合、プレイヤーの見た目や挙動は、すべてのプレイヤーで同一、というケースは稀でしょう。 むしろ、すべてのプレイヤーは異なっているのが普通だと思います。

UNETは色々なものを抽象化してくれていますが、それが故に、普通にチュートリアルを読んでセットアップしただけでは、決まったプレイヤーのPrefabしかスポーンさせることはできません。

そこで、以下のようにすることで、任意のクラス(やPrefab)を用いてプレイヤーを生成することが可能となります。

// Prefabを複数用意しておく
[Header("プレイヤー1のPrefab"), SerializeField]
GameObject _player1Prefab;

[Header("プレイヤー2のPrefab"), SerializeField]
GameObject _player2Prefab;

// ...中略...

// 1 = player1, 2 = player2
var message = new IntegerMessage(isPlayer1 ? 1 : 2);
if (!ClientScene.AddPlayer(ClientScene.readyConnection, 0, message))
{
    Debug.Log("Failed to add a player.");
    return;
}

// .. 中略...

// クライアント側から「プレイヤー」の追加要求があった場合に実行される
public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader reader)
{
    Debug.Log("[NETWORK] OnServerAddPlayer with message.");

    var message = reader.ReadMessage<IntegerMessage>();
    var player = message.value == 1 ? Instantiate<GameObject>(_player1Prefab) : Instantiate<GameObject>(_player2Prefab);
    NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);
}

重要なポイントは2点です。

ひとつめは、クライアント側でpublic static bool AddPlayer(Networking.NetworkConnection readyConn, short playerControllerId, Networking.MessageBase extraMessage);を利用してサーバに要求を送ります。

このメソッドは、現在張られているコネクションを用いて、コントロールIDおよび付加情報を持ってサーバにリクエストを送ります。
このとき、extraMessageに、どのプレイヤータイプを生成すればいいかを格納してリクエストを送っているわけです。
ちなみに、上記サンプルではプレイヤーの種類が2タイプなので 1 か 2 をメッセージとして送っている簡単な例です。
もちろん、 Integer 以外にもメッセージとして利用できますし、独自のクラスを定義してそれを利用することも可能になっています。

そしてふたつめ。
ふたつめはサーバ側のハンドリングです。
クライアントからメッセージ付きでリクエストが来た場合はpublic override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader reader)が呼ばれます。

OnServerAddPlayerメソッドはオーバーロードがあるので注意が必要です。
メッセージ付きの場合は上記のメソッドをオーバーライドして実装する必要があります。

※ 余談ですが、これらのメソッドは「オーバーライド」しないとダメっていうのに最初ハマりました。StartやUpdateはオーバーライドじゃないのに、仕組みが微妙に異なると混乱する( ;´Д`)

さて、これでメッセージに応じて適切にプレイヤーを生成することができました。 今回の例はとても簡単なパラメータのみですが、メッセージをもっと拡張することで、見た目だけではなく、生成時のパラメータの初期化などにも利用することができると思います。

サーバ・クライアント間でメッセージを送信する

ネットワーク対応したコンテンツであれば当然、クライアント・サーバ間でなにかしらのメッセージのやり取りを行いたいものです。 前述の「Command」アトリビュートは主に、各プレイヤーごとに定義された、まさに「コマンド」を実行するものです。

例えて言えば、マルチプレイで相互に接続された各プレイヤーごとの「戦う」などの(RPGと似た意味での)コマンドを実行するもの、と考えると分かりやすいかもしれません。 なので各プレイヤー(ローカルプレイヤー)の振る舞いを実行するためのものが「コマンド」です。

しかし、例えばプレイヤーがなにかしらの行動を行い、例えば「穴に落ちた」など、「コマンド」というよりは「結果」や「アクション」に近いものは、前述のコマンドでは実行できません。 そうした場合に使えるのが、ネットワーク間で通信が行える「Unity - Manual: Network Messages」です。

メッセージを受け取るハンドラを設定する

クライアント・サーバ間でメッセージをやり取りするには、そのメッセージの受信を検知できるようハンドラを登録する必要があります。 (イメージ的には普通のイベントの登録と似たものです)

通常、C#event を用いる場合はデリゲートの宣言とそれにメソッドを追加する形で設定していくのが普通かと思います。 しかし、UNETのクライアント・サーバ間でのやり取りはそれとは異なり、メッセージタイプを表すID(short型の値)を指定してメッセージを送信する方法を取ります。 (ちょうど、ポート番号を指定してコネクションを張ってやり取りするようなイメージでしょうか)

ちなみにメッセージタイプは予約されている番号がいくつかあり、独自のメッセージをやり取りする場合はそれを避けて定義する必要があります。

docs.unity3d.com

基本はピンポン?

なにがしかのイベントがあり、それをすべてのクライアントに伝えたい場合は多いでしょう。 その場合、クライアント側だけで処理をしてしまっても、それはローカルの状態が変わっただけでオンライン上の他のユーザへは反映されません。 現在接続しているユーザすべてに状態を反映させるためには、サーバ側から全クライアントに対してRPCを利用して命令を飛ばしてやる必要があります。

そのため、とあるクライアントで起きたイベントをいったんサーバに通知し、それを持って全クライアントに改めて通知してもらう、という方法を取る必要があります。

    void InvokeEvent()
    {
        if (hasAuthority)
        {
            CmdInvokeEvent();
        }
        else
        {
            GameManager.Instance.GetAuthority(gameObject, () =>
            {
                Debug.Log("in callback.");
                CmdInvokeEvent();
            });
        }
    }

    [ClientRpc]
    void RpcInvokeEvent()
    {
        _event.Invoke();
    }

    [Command]
    void CmdInvokeEvent()
    {
        //_event.Invoke();

        RpcInvokeEvent();
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            if (GameManager.Instance.IsLocalPlayer(other.gameObject))
            {
                InvokeEvent();
            }
        }
    }

手動でNetworkManagerHUDと同様の処理を行う

NetworkManagerHUDを利用すると、マネージャクラスを使って簡単にサーバ起動、クライアントのコネクションを行ってくれます。 が、細かい処理を行おうとするとこのあたりも自分で実装するケースが出てきそうです。 その場合には自分でNetworkManagerHUD相当のことをしないとなりません。 それについてはこちらの記事にコード例を載せてくれていたので紹介します。

なお、簡単にホストの起動とクライアント接続をするのみならば、以下のようにすることで対応可能です。

        void OnGUI()
        {
            if (!IsClientConnected())
            {
                RenderSelectUNET();
                return;
            }

            if (NetworkClient.active && !ClientScene.ready)
            {
                ClientScene.Ready(client.connection);
                return;
            }


            if (_isSelected)
            {
                return;
            }

            RenderSelectPlayerType();
        }

        private bool _isConnected = false;
        private NetworkClient _client;

        void RenderSelectUNET()
        {
            int btnWidth = 80;
            int btnHeight = 20;
            int left = 10;
            int top = 10;

            if (GUI.Button(new Rect(left, top, btnWidth, btnHeight), "LAN Host"))
            {
                StartHost();
                client.RegisterHandler(MsgType.Connect, OnConnected);
                //string address = networkAddress == "localhost" ? "127.0.0.1" : networkAddress;
                //NetworkServer.Listen(address, networkPort);

                //_client = new NetworkClient();
                //_client.RegisterHandler(MsgType.Connect, OnConnected);
                //_client.Connect(address, networkPort);
            }
        }

大事な点は、NetworkClientで接続を行い、ClientScene.Readyを読んで、クライアントサイドの準備が完了したことを通知する点です。

その他メモ

同期を滑らかにする

Movement Thresholdを小さくすることで、小さな動きでも同期をかけてくれるため滑らかになります。(まだ検証していませんが、その分同期処理が多く走ることになるので負荷はあがると思います)

Interpolate Movement Factorを小さくすることで補完をかける閾値を変えることができます。つまりそれだけ多く補完をかけることになるのでより滑らかになります。

f:id:edo_m18:20170203111446p:plain

uGUIのEventSystemを理解する

概要

Unity4.6から導入されたEventSystem。 調べてみると色々と学びがあるのでメモとして残しておきます。 (若干、今さら感がありますが)

まず最初に大事な概念として、このシステムはいくつかのクラスが密接に連携しながらイベントを処理していく形になっています。アーキテクチャ的にも学びがある実装になっています。

登場人物

このシステムを構成する、必要な登場人物は以下にあげるクラスたちです。

  • BaseInputModule
  • BaseRaycaster
  • ExecuteEvents
  • PointerEventData
  • RaycastResult

すごくおおざっぱに言うと。

BaseInputModuleが全体を把握し、適切にイベントを起こす、まさに「インプット」を管理するクラスとなります。

まず、BaseRaycasterによって対象オブジェクトを収集します。(複数のRaycasterクラスがある場合はすべて実行して、対象となるオブジェクトをかき集めるイメージ) そして得られたオブジェクトに対してどういう状態なのかを判断します。例えば、ホバーされているのか、ホバーが解除されたのか、あるいはクリックされたのか、などなど。

そしてその判断された状態に応じて、ExecuteEventsを利用してイベントを送出する、というのが全体的な流れになります。

カスタムする

Baseと名前がついていることから分かる通り、これらを適切に継承・使用することで、独自の仕組みで対象オブジェクトを決定し、独自のイベントを伝播させる、ということも可能になります。

BaseInputModuleを継承したカスタムクラス

BaseInputModule を継承したカスタムクラスを作成することで、独自のイベントを定義することができます。そもそも冒頭で書いたように、このクラス内でオブジェクトの収集を行い、イベントの状態管理をするのが目的なのでそれを行うためのクラスとなります。

なお、 BaseInputModuleUIBehaviour -> MonoBehaviour を継承しているクラスのため、GameObjectにアタッチすることができるようになっています。

※ ちなみに、シーン内でアクティブなInputModuleはひとつだけと想定されています。

Processメソッドのオーバーライド必須

BaseInputModule には Process メソッドがabstractで定義されており、これのオーバーライドは必須となっています。この Process メソッドは、BaseInputModule 内で自動的に呼ばれ、Updateと似たような形で毎フレームごとに呼ばれるメソッドとなっています。

なので、作成したカスタムクラスを適当なGameObjectにアタッチし、Process メソッド内に処理を書くと毎フレーム呼ばれるのが確認できると思います。

BaseRaycasterを継承したカスタムクラス

BaseRaycasterを継承したカスタムクラスを作成することで、収集の対象とするオブジェクトを独自で定義することができます。このベースクラスのRaycastメソッドとeventCameraプロパティはabstractで宣言されており、派生クラスではoverride必須となっています。

Raycastメソッドのオーバーライド

BaseRaycasterRaycastメソッドをオーバーライドすることで、EventSystem.RaycastAllメソッド実行時に収集対象が収集されます。

RaycastAllメソッドのシグネチャは以下のようになっています。

public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults);

引数として、ポインターイベントのデータオブジェクトと、Raycast結果を格納するList<RaycastResult>型のオブジェクトを渡します。このRaycastAllメソッド内で、BaseRaycasterを継承したクラスのRaycastが順次呼び出されるので、その処理の中でヒットしたオブジェクトをリストに追加します。

最後に、全RaycasterのRaycast結果を元にして、対象オブジェクトにイベントを送出します。

Raycastメソッドで対象オブジェクトを収集する

Raycastと名がついていますが、別にRaycastを必ず実行しないとならないわけではありません。
あくまで、Raycastが判断として使われているためについている名前でしょう。つまり、このメソッド内で適当なオブジェクトを結果リストに入れてあげれば、Raycastをしていなくともそれが「ヒット候補」として送られることになります。

RaycastResult

RaycastResultオブジェクトは、Raycastした結果を保持するオブジェクトです。Raycasterによって収集されたオブジェクトの結果を保持するもの、と考えるといいと思います。

BaseRaycasterクラスを継承したサブクラスのRaycast実行時に、結果を保持する際に使用します。

ライフサイクル

全体の簡単なライフサイクルを見てみましょう。

  • BaseInputModule.Process
  • EventSystem.RaycastAll
  • 各BaseRaycasterを継承したクラスのRaycast
  • ExecuteEventsを利用してイベントを伝達

大まかに言えば、上のようなライフサイクルでイベントを実行していきます。以下に、動作原理を理解するためだけの、とてもシンプルなコード例を載せておきます。

InputModuleサンプル

最初はInputModuleのサンプルです。
Processメソッド内で対象となるオブジェクトを集めます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputModule : BaseInputModule
{
    private List<RaycastResult> _result = new List<RaycastResult>();
    private PointerEventData _eventData;

    private void Awake()
    {
        _eventData = new PointerEventData(eventSystem);
    }

    public override void Process()
    {
        _result.Clear();
        eventSystem.RaycastAll(_eventData, _result);
        Debug.Log(_result[0].gameObject);
    }
}

PointerEventDataは、UIを操作するための「ポインタ」の位置や状態などを保持するデータです。
このデータを元に、Raycasterは対象オブジェクトがどれかを識別します。

なので、実際にはRaycastAllメソッドに渡す前に、適切に「現在の」ポインタの状態にアップデートする必要があります。が、今回はサンプルなので生成するだけに留めています。

Raycasterサンプル

次に、InputModule内で実行されるRaycastです。実際にはeventSystem.RaycastAllを通じて間接的に実行されます。

このメソッドの第二引数に渡しているList<RaycastResult>型のリストが、結果を保持するために使用されていることに注目してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Raycaster : BaseRaycaster
{
    [SerializeField]
    private GameObject _obj1;

    [SerializeField]
    private GameObject _obj2;

    public override Camera eventCamera
    {
        get
        {
            return Camera.main;
        }
    }

    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        RaycastResult result1 = new RaycastResult()
        {
            gameObject = _obj1,
            module = this,
            worldPosition = _obj1.transform.position,
        };

        RaycastResult result2 = new RaycastResult()
        {
            gameObject = _obj2,
            module = this,
            worldPosition = _obj2.transform.position,
        };

        eventData.pointerCurrentRaycast = result2;

        resultAppendList.Add(result1);
        resultAppendList.Add(result2);
    }
}

Raycastメソッドには、ポインタの状態としてPointerEventDataが、また結果を保持するためのリストとしてList<RaycastResult>型のリストが渡ってきます。

この第二引数のリストが、上のInputModule内で渡したリストになります。これでなんとなく関係が見えてきたのではないでしょうか。

あとは、レイキャストを実行するなりして「対象オブジェクト」を識別します。今回は動作原理をわかりやすくするため、レイキャストなどは行わず、予め登録されたオブジェクトをそのままリストに追加するだけの処理にしています。

実際にはここで、レイキャストや、その他必要なチェックを経て、実際のオブジェクトを選出することになります。

まとめ

InputModuleが全体のインプットを管理し、イベントシステムに対して適切にRaycastを実行させ、収集したオブジェクト(RaycastResult)を利用して各イベントの伝搬を行う過程がなんとなくわかったかと思います。

InputModuleは、RaycastResultのデータから各オブジェクトの状態(距離など)を使って、そのオブジェクトがホバー状態なのか、それが解除されたのか、などなどを判断し、またそう判断されたオブジェクトに対してはExecuteEventsを使ってイベントを伝達します。

オブジェクトを集める、判断する、イベントを伝える、というのが、いくつかのクラスが密接に連携しながら、かつ拡張性高く実装されているのが分かってもらえたかと思います。

VR向けにuGUIを拡張したりしているので、機会があったらそれも書きたいと思います。

ComputeShaderを触ってみる その1 ~スレッド編~

概要

並列化可能な、膨大な数の計算を行う場合はCompute Shaderの出番です。
今回はこの「Compute Shader」を触ってみたのでそのメモです。

Compute Shaderの最小単位

Compute Shaderを利用する場合、まずは.computeファイルを作成します。
そして作成したCompute Shaderに以下を記述すると最小の構成となります。

#pragma kernel CSMain

[numthreads(4, 1, 1)]
void CSMain()
{
    // do nothing.
}

このCompute Shaderはなにもしてくれませんが、構成がどうなっているかを知るには十分です。
まず、#pragma kernel CSMainで「CSMain」関数がカーネル*1であることを伝えます。
当然ですが、シェーダ内には任意の関数を定義することができます。
その他の言語の関数と何ら変わらず、関数呼び出しを通して計算を行うことができます。

その中で、カーネルとして動作する関数を#pragma kernelで指定している、というわけです。

スレッド

さて、Compute Shaderは(当然ですが)GPU上で動作します。
そしてGPUと言えば並列処理が得意ですよね。
そのGPUに並列処理をしてCPUの代わりに計算を行ってもらうのがCompute Shaderです。

そしてその並列処理を最大限活用するのが「スレッド」です。

スレッド数は[numthreads(x,y,z)]で指定します。
ここで指定した数だけスレッドが作られ、それぞれがカーネルのコピーとして動作します。

冒頭の例では[numthreads(4,1,1)]となっているので全部で4スレッドとなります。

スレッドと次元

なぜ(4,1,1)なのに4スレッドなのか、と思われたかもしれません。

これは、多次元配列としてスレッド数を指定しているから、というのが理由になります。
つまり、X軸に4つ、Y軸に1つ、そしてZ軸に1つの配列をイメージしてください。
Y,Zともに1なので、実際はX軸のみ、1次元の配列になりますよね。

なので合計で4スレッドになる、というわけです。

試しに[numthreads(4,4,1)]と指定すると、X,Y軸が4つずつ、Z軸が1つの、4 * 4 = 16スレッドが生成されることになります。

なぜこんなまどろっこしい方法でスレッド数を指定するのか、というと、GPUが得意としているテクスチャへのアクセスなどが行いやすいから、というのが自分の理解です。
どういうことかというと、テクスチャは2次元で表されます。幅と高さですね。

そしてテクセルをそれぞれ二次元配列として表した場合、左上を(0, 0)、その右隣を(1, 0)などと表現することができます。

こうした、二次元配列へのアクセスには、ふたつの添字があると便利ですよね。
そして複数次元でスレッドを表すことで、このあと説明する「スレッドグループ」とあいまって、とても簡単に、各テクセルへアクセスすることが可能となります。

要は、二次元(ないし三次元)の要素を扱うのだから、それに応じたスレッドの定義をしておいたほうがいいよね、くらいの理解でいいと思います。(というか自分はそんな程度の理解ですw)

スレッドグループ

さて、スレッドの数については前述の通りです。
これに加えて、さらに多次元の「スレッドグループ」というものがあります。

最初はなんのこっちゃな感じですが、まずはC#側のコードを見てみましょう。

// Get kernel ID by string. Just example below.
int kernel = shader.FindKernel("CSMain");
shader.Dispatch(kernel, 1, 1, 1);

コンピュートシェーダを起動するC#コードは上記のような感じになります。
shaderには、インスペクタなどから.computeファイルを指定するなどして参照を保持しておきます。

そしてFindKernelメソッドを利用して、カーネルIDを取得します。
実はカーネルは、ひとつのシェーダ内に複数記述することができ、上から出現順にIDが連番で振られていきます。
(なので、FindKernelメソッドを使わず、直接0などを指定しても動作します。サンプルのように、カーネルがひとつしかない場合は直接0を指定したほうが分かりやすいサンプルになるかもしれません)

通常は複数カーネルが存在し、かつ順番も(リファクタリングなどで)変化するかもしれないため、FindKernelメソッドで適切にIDを取得する形を取ります。

カーネルIDが取得できたら、バッファ(ComputeBuffer)や計算に使う値などを適切にセットアップした上で、Dispatchメソッドを使ってコンピュートシェーダを起動します。

Dispatchメソッドの引数は全部で4つ。

第一引数は、起動するカーネルIDを指定します。
残りの3つは、前述した「スレッドグループ数」を指定するものになります。

上の例ではX,Y,Zそれぞれに1を指定しています。
こちらもスレッド数と同じ考え方なので、結果としてグループはひとつだけ指定したことになります。
そして1グループ、4スレッド、つまり合計4スレッドが一度のDispatchメソッドで実行される、というわけです。

Compute Shaderを利用した計算を行う場合、このスレッドとスレッドグループの概念は必須で覚えないとなりません。
というのも、計算が並列で行われるため、どのスレッドが今実行中なのか、その位置を把握しながらコードを書かないとならないためです。

特に、バッファなどは一次元配列の形でデータを格納するため、「どの場所に計算結果を格納するか」はスレッド番号などから判断しないとなりません。

とはいえ、最初はどういうことかすぐに理解するのはむずかしいと思います。
MSDNにスレッドグループを説明する画像があったので引用します。

f:id:edo_m18:20170502103117p:plain

スレッドIDを取り出す

上の図を見て「あーなるほどね」となったらここは読み飛ばしてもらってOKです。
最初見たときは、さっと眺めただけではよく分かりませんでした。

が、落ち着いて考えればそこまでむずかしい話ではありません。

まず、図で説明されているのは[numthreads(10,8,3)]で合計240スレッドが指定されたコンピュートシェーダに対して、Dispatch(5,3,2)で合計30グループのスレッドグループからなるスレッド群が起動するところを説明しています。(つまり合計7,200スレッド)

図の最初のテーブルは「スレッドグループ」を表しています。
(5,3,2)なので、X軸に5、Y軸に3、Z軸に2の三次元のテーブルとして表現されています。

そして次のテーブルは、(2,1,0)グループのスレッド詳細をクローズアップしています。
240スレッドが全グループそれぞれで実行されているので、ひとつのグループの詳細を見てみると、またさらに三次元のスレッド群で構成されている、という入れ子構造になっているわけですね。

下のテーブルでは今度は(7,5,0)の位置にあるスレッドについて述べています。

実は、コンピュートシェーダのカーネルへの引数は、通常のシェーダと同様、セマンティクスを用いて必要なデータをシステムから渡してもらうことができます。
そのセマンティクスが、図の下に書かれているSV_GroupThreadIDSV_GroupIDSV_DispatchThreadIDSV_GroupIndexとなります。

今着目しているスレッドが実行される際に、それぞれのセマンティクスを指定した引数にどんな値が渡ってくるか、を示しているのが図の意味なんですね。

SV_GroupThreadIDは、実行しているグループ内でのスレッドIDです。なのでそのまま(7,5,0)なわけですね。
SV_GroupIDは、スレッド群を管理しているスレッドグループのIDなので(2,1,0)となります。

そして若干厄介なのがSV_DispatchThreadIDでしょう。
これは、Dispatchと名前がついている通り、現在実行中のスレッドIDを一意に特定できる情報が格納されます。
つまり、この値を参照すれば、どのスレッドグループのどのスレッドが今実行されているのかが分かる、というわけです。

そして図が説明してくれているのはこのIDの計算方法です。
計算方法を抜粋すると以下のようになっています。

([(2,1,0) * (10,8,3)] + (7,5,0)) = (27,13,0)

つまり、カーネルの引数には(27,13,0)という値が渡ってくるというわけです。(ちなみに引数の型はint3

上の例では3次元なので若干むずかしいですが、簡単のために2 * 2次元のスレッドグループの、4 * 4スレッドを実行した例の図を作成してみました。
以下の図を見てみてください。

f:id:edo_m18:20170509160736p:plain

上の図は、左から順に、「スレッドグループ番号」「スレッドグループごとのスレッドID」「Dispatch Thread ID」の順に、どういう値が割り振られるかを示したものです。
上段にある拡大図は、SV_DispatchThreadIDがどう計算されるかのサンプルを示したものです。

最後の「SV_DispatchThreadID」を見てもらうと分かりますが、うまくX,Y軸の値が連続して並んでいますね。

1行のスレッド数は、「X軸のスレッド数 x X軸のスレッドグループ」となります。
それが、「Y軸のスレッド数 x Y軸のスレッドグループ」の数だけ繰り返されるわけです。

あれ? これを見てなにかに似ていると思わないでしょうか。

そう、テクスチャを二次元配列にしたときの添字と一致しているのが分かるかと思います。
これが、冒頭で書いた「テクスチャなどの多次元配列へのアクセスを考慮したものだ」という話につながります。

スレッド数とスレッドグループ数を多次元で表現すると、こうしたデータへのアクセスがとても容易になるのが分かってもらえたかと思います。

詳細は次回にまわしますが、テクスチャへアクセスするためのコード断片をお見せしましょう。

#pragma kernel CSMain

RWTexture2D<float4> texCopy;
Texture2D<float4> tex;

[numthreads(8,8,1)]
void CSMain(uint2 id : SV_DispatchThreadID)
{
    float4 t = tex[id];
    texCopy[id] = t;
}

今は細かいところは置いておいて、カーネル関数の中身だけに着目してください。
テクスチャへの添字アクセスで、SV_DispatchThreadIDをそのまま使っていることに気づいたでしょうか。

前述のように、SV_DispatchThreadIDは、うまく二次元配列の添字として機能してくれるので、値を加工することなくそのまま使えている、というわけですね。

今回は、ComputeShaderを使う上で大事な「スレッドの概念」についてまとめました。
ComputeShaderを使ってテクスチャを利用して様々な計算が行えるので、次はテクスチャとバッファを利用した計算について書きたいと思います。

*1:カーネルは「核」を意味するので、計算の核となる関数のこと