📦

step by step VContainer vol.2

2022/12/25に公開1

当記事の概要

VContainerってなんか凄そう?VContainerを理解したいけど「意味わからん!」というUnityを交えたC#のプログラミングにある程度慣れてきた中級者向けに、VContainerに関連する用語や知識、プログラムサンプルを数回の記事に分けて整理したものになります。主に自分のために書いていますが、もしどなたかの参考になれば幸いです。

前回はこちらからどうぞ。

第2回:用語編 その1- UnityおよびC#に関連する知識

第2回の今回は、VContainerと関係の深い知識や用語について次の内容を整理してみました。

  • オブジェクト指向プログラミング
  • UnityのGameObjectを主軸とした構造の問題点
  • Unityのライフサイクルと「エントリーポイント」
  • デリゲート
  • ラムダ式
  • ガーベジコレクション

人によってはご存じの用語も多々あると思います。理解のある内容は適宜読み飛ばしながら進めてもらえると良いと思います。また、この記事で取り上げた用語をある程度理解できている方は、もう公式のHelloWorldページを読んでしまった方が理解が早いと思います。

オブジェクト指向プログラミング

C#でプログラミングをする(C#スクリプトを記述する)際の基本構造を表す言葉です。まずこれの基本的な内容を理解できていないケースでは、VContainerを使うのはとてもとても難しいと思います。抽象クラスやインターフェースあたりが必要に応じて作成・管理でき、ポリモーフィズムを理解している事は必須です。詳細は割愛します。

UnityのGameObjectを主軸とした構造の問題点

VContainerの作者ハダシA氏はUnityの構造に次のような問題点があると考えたそうです。

  • ゲームのオブジェクトの不安定な寿命:一般的なアプリケーションに比べて生成と破棄が激しい
  • 複雑な制御フロー:GameObject同士が命令をする・される側になりえるため、制御順序が混乱しがち

そこで、VContainerを使うことで、オブジェクトの寿命に柔軟に対応でき、制御フローをシンプルにすることはできないか?と考えたそうです。

Unityのライフサイクルと「エントリーポイント」

Unity内部に設けられたスクリプトの実行順序を示す処理の流れです。MonoBehaviourを継承した.csファイル上でデフォルトで用意されているStartメソッドやUpdateメソッド、書き足すことで利用できるFixedUpdateメソッドやOnDestroyメソッドとかのことです。VContainerでは、Unityのライフサイクル上にあるいくつかの処理の起点を「エントリーポイント」と呼んでいます。エントリーポイントにおいて実行したい処理はVContainerに用意されたインターフェースを活用することで、MonoBehaviourを使ったUnity依存のスクリプトを脱却し、純粋なC#の処理に置き換えることを可能にします。

置き換え元メソッド 置き換え先インターフェース&メソッド
Start() IStartable
IStartable.Start()
Update() ITickable
ITickable.Tick()
FixedUpdate() IFixedTickable
IFixedTickable.FixedTick()

Unityスクリプトのライフサイクル

デリゲート

C#にて処理を"移譲"するための仕組みです。もっと分かりやすく言えば、メソッドを格納できるフィールドといったところでしょうか。他のプログラミング言語では関数型と呼ばれたりしています。VContainerでは、クラスだけでなくデリゲートを登録して「注入する」ことに利用できます。

基本的なデリゲートの記法以外に、最近のUnityではより簡単な記述でデリゲートを利用できるActionデリゲートやFuncデリゲートがよく活用されています。VContainerではFuncデリゲートを取り扱うことが多いため、Funcについてのみ取り上げます。

Funcデリゲートは、「Func<引数の型1,引数の型2,…,戻り値の型> デリゲート名;」と記載することで定義し、「指定された引数の型と戻り値の型をもったメソッドが登録できるようになる」というものです。

以下は「intを引数にもち、戻り値がstringのメソッドを登録できるFuncデリゲート」を動作させてみたサンプルです。

FuncSample.cs
using System;
using UnityEngine;

public class FuncSample : MonoBehaviour
{
  // デリゲートに登録するためのメソッドをいくつか定義する
  string Method1(int i)
  {
    Debug.Log("I'm Method1");
    return "Method1's result = "+i;
  }

  string Method2(int i)
  {
    Debug.Log("I'm Method2");
    return "Method2's result = "+i;
  }

  string Method3(int i)
  {
    Debug.Log("I'm Method3");
    return "Method3's result = "+i;
  }

  string Method4(int j, float k)
  {
    Debug.Log("I'm Method4");
    return "Method4's result = "+j+" & "+k;
  }

  void Start()
  {
    Func<int,string> func1; // 引数にintをひとつ持ち、戻り値の方がstringのメソッドが登録できるfunc1を定義、
    func1 = Method1; // 最初は必ず = で初期値を代入する必要がある
    func1 += Method2; // 以降は += を使ってメソッドを追加していくことができる
    func1 += Method3;
    func1(0); // func.Invoke(0)と記述しても良い。登録メソッド全てが引数0を入れて実行される。
    string result1 = func1(10); // Funcは戻り値の型にstringを持つのでreturnされた値も取得できる
    Debug.Log( result1 ); // ただし、戻り値は一番最後に登録されたメソッド(今回はMethod3)に対してのみ発生する
    func1 -= Method2; // -= にメソッド名をつづけて書くことで登録から任意のタイミングで除外できる
    func1(20); // この時点で結果表示されるのはMethod1とMethod3の内容のみ
    func1 = Method2; // 初期化以外で = と記述した場合はシンプルに上書きされる
    func1(30); // この時点で結果表示されるのはMethod2の内容のみ
    Func<int,float,string> func2 = Method4; // ジェネリクス(<~>の部分)の中身が増えた場合は一番最後が登録メソッドの戻り値の型で他は全て登録メソッドの引数
    func2(100, 11.1f); // 引数が増えているので実行時は引数を2種類指定する。加えてFunc<int,float・・・としたように、記載する順番も注意が必要
    Debug.Log( func2(200, 22.2f) );
  }
}


FuncSample.csの実行結果

ラムダ式

ラムダ式は、前述のデリゲートに使用するためのメソッド定義をより簡単に記述するための方法です。メソッドとして定義した場合は、様々なプログラムから呼び出しを想定しているかと思いますが、中には1度切りしか使用せず、わざわざメソッド名などの名前をつけてまで管理するのが面倒に感じられるケースがあります。そういった場合にラムダ式で記述することで手間を削減できます。

LambdaSample.cs
using System;
using UnityEngine;

public class LambdaSample : MonoBehaviour
{
  string Method5(int i)
  {
    Debug.Log("I'm Method5 with "+ i);
    return "Method5's result = "+i;
  }

  void Start()
  {
    Func<int,string> func1 = Method5;
    func1(0);

    // ラムダ式を使ってMethod5と同じ内容を再現する。メソッド名と戻り値の型を省略できた。
    Func<int,string> func2 = (int i) => {
      Debug.Log("I'm Lambda1 with "+ i);
      return "Lambda1`s result = "+i;
    };
    func2(10);

    // Funcの定義から引数の型すらも省略して記述が可能です(xの部分)
    Func<int,string> func3 = x => {
      Debug.Log("I'm Lambda2 with "+ x);
      return "Lambda2`s result = "+x; 
    };
    func3(20);
  }
}

ガーベジコレクション

ガーベジコレクションとは、C#をはじめとしたプログラミング言語に備わる「不要なデータを自動でメモリから取り除き、メモリを解放してくれる」機能です。普段プログラミングをしている状態では、いつこの機能が動いているかを特に意識する必要はなく、また意識しないでプログラミングができるような作りになっています。

ところがゲーム開発においては実はそうも言ってはいられません。ゲームでは弾、エフェクト、敵キャラクターなどなどが常にバンバン生成され、バンバン消えていきます。この時、ガーベジコレクションが行われる条件などについてある程度把握していないと間違ったデータの削除などが行われ、後で使おうと思っていたデータがなくなっていることもあるかもしれません。

同様に怖いのが、ガーベジコレクションの頻度やガーベジコレクションの実行された時にかかる時間です。ガーベジコレクションは常に動いているものではなく、メモリにデータがある程度増えた段階で初めて実行されます(数秒〜数10秒に1回など、ゲームを実行しているデバイスによって変わる)。通常これはごく短い時間で行われるので影響はないと考えられていますが、ゲームが大規模化するにつれてガーベジコレクションの対象となるデータが爆発的に積もり上がります。それらが最終的にゲームが遅延するほど、最悪ゲームが停止している感覚をプレイヤーに与えるほど致命的なものになる可能性も存在します。

VContainerでは、こう言ったことが起こらないようにガーベジコレクションを極力起こさないように軽量な作りをしています。DIコンテナは、一般的に「注入する」ことを行う関係でデータの生成処理を受け持つことになるわけですが、VContainerにおいては生成と破棄の双方、つまりデータの寿命を上手に管理する機能もついています。

第2回目の終わりに

用語編はまだまだ続きます。次回はVContainerを使い始める前に理解しておいたほうが良いデザインパターンなどについて取り上げようと思います。