うどんゲームメモ

ゲームの技術メモをまったりと

ShaderGraphを使って頂点シェーダーで遊ぶ

この記事はUnityゆるふわサマーアドベントカレンダー 2018の22日目の記事になります
21日目の記事は@am1tanakaさんのNavMeshAgentでよい感じにキャラクターを歩かせるでした!

qiita.com

Unity2018.2に付随するShaderGraphのアップデートとしてついにvertex shaderの適応が可能になり、頂点情報がいじれるようになりました
今回は早速その機能を使い、簡単な頂点シェーダーをShaderGraphで作って遊んでみたいと思います


ShaderGraph

もうすでに多くの人が知っているかと思いますがShaderGraphはUnity2018から導入されたノードベースのシェーダーエディタです
従来のUnityではシェーダーを実装するにあたってShaderLabによるコーディングが必要でしたが、これによってビジュアルベースでシェーダーを構築できるようになりました
UEは勿論のこと、MayaやSubstanceBlenderなどのDCCツールを活用していた方などにとっては喜びの声が大きいものでした。最初はノードの種類も少なく表現の幅や使い心地に難があったのですが、様々な改修があり、Unity2018.2ではバージョンは3.0となりました。今後も期待ができる機能の一つです

使ってみる

f:id:myudon:20180822002929p:plain

使い方は簡単で、マスターノードに追加されたPositionに頂点情報となるVectorをつなぐだけです

f:id:myudon:20180822002202p:plain

まずは簡単な例です、ワールド座標に対して時間変数の正弦余弦を加算させてぐるぐるさせます

f:id:myudon:20180822020604g:plain

ボールがぐるぐる動いてますね、もうちょっと遊んでみます

qiita.com

上記のサイトにかなり簡易的な草の動きを実装した頂点シェーダーがあるので、ShaderGraphで組んでみます

f:id:myudon:20180822015303p:plain

草が風でそよそよする感じの動きですね
頂点情報について、UV座標で重みをつけたアニメーションを行います
頂点情報をいじっている部分をピックアップしてみましょう

f:id:myudon:20180822223821p:plain

ノードとしてはUVからパラメータを引っ張ってきて、定義したプロパティノードから各種必要な定数を引っ張ってくる感じです
UVから引っ張ってきたy値と定数パラメータを用いて頂点のx成分の移動値を作成し、座標に加算しています

これを適当な板に貼り付けて動かしてみるとこうなります、寂しいので3つぐらい並べてみました

f:id:myudon:20180822020625g:plain

時間に合わせて草が揺れるようになりました
手軽にササっと作れて、動きとしてアウトプットを確認できるので学びとして楽しいですね
今回は単純な動きを作成しましたが、物理挙動に紐づく波の動きだったり動的な凹み表現だったり遊べるものは多いので興味が湧いた方は遊んでみてください


いかがでしたでしょうか、少し緩めの記事ですが、シェーダーって触ったことないけどShaderGraphちょっと面白そう!などちょっとしたきっかけになればいいかなと思っています
個人的にShaderGraphでかなりシェーダーの技術的な敷居は落ちたかなーと感じているので、これを機にエンジニアは勿論、クリエイターからの表現のアプローチが増えていくことを期待しています、自分もいろいろ遊んでいる最中なので面白いものがあったら共有していきたいですね

Unity新Prefabワークフロー、Prefab Variantsの紹介

この記事はUnityゆるふわサマーアドベントカレンダー 2018の13日目の記事になります
12日目の記事は@sonoichi-60さんのリフレクションについてまとめたでした!

qiita.com

Nested Prefabの標準搭載など、Unity BerlinにてPrefab周りについてのワークフローが改修が発表されました
機能としては2018.3に搭載予定らしく、プレビュービルドが公開されています

unity3d.com

今回はそのPrefabシステムの一つ、Prefab Variantsについての記事です


Prefab Variants

Prefab variantsは所謂プレハブの継承的なシステムです
あるPrefabからのパラメータ継承と、要素のオーバーライドを実現することが出来ます
ベースプレハブの変更に伴う派生先の更新や、複数のPrefab継承先なども可能で、Unityにおける開発フローで効率を上げることができます
UnityがPrefabシステムのマニュアルを公開しているので、興味がわいた方は下記のリンクから読んでみてください

docs.google.com

実践

使う場合は上記のプレビュービルドのリンクからプレビュー版を落としてきてください

まずベースプレハブを作成します、今回は適当にUIプレハブを作成しベースにします
プレハブ上で右クリックし、Create→Prefab Variantsで作成できます、作成したプレハブは矢印の模様がついたプレハブアイコンになります

f:id:myudon:20180805043806p:plain

作成したPrefab Variantsは同じようにシーン上で自由に設定ができます
ベースとの差分などはプラスアイコンなどで表示され、無論variantとしての保存ももちろんメニューからそれをベースに反映することもできます

f:id:myudon:20180805044321p:plain

ベース側が変更をかけた場合もしっかりvariantへと反映されます、上記のスクショみたいに差分でApplyを行うのですが、これまでみたいに変更を一括でインスペクタから反映させることもできます
その場合はApplyの時に変更点が出るようになったので確認しましょう

f:id:myudon:20180805045225p:plain

違う派生元からのベース修正もしっかりと反映して運用できます、逆に言えば様々なプレハブへの変更なども考慮しないといけないので使い方に注意は必要になってきます
派生先でNested Prefabなど複雑になってくると事故が起きるかもですね

また、ここまでのプレハブの編集について、シーン上でコンポーネントなどをいじることも可能ですが基本的にはPrefab Modeでの編集をすることになります
シーン上でプレハブの関係を壊すような操作は許可されていません、削除などの操作を試みようとするとUnityに怒られます

f:id:myudon:20180805051322p:plain

プレハブをOpenする操作を行えばPrefab Modeでプレハブを開いてくれます
Prefab ModeではPrefabの関係操作や更新を行うことが出来ます、Nested Prefabの場合はその個々のプレハブについて編集画面にジャンプすることが出来ます

f:id:myudon:20180805052516p:plain

Nested Prefabで差分を出した場合は反映先を選択することが出来ます、複雑化してくると管理が難しいかもですが操作の意味合いが明示化されていて便利ですね

f:id:myudon:20180805052710p:plain

また普通にオブジェクトとしてBreak Prefab Instanceして使いたいなってときはUnpack Prefabを使用しましょう
UnpackだけだとNested Prefabは無視してプレハブ接続を解除、Completelyのオプション付き操作を行うことで全てのプレハブ接続を解除します

f:id:myudon:20180805053114p:plain

ざっと現状のPrefab Variantsの機能を説明しました、詳しくは公式Manualを見ながら自分で触ってみるといいでしょう


Nested Prefabの公式導入、Prefab Variants、Prefab Modeなど触っていて大分ワークフロー変わったなぁと感じました
便利なのですが、しっかりと操作や効率化を念頭に入れて学習していかないとなって感じです
今後も変更が入っていくと考えられるのでこの先の動向に注目です

IncrementalCompiler時代のUnityC#Tips(C#7.2)

この記事はUnityゆるふわサマーアドベントカレンダー 2018の6日目の記事になります
5日目の記事は@splas_boomelangさんのUnity用のパッケージ(アセット)を配布する時のお作法でした!

qiita.com

Unity2018.1以降からIncrementalCompilerがPreview版として公開されるようになりました
まだUnity開発界隈では6.0の機能を徐々に会社の実プロダクトレベルで実験的に活用し始めたぐらいのフェーズかなと思いますが(async/awaitがじわじわと広まりだしたぐらいかな?) 、IncrementalComplierを導入することでC#7.2までの機能を使用することができます
6.0のasync/awaitレベルのUnity開発に対する革新的な変更はないため、そんなにインパクトがあるわけではないのですがちょっとしたTipsとしてUnity開発に使えそうなものをちょこちょこっと紹介します


IncrementalCompiler

まずIncrementalCompilerについて軽くお話します
IncrementalCompilerとは2014年よりMicrosoftオープンソース化した次世代コンパイラ「Roslyn」をUnity内で用いることでコンパイル時間の削減とC#7.2までの機能の使用を可能にするものです
上述にもあったようにまだPreview版でバージョンも0.0.42-preview,16と試験段階かなと言わざるを得ない状態です、これにより今のところ運用を見送っているプロジェクト等もあるのではないでしょうか
自分が個人規模の開発で使用してみた感じは特に問題は起きていないため、使用を続けています、その内Unityのアップグレードに従って標準環境になっていくことは見えているので早いうちから慣れておくのは悪くない選択肢かなと

Unity Incremental C# Compiler - Unity Forum

Tips

それではIncrementalCompiler導入によって使用できるC#機能の紹介です

タプル

タプルとは簡潔に説明すると複数のオブジェクトを1つのオブジェクトとしてまとめたものです 元々.Net4で導入されていたもので、パッケージとして導入し使用することは出来たのですがC#7よりC#の正式な機能として導入されるようになりました
Tuple<T1, T2>みたいな感じでジェネリクス保有する型を定義して使用します
複数の値を戻り値として受け取れるため便利な機能の一つです、自分はよく使用しています
.NetのTupleだとアクセサが固定でitem1といった抽象名でしか扱えずコードが見づらくなりがちでしたが、C#では名前を定義して使うことができます

// タプルの定義
(int age, string name) tuple = (10, "udon");

// タプル値の参照
Debug.Log(tuple.age); //10
Debug.Log(tuple.name);

// タプルで返す
(bool success, int result) Sum(int x, int y)
{
    if(x > 100 || y > 100)
    {
        return (false, -1);
    }
    else
    {
        return (true, x + y);
    }
} 

またタプルで受け取った複数のオブジェクトに対してそれらを分解する機構も作られています
上記のSumのように定義したタプルを変数宣言することなく個々の戻り値を使えるわけですね、タプルを使用するケースではタプル自体に命名する意義がない場合が多いので嬉しい配慮です

(var success, var sum) = Sum(100, 100);
Debug.Log(success); //true
Debug.Log(sum); //200

出力変数宣言

出力引数を受け取る際、事前に変数を宣言し受け取り口を作ってからそれを指定する必要がありましたが、C#7からは式の中で受け取り口の変数宣言を行うことが出来るようになりました
これに関しては劇的に使用するわけではありませんが、スマートに書けるので身に着けときましょう

Dictionary<string, string> hogeDictionary;

hogeDictionary.TryGetValue("hoge", out var value);
Debug.Log(value);

async T

async/awaitは確かに強力なソリューションの一つですが、そのネックの一つとして返り値がTaskに縛られてしまうというものがありました
特にTaskとしての機能が要らないようなケースでもasyncさせるためにはTaskとして宣言する必要があり割とパフォーマンス的な観点で見るとおやっ?ってなったり、シンプルな値返還の形を作るだけでも若干await操作がめんどくさかったりしました
C#7.0からは任意の型に対してasync宣言が行え、それをawaitで待機できるようになりました
ここで具体的な説明をしてしまうとそれだけで一つの記事になっちゃうかもなので詳しくは下記のリンク先のtask-likeについての項目を読んでみてください

非同期メソッド - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


ざっくり自分が使用したり、知識として抑えているものを紹介しました
この他にも様々な機能の追加がされているので、IncrementalCompilerを個人で試しに使ってみてください(特にUniRx6等はバリバリ使用しているので見てみるといいかも)

C#MessageBoxのようなダイアログをコルーチンみたいな感じで作ってみる

この記事はUnityゆるふわサマーアドベントカレンダー 2018の2日目の記事になります
1日目の記事は@splas_boomelangさんのOculusGoのコントローラーをUnity上でエミュレートするでした!

qiita.com

ゲーム開発を行う上で、思わぬ部分でネックになりやすいのがアウトゲームです
中でもゲーム中でもよく表示するダイアログ、ソーシャルゲームを開発する上で数えきれないほど実装することになると思います
どんな場面でも必要になり様々なロジックを担保するダイアログ、この設計が手を抜くと落とし穴(カオスコード)にはまりやすいです
今回はそのダイアログの作りの一つとしてC# MessageBoxを参考にしたダイアログについて説明します


C# MessageBox

C# MessageBoxは.NETFrameworkに搭載されているダイアログ表示用のクラスです

MessageBox クラス (System.Windows.Forms)

Windows民ならよく見るこれを作ってるやつです

f:id:myudon:20180801015734g:plain

MessageBoxでは、ダイアログ自体はロジックを持たず呼び出す側が担保します
呼び出し側がダイアログを展開し、DialogResultで結果待ちをし、その結果を利用してロジックを持つ呼び出し側が処理を行うわけです
聞いてみると普通じゃんって感じがすると思うのですが、アウトゲームを作っていくうえでロジックの所在などをあまり気にせず動くものを作る意識で開発を進めていくと割とダイアログがロジックを持つパターンで実装しがちかなって思います
あまり責務を分散させずにスマートなUI処理がしたいなと思い、これをコルーチンチックに実装してみました

実装

今回紹介するのはシンプルなダイアログのケースです
ダイアログを展開し、ユーザーからのポジティブ/ネガティブな結果を受け取りダイアログをクローズ、処理に移行するという形になります
具体的なケースとしては処理確認系ダイアログが近いでしょう、〇〇しますか?と聞いてはい・いいえで答えるようなやつですね

下記が実装コード、まずはダイアログ本体です
シンプルに形成したものなので各々が使う形に実装をアレンジしていただければと思います
実装の特徴としては簡単なダイアログの実装に加えて待機用のクローズフラグとダイアログのリザルトを含めてあります

public class ExampleDialog : MonoBehaviour {

    /// <summary>
    /// ダイアログそのもののRectTransform
    /// </summary>
    [SerializeField]
    private RectTransform _dialogBody;

    /// <summary>
    /// ダイアログに表示する文章
    /// </summary>
    [SerializeField]
    private Text _description;

    /// <summary>
    /// はいボタン
    /// </summary>
    [SerializeField]
    private Button _positiveButton;

    /// <summary>
    /// いいえボタン
    /// </summary>
    [SerializeField]
    private Button _negativeButton;

    /// <summary>
    /// 処理を終えてクローズしたかの判定
    /// </summary>
    private bool _isClosed;
    public bool IsClosed => _isClosed;

    public DialogResult Result { get; private set; }

    /// <summary>
    /// プレハブからダイアログ作る
    /// </summary>
    /// <param name="parent">吊るす親</param>
    /// <param name="description">ダイアログに表示する文章</param>
    /// <returns>ダイアログのインスタンス</returns>
    public static ExampleDialog CreateDialog(RectTransform parent, string description)
    {
        var dialog = Instantiate(Resources.Load<ExampleDialog>("ExampleDialog"), parent);
        dialog.Initialize(description);
        dialog.Open();
        return dialog;
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    /// <param name="description">ダイアログに表示する文章</param>
    private void Initialize(string description)
    {
        _description.text = description;
        Result = new DialogResult();
    }

    /// <summary>
    /// ダイアログオープンアニメーション
    /// </summary>
    private void Open()
    {
        _dialogBody.DOScale(1f, 1f).OnComplete(() => SetEvent());
    }

    /// <summary>
    /// ダイアログクローズアニメーション
    /// </summary>
    private void Close()
    {
        _dialogBody.DOScale(0f, 1f).OnComplete(() => 
        {
            _isClosed = true;
            Destroy(gameObject);
        });
    }

    /// <summary>
    /// ダイアログボタンのイベント処理、リザルト確定とクローズ
    /// </summary>
    private void SetEvent()
    {
        _positiveButton.onClick.AddListener(() =>
        {
            Result.ResultType = DialogResult.ResultTypeEnum.POSITIVE;
            Close();
        });

        _negativeButton.onClick.AddListener(() =>
        {
            Result.ResultType = DialogResult.ResultTypeEnum.NEGATIVE;
            Close();
        });
    }
}

次にダイアログの結果となるクラスです
ここネガポジをリザルトとして定義して持たせていますが、個々のダイアログで得体結果は違ってくるのでそこは応用が必要になってきます
各々のUIの基盤やシステム構想、ダイアログの使用箇所などと相談して工夫を加えると良いでしょう

// ダイアログの結果
public class DialogResult
{
    public enum ResultTypeEnum
    {
        NONE, // 何もなし
        POSITIVE, // はい
        NEGATIVE, // いいえ
    }

    /// <summary>
    /// ダイアログの結果タイプ
    /// </summary>
    public ResultTypeEnum ResultType;

    // ほかに必要そうなリザルトはここに置く、インターフェイスを定義して応用しても良い
}

最後にダイアログのクローズ及び結果を待つカスタムコルーチンです
特に変わった実装はしておらず、シンプルにダイアログのクローズを監視しています
ここもダイアログの仕様などでアレンジが入る箇所ですね

// ダイアログを待つカスタムコルーチン
public class WaitForDialogClose : CustomYieldInstruction
{
    public DialogResult Result => _dialog.Result;

    private ExampleDialog _dialog;

    public override bool keepWaiting
    {
        get
        {
            // ダイアログがクローズするまで待つ
            return !_dialog.IsClosed;
        }
    }

    public WaitForDialogClose(ExampleDialog dialog)
    {
        _dialog = dialog;
    }
}

以上が今回のダイアログのシンプルな形での実装例です
全てのダイアログに対して汎用的に組み込むことができるかと言われると怪しい部分もありますが、臨機応変に応用しましょう


実験

private IEnumerator Test()
{
    var dialog = ExampleDialog.CreateDialog(_canvas, "ほげほげ?");
    var result = new WaitForDialogClose(dialog);

    yield return result;

    Debug.Log(result.Result.ResultType);
}

上記のようなコルーチンを走らせて挙動を見てみます

ダイアログの結果を受け取ることができました、後は受け取った結果を使って処理を行いましょう
また機会があればアウトゲームについてのこういった小さな技術共有をしていきたいと思います

EasyMotionRecorderを応用したモーキャプデータのモバイル共有

現在、ある案件にてUnityでモーションキャプチャを行い、そのモーションをなんとかしてスマホで共有したいという要求があり、それを現実化するために空いた時間で少しずつ開発を行っていました
その際にとあるモーションの再生録画機構のおかげで作業工程を減らすことができたためメモ

EasyMotionRecorder

github.com

EasyMotionRecorderは某Vtuberで有名なDuoがシンプルなモーションの再生及び録画機構として公開しているライブラリです
元々はAnimationClipを共有しようとか考えていたのですが、モバイルでのランタイム共有においてUnityAssetの共有がかなりネックになってしまったため独自のモーション機構から共有を試みようと思ったのがきっかけでした
基本的にはモーションのあるフレームに必要なボーン情報などをシリアル化可能な状態でまとめ、配列化して共有すればいいのですがそのまとめる工数をあまり取りたくなかったのです
ざっと実装を見た感じ、シリアル化とランタイム実行を考慮したすごく都合のいい状態でまとめてくれていたこれを採用しました

システム

f:id:myudon:20180715224748p:plain

システムは上図のようになっています、とりあえず共有ができればよかったためクライアント以外の部分はかなり簡易的な作りになっています

  1. モーキャプサイドが専用のシーンをエディタで叩いて、モーションキャプチャからモーションデータを生成(別にランタイムでもいいです)

  2. サーバーに転送しユーザー情報やモーション情報と紐づけて保存

  3. スマホ側がサーバーにリクエストして指定のモーションをダウンロード

  4. ローカルに保存しておいて任意で再生

ざっくり言うと上記のフローです
クライアントサイドとしてUnityでモーションキャプチャやモーションのダウンロードを行い、AWS上にサーバーとクラウドストレージ、RDBを用意してモーションデータや関連情報の管理をしています

実装

それでは本題であるUnityとEasyMotionRecorderでのモーションデータの共有に入ります
使用ケースで実装形式が変わるため共有のために行っている処理を抜粋して簡単に説明します
まずモーションデータの生成です、今回はモーションキャプチャーの機器としてPerception Neuronを使用しました
EasyMotionRecorderを使用することでかなり簡単にGUIをいじって生成することができます、詳しくは公式ドキュメントを読んでいただくのが早いのでここでは説明を省略します

f:id:myudon:20180716014034p:plain

Recordを終了させると_posesにHumanoidのモーションデータが記録されるため、このデータを使っていきます


次にモーションデータをシリアル化します、ここではDuoのモーションデータ形式をほぼそのまま使用しています
EasyMotionRecorderではHumanPosesという形式のデータで保存しており、中身はアニメーションフレーム単位でのモーション情報です
Vector3とQuaternionだけシリアル化の関係でfloatに分解しています、最近ではcsv吐き出しの機構をduoが追加したっぽいのでそちらを使うのでもよいでしょう
また、シリアライズに関してはシンプルな方法で記述しています、使用する開発環境やコストに合わせてシリアライズ形式は好きなものを選択してください

[System.Serializable]
public class SerializableMotionData
{
    // DB管理しない付随するモーション情報があればつける
    public string hoge;

    // duoのHumanPosesに含まれるモーション情報
    public List<HumanoidPoses.SerializeHumanoidPose> poses;

    public SerializableMotionData(string hoge, List<HumanoidPoses.SerializeHumanoidPose> poses)
    {
        this.hoge = hoge;
        this.poses = poses;
    }
}
// シリアライズの簡易例

    /// <summary>
    /// バイナリシリアライズ
    /// </summary>
    public static byte[] SerializeMotionData(SerializableMotionData obj)
    {
        MemoryStream stream = new MemoryStream();
        BinaryFormatter bf = new BinaryFormatter();

        bf.Serialize(stream, obj);
        return stream.ToArray();
    }

    /// <summary>
    /// バイナリデシリアライズ
    /// </summary>
    public static SerializableMotionData DeserializeMotionData(byte[] bytes)
    {
        MemoryStream stream = new MemoryStream(bytes);
        BinaryFormatter bf = new BinaryFormatter();
        var obj = bf.Deserialize(stream) as SerializableMotionData;
        return obj;
    }

そして投稿です、UnityWebRequestでいいんですが、今回は都合でObservableWWWを使用しています

// 任意のレスポンスを定義してやり取り
    public static async Task<TResponse> PostMotionData(byte[] poses)
    {
        var endpoint = "";
        var form = new WWWForm();

        // ここで送信するモーション情報を定義

        // モーションデータのバイナリ
        form.AddBinaryData("poses", poses);

        // using UniRx
        var www = await ObservableWWW.Post(endpoint, form);
        var response = JsonUtility.FromJson<TResponse>(www);

        return response;
    }

次に共有するモバイル側の実装です、今回使用したケースでは一回モーションの関連情報をサーバーに問い合わせてモーションをダウンロードするんですが、そこは省略し直接モーションデータをURLからダウンロードする部分を記述します
投稿同様にObservableWWWでシンプルに記述しています

// URL指定してDL
    public static async Task<SerializableMotionData> GetMotionData(string url)
    {
        // using UniRx
        var www = await ObservableWWW.GetAndGetBytes(url);
        var motion = DeserializeMotionData(www);

        return motion;
    }

モーションデータをデシリアライズし、モーションプレイヤーに流し込みます
MotionDataPlayerという再生機構は用意されているので、適当にデータを紐づけておいてそこに代入する方式をとりました

f:id:myudon:20180716024006p:plain

   // HumanPosesに対して追加
  public void SetPoses(List<SerializeHumanoidPose> poses)
    {
        Poses = poses;
    }
    // MotionDataPlayerに対して追加
    public void SetMotion(List<HumanoidPoses.SerializeHumanoidPose> poses)
    {
        _recordedMotionData.SetPoses(poses);
    }

後はMotionDataPlayerで再生してやればモバイルのランタイムでモーションを見ることができます
フレーム単位でポーズをいじってるだけなのでその辺をいじる機構を作れば色々再生周りの機構も作れるでしょう


AR100Projects

最後に自分が行ってる活動の宣伝をば
この機構の開発の発端は、イワケンこと同期の岩崎謙太(@tanaka_lit)とともに行っている「AR100-Projects」という施策の下で開発中のプロダクトです
このプロジェクトはイワケンとともにARで世界にインパクトを与える、プロダクトを量産するという思想の下に、時代がAR本格化する数年後に向けてARプロダクトを100個作ろうというとてもクリエイティブなものです
ARやVRについて圧倒的な熱意を持っているイワケンやプロダクトに協力してくれる様々な人たちとともに、ARでできることを考えつつ形にしていっています
まだまだ発展途中ですが、自分も技術者としてというよりも、クリエイターとして自分を高めていくためにこのプロジェクトを盛り上げて成功させようと考えています
開発したプロダクトは勿論、ここで得た技術知見の共有などもブログで積極的に行っていく予定なので応援していただけると幸いです

Unity開発における継承とコンポジション

開発を進めていくうえでクラスの関係性をどのように設計するかを画策することは多々あると思います。
最近ではUnity2018でECSといったUnityでの開発アーキテクチャも話題になり、Unityでそれらをどう管理するのが正解か?という議論もちょこちょこ見られるようになりました。(自分だけかも)
そんな今回はUnityの開発思想に則して、少し基礎的な部分になりますがクラス設計の小話をしたいと思います。ECSの理解に対して必須の見識だと感じているので参考になれば幸いです。(変な解釈の部分があればご指摘ください)

継承とコンポジション

まず、プログラミングにおいて基礎的な要項ですが、継承とコンポジションの相違性について洗い出します。
継承もコンポジションも複数クラスの共有化という点では同じです、簡単に言ってしまうと違いはその共有する際のクラス間の関係性です。
継承の関係をざっくり言うと「is-a」です。継承ではスーパークラスの性質をサブクラスが全て引き継ぎ、全て我が物として使用します。
コンポジションの関係をざっくり言うと「has-a」です。共有するクラスをメンバとして管理し、依存したい部分にアクションを起こします。
継承はスーパークラスそのものでありそのサブタイプとなりますが、コンポジションではあくまでも共有する機能を持った拡張クラスでしかありません。

Unityとコンポーネント指向

それでは継承とコンポジションの相違点を踏まえ、Unityで開発する際のクラス関係を考えましょう。

Unityはコンポーネント指向をベースとしており、ある実装の枠組みに対してコンポーネントという機能単位で実装を図っていくことがベターです。
例えばある3Dキャラクターの敵を実装するとして、描画を行うコンポーネント、座標を管理するコンポーネント、敵キャラとしてアクションを起こすコンポーネント...等と作用ごとに分割し実装していきます。
それに対してクラスを共有するような物が出てきたらどうでしょう。

上の例から、様々な敵クラスに派生していくことを考えます。当然、敵毎のユニークなアクションを行うものは別途で実装を行いますが、3Dキャラクターとしての共通処理、敵としての共通処理は共有化を図ることになります。
その時、正解なのは「is-a」ベースの継承クラスを用意することでしょうか、それとも「has-a」ベースの共有コンポーネントを用意することでしょうか。
上記でも述べた継承の特性をもう一度考えます、継承という関係性はサブクラスがスーパークラスに対して大きく依存をすることになります。スーパークラス内で敵クラスについて共通処理の改修が行われた際にその派生した敵キャラクターについては保証されません。設計者がそれについて担保した設計にしているのならば別ですが、リリース毎に改修作業が増える可能性は十分にあります。

コンポーネント指向において、完全な「is-a」関係を要するものはそう多くはないと考えています。共有化すべき処理をCommonなりなんなり共有コンポーネントとして与え、それについてアクセス側がアクションを起こすという形が無難であり、そういった「has-a」の関係性で構成されるべきです。
継承が必要な関係性の場合は良いのですが、コンポーネント指向である以上、実装において共有化を要する際には機能単位でのコンポジションベースで考えることを徹底したほうがいいですね。


Unity2018のECSはコンポーネント指向を理解し、コンポーネント単位での振る舞いを考えることで理解が深まるものだと感じています。
コンポジション等の設計指向の基礎を再確認し、ECSを学ぶことで最善な設計を追求していきたいです。
自分もまだこの部分の解釈については検討途中であるため、感想や意見、指摘などがあればお願いします。

Rxで条件付きの能力を設計する

バトルゲームを作成していて頻繁に出てくるのが「Aの時Bを発動する」と言った条件付きの能力です 例えばパズドラのゼウスというキャラクターは「HPが満タンの時、味方全体の攻撃力が3倍になる」という能力を持っています

f:id:myudon:20180329003932j:plain

またスプラトゥーンのギアの中にも、「対戦開始から30秒までの時、速度が上昇する」「復活した時、相手の位置が遠くから見える」等といった条件付きの能力を含んだものが存在します

f:id:myudon:20180329004718p:plain

勿論これらの他にもこれらのような条件をトリガーとして発動する能力は多く、バトルをデザインする上でかなりメジャーな材料と言えるでしょう

ですが、それらを安直にゲームシステム内にどんどん実装していくと、ゲーム開発が進むにつれ見るに堪えない構造になりがちです。これは時間を条件としたり、キャラクターのパラメーターを条件としたり、取り巻く環境の状況を条件としたりと様々な箇所にトリガーが考えられるからです
また、これらの能力はゲームを運用して、バトル環境をサイクルしていくにつれても増えていきます、Aという条件でB、Aという条件でC、AとDという条件でC等、組み合わせ的にも増えていくことが考えられるため、変更や振る舞いの追加に強い設計が求められます

そこで今回はUniRxを使って、この課題に対してゲームのシステムロジックに依存せず稼働し、かつ変更などに柔軟に対応できる設計を考えていきます

設計と実装例

まずはベースとなる形を考えます、条件付きの能力をレベルデザインの点から見た時、条件と効果は別々で考えていき、そこから組み合わせ的に一つの物を作りだすということが多くなります
つまり、デザインする上で条件と効果がそれぞれ単独で動く作りであると、個々にテストを行い、調整を行うことが出来ます

条件と効果

条件側は、単一の担当している条件を監視して達成したかどうかだけを通知し、効果側は単一の担当している効果の発動や有効無効の切り替えを行うようにします
それぞれ条件と効果の基盤をスーパークラス化し、それを元に実装していきます

using System;
using UnityEngine;
using UniRx;

public abstract class ConditionBase : IDisposable
{
    protected Component _dependencyComponent;

    public ConditionBase(Component component)
    {
        _dependencyComponent = component;
    }

    //条件達成通知
    public Subject<bool> OnAchieve = new Subject<bool>();

    public void Dispose()
    {
        OnAchieve.Dispose();
    }

    public static ConditionBase GetCondition(ConditionType type, Component dependency)
    {
        // 具体的な条件の取得
    }

    public enum ConditionType
    {

    }
}
using System;
using UnityEngine;

public abstract class EffectBase
{
    protected Component _dependencyComponent;

    public EffectBase(Component component)
    {
        _dependencyComponent = component;
    }
    
    //効果の有効化/無効化
    public abstract void SwitchEffect(bool invoke);

    public static EffectBase GetEffect(EffectType type, Component dependency)
    {
        //具体的な効果の取得
    }

    public enum EffectType
    {

    }
}

Binder

条件と効果を元に、この2つをBindする大本の能力の基盤クラスを設計します、能力自身はあくまでも条件と効果の中身に干渉せず、ただBindするだけです
ここで汎用的な能力設計を行うために、RxでどのようにBindをするかを考えていきます
能力が持つ条件については、例えば上記で上げた例では単一条件で単一効果を担保していましたが、例えば複合条件、また複数条件の内1つでも満たしていたら発動など、条件をAND的に、OR的に考えることも考慮したほうが汎用性が高いと言えます
そこで、複数の条件からくる通知を束ねて考えて、その束からAND,OR的に導出をすることで様々な条件発動に対する回答を試みます、ここで役に立つのがRxです
条件達成をイベントストリーム的に捉え、それらを並列に監査します、イメージは下図です

f:id:myudon:20180408111705p:plain

これをUniRxを用いて実装していきます、肝になるのがCombineLatestです
CombineLatestは複数のストリームを統一して監視するものです、対象とする複数ストリームよりいずれかのストリームから値が発行された時に全てのストリームが保持している値を流します
これを応用し、条件の達成通知を統合して購読します
f:id:myudon:20180401224826p:plain
Binderに条件と効果の識別子、そしてその処理に必要となる依存性を外部から流し込みます、そしてBinderを立ち上げて条件と効果の結びつきをCombineLatestによって実現します

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UniRx;

public class Binder : IDisposable
{

    private List<ConditionBase> _conditions;

    private List<EffectBase> _effects;

    private IDisposable _andDisposable;
    private IDisposable _orDisposable;

    // 能力の干渉対象
    private GameObject _dependencyObject;

 public Binder(Component dependency, List<ConditionBase.ConditionType> conditionTypes, List<EffectBase.EffectType> effectTypes)
    {
        _dependencyObject = dependency.gameObject;

        _conditions = conditionTypes.Select(type => ConditionBase.GetCondition(type, dependency)).ToList();
        _effects = effectTypes.Select(type => EffectBase.GetEffect(type, dependency)).ToList();
    }
  
    //初期化でそれぞれの通知をCombineLatestで束ねて処理
    public void Initialize()
    {
        _andDisposable =
            _conditions.Select(x => (IObservable<bool>)x.OnAchieve)
            .CombineLatest()
            .Subscribe(_ => _effects.All(x => true))//andで条件を束ねる
            .AddTo(_dependencyObject);

       _orDisposable =
            _conditions.Select(x => (IObservable<bool>)x.OnAchieve)
            .CombineLatest()
            .Subscribe(_ => _effects.Any(x => true))//orで条件を束ねる
            .AddTo(_dependencyObject);
    }

    public void Dispose()
    {
        _conditions.ForEach(condition => condition.Dispose());

        _andDisposable.Dispose();
        _orDisposable.Dispose();
    }
}

今回はもっとも単純な形としてEnumから具体的なサブクラスを呼び出し、共通処理からポリモーフィズム的な条件と効果の呼び出しを行っています
条件についても 基本型としてBindを設計しましたが、条件の性質によっては束ね方が変わってくるかと思います

メリット

まず1番のメリットは先述にもあったように、条件と効果が疎に連結され、組み合わせを柔軟に組むことができることです
能力の追加や仕様変更に対しても強く、また条件についてはストリームで監視を行う以上条件の処理の重さに合わせて監視の粒度を調整することも可能です
何より効果毎の調整といったデバッグ機能などのテストコードの実装が容易であり、細かい変更にコストを割くことが無くなるでしょう

デメリット

一つはRxそのものがゲームロジックにクリティカルに介入するという事そのものです
自分はRxが確かに好きなのですが、好きであるからこそしっかりと使い所は見極めるべきだと考えています
特定のゲームロジックに対し監視を仕掛ける以上、Rxは処理コストを要します、ゲーム内容によってはここの処理コストがボトルネックとなってしまう可能性もあり、簡単に導入を決定できるものではないと言えます
もう一つはイベント処理の通知化にとらわれてしまう事です
依存性を持つコンポーネントに対して監視し、条件が処理を取り行う為、条件側からイベントを何かしらの形で購読できる形が必要になってきます
その為に多数のSubjectの発生やReactive化が必要となるという懸念があります、無論それによる処理負荷の肥大の可能性も考えられるでしょう


今回、設計の一例としてRxを用いた汎用化を紹介しましたが、あくまでも形の1つであり、内部の既存構造によって最善形は変わってくるかと思います
ですが、瞬間的に完結しないゲームロジックの汎用的設計の1つの解決案として、Rxを頭の片隅に置いておくことは良いことですし、道具として知ってだけでもロジックの捉え方が広くなるのでおすすめです