高速シミュレーションを回せるようにするためのインゲーム設計

はじめに

こんにちは。サイバーエージェントのゲーム・エンターテイメント事業部(SGE)でUnityエンジニアをしている吉成です。

今日のゲーム開発では、リリース時から長くユーザーの皆さまにゲームを楽しんでもらうために、リリース時やその後の運用で、適切なレベルデザインを行うことが重要となります(誤用とされることもありますが、今回はバランス調整をレベルデザインと表現します)。

特にインゲームでのレベルデザインは重要です。

例えば、運用で新しいキャラクターや装備を追加する際は、新しく発生する様々な組み合わせに対して、インゲームが破綻しないようなパラメータやスキルを設定する必要があります。その実現のためには、実際に様々な組み合わせを試してみる必要がありますが、実際のゲーム環境で何度も組み合わせを試すのは時間がかかります。そのため、インゲームに対して手軽に高速なシミュレーションを回せる仕組みが必要になります。

本記事では、MVPアーキテクチャを用いて、高速シミュレーションを回せるようにするためのインゲーム設計を紹介します。

また、本章ではMVPアーキテクチャやUniRxの知識を必要とします。MVPアーキテクチャやUniRxに関しては、弊社の技術ブログ内の『Web出身のUnityエンジニアによる大規模ゲームの基盤設計』という記事に執筆をしておりますので、そちらを参考にしてください。

※本記事は2019年4月14日に開催された技術書典6にて販売した「UniTips Vol.3」からの転載となります。

今回のゲーム

今回は『速度のパラメータを持つユニティちゃんが、設定された距離を走る際のタイムを測る』というゲームを作成します。ランダム性もなく、そもそも距離を速度で割ったらタイムも出せてしまうので、ゲームと呼んで良いのかも微妙ですが、今回は次の仕様を満たすゲームを例に説明を行います。

  • 起動と同時にユニティちゃんが走り始める
  • 右上に経過時間のタイマーを表示する
  • ゴールしたら「Goal」の表示を出す
  • ゴールしたらタイマーを止め、ユニティちゃんも停止させる

今回は結果のタイムを見て、距離や速度のパラメータを調整することを考えます。そのため、距離や速度のパラメータを変更して簡単にゲーム結果を確認できる、高速でシミュレーションを行える設計を目指します。

シミュレーションを考えない単純な実装

まず、シミュレーションを考えない単純な実装を見てみます。

今回の設計を図で表すと次の図のようになります。

私はアウトゲームのようなシミュレーションを考えないページを作る際などは、このようなPresenterでツリー構造を作る設計で実装を行います。この構造自体が最適なのかは分かりませんが、今回はこの作りをベースに話をします。

それでは、GameSettingとGameとCharacterとTimeをそれぞれ見ていきます。

GameSetting

GameSettingModelのコードを次に示します。

public class GameSettingModel
{
    // ゴールまでの距離
    public float GoalDistance { get; private set; }

    // キャラクターの速度
    public float CharacterSpeed { get; private set; }

    // コンストラクタ
    public GameSettingModel()
    {
        GoalDistance = 100f;
        CharacterSpeed = 10f;
    }
}

今回はシンプルに、ゴールまでの距離とキャラクターの速度のパラメータを1つのクラスにまとめます。

今回はベタ書きで値を入れていますが、実際にはマスターデータの値を参照したり、サーバーから送られてくる値を参照して、値を設定する可能性もあると思います。

Game

GameのModelとViewとPresenterのコードを次に示します。

using UniRx;

public class GameModel
{
    // Game設定
    public GameSettingModel GameSetting { get; private set; }

    // ゴールまでの距離
    public float GoalDistance { get; private set; }

    // ゴールしたかどうか
    private ReactiveProperty<bool> _isFinishedProperty;
    public IReadOnlyReactiveProperty<bool> IsFinishedProperty
    {
        get { return _isFinishedProperty; }
    }
    public bool IsFinished
    {
        get { return _isFinishedProperty.Value; }
    }

    // コンストラクタ
    public GameModel()
    {
        GameSetting = new GameSettingModel();
        GoalDistance = GameSetting.GoalDistance;
        _isFinishedProperty = new ReactiveProperty<bool>();
    }

    // ゴールしたかを設定
    public void SetIsFinished(bool isFinished)
    {
        _isFinishedProperty.Value = isFinished;
    }
}

GameModelではゴールしたかどうかの値をReactivePropertyで持ちます。

また先ほどのGameSettingModelもGameModelに持たせます。

using UnityEngine;

public class GameView : MonoBehaviour
{
    // ゴール表示
    [SerializeField]
    private GameObject _goal;

    // ゴールの表示を設定
    public void SetGoalVisible(bool isVisible)
    {
        _goal.SetActive(isVisible);
    }
}

GameViewではゴールした際の表示の出し分けを行います。

using UnityEngine;
using UniRx;

public class GamePresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private GameView _view;

    // Model
    private GameModel _model;

    // キャラクターのPresenter
    [SerializeField]
    private CharacterPresenter _character;

    // 経過時間のPresenter
    [SerializeField]
    private TimePresenter _time;

    // Start処理
    private void Start()
    {
        _model = new GameModel();

        _character.Initialize(_model.GameSetting.CharacterSpeed);
        _time.Initialize();

        Bind();
        SetEvents();
    }

    // Update処理
    private void FixedUpdate()
    {
        var deltaTime = Time.fixedDeltaTime;

        if (_model.IsFinished)
        {
            return;
        }
        _character.ManualUpdate(deltaTime);
        _time.ManualUpdate(deltaTime);
    }

    // Bind処理
    private void Bind()
    {
        _model.IsFinishedProperty
            .Subscribe(_view.SetGoalVisible)
            .AddTo(this);
    }

    // イベント設定
    private void SetEvents()
    {
        _character.OnDistanceChangedCallback = OnDistanceChanged;
    }

    // 進行距離が変更された際に呼ばれる
    private void OnDistanceChanged(float distance)
    {
        if (distance >= _model.GoalDistance)
        {
            _model.SetIsFinished(true);
        }
    }
}

今回は固定フレームレートで処理を行いたいため、FixedUpdateメソッドを利用していますが、分かりやすさを重視するため、以降の本文中ではFixedUpdateをUpdateと表現します。

GamePresenterはCharacterPresenterとTimePresenterを持ち、各々の初期化も行います。

また、Update処理はGamePresenterで行い、その中でCharacterPresenterとTimePresenterのManualUpdateメソッドを呼ぶことで、各々でUnityによって実行されるUpdateメソッドを呼ばないようにします。

そして、GamePresenterはGameModelのゴールしたかどうかの値を監視し、結果をGameViewに反映します。また、CharacterPresenterの進行距離が変わった際のイベントを監視し、進行距離が変わるたびにゴールしたかどうかの判定を行ないます。

Character

CharacterのModelとViewとPresenterのコードを次に示します。

using UniRx;

public class CharacterModel
{
    // 速度
    public float Speed { get; private set; }

    // 進行距離
    private ReactiveProperty<float> _distanceProperty;
    public IReadOnlyReactiveProperty<float> DistanceProperty
    {
        get { return _distanceProperty; }
    }
    public float Distance
    {
        get { return _distanceProperty.Value; }
    }

    // コンストラクタ
    public CharacterModel(float speed)
    {
        Speed = speed;
        _distanceProperty = new ReactiveProperty<float>();
    }

    /// 進行距離の値を設定
    public void SetDistance(float distance)
    {
        _distanceProperty.Value = distance;
    }
}

CharacterModelでは進行距離をReactivePropertyで持ちます。

また、キャラクターの速度も持ちます。

using UnityEngine;

public class CharacterView : MonoBehaviour
{
    [SerializeField]
    private Transform _unityChanContainer;

    // ユニティちゃんのPrefab
    [SerializeField]
    private GameObject _unityChanPrefab;

    // 初期化
    public void Initialize()
    {
        var unityChan = Instantiate(_unityChanPrefab, _unityChanContainer);
        unityChan.transform.localPosition = new Vector3(0f, 0.5f, 1f);
    }

    /// 進行距離を設定
    public void SetDistance(float distance)
    {
        // 今回は進行距離がそのままZになるとする
        var position = transform.localPosition;
        position.z = distance;
        transform.localPosition = position;
    }
}

CharacterViewでは進行距離を元にキャラクターの位置の設定を行います。

また、初期化時にユニティちゃんの3DモデルをPrefabから生成します。

using System;
using UniRx;
using UnityEngine;

public class CharacterPresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private CharacterView _view;

    // Model
    private CharacterModel _model;

    // 進行距離が変更された際に呼ばれるCallback
    public Action<float> OnDistanceChangedCallback;

    // 初期化
    public void Initialize(float speed)
    {
        _view.Initialize();

        _model = new CharacterModel(speed);

        Bind();
    }

    // 手動Update
    public void ManualUpdate(float deltaTime)
    {
        Move(deltaTime);
    }

    // 移動
    private void Move(float deltaTime)
    {
        var distance = _model.Distance + _model.Speed * deltaTime;
        _model.SetDistance(distance);
    }

    // Bind処理
    private void Bind()
    {
        _model.DistanceProperty.Subscribe(OnDistanceChanged).AddTo(this);
    }

    // 進行距離が変更された際に呼ばれます
    private void OnDistanceChanged(float distance)
    {
        OnDistanceChangedCallback?.Invoke(distance);
        _view.SetDistance(distance);
    }
}

CharacterPresenterはCharacterModelの進行距離の値を監視し、値が変更された際にコールバックを呼んだ後、結果をCharacterViewに反映します。このコールバックはGamePresenterがゴールしたかどうかの判定をするために用いられます。また、キャラクターの進行距離の更新も行います。

Time

TimeのModelとViewとPresenterのコードを次に示します。

using UniRx;

public class TimeModel
{
    // 経過時間
    private ReactiveProperty<float> _timeProperty;
    public IReadOnlyReactiveProperty<float> TimeProperty
    {
        get { return _timeProperty; }
    }
    public float Time
    {
        get { return _timeProperty.Value; }
    }

    // コンストラクタ
    public TimeModel()
    {
        _timeProperty = new ReactiveProperty<float>();
    }

    // 経過時間を設定
    public void SetTime(float timer)
    {
        _timeProperty.Value = timer;
    }
}

TimeModelでは経過時間をReactivePropertyで持ちます。

using UnityEngine;
using UnityEngine.UI;

public class TimeView : MonoBehaviour
{
    // 経過時間
    [SerializeField]
    private Text _time;

    // 経過時間を設定
    public void SetTime(float time)
    {
        int second = Mathf.FloorToInt(time);
        int millisecond = Mathf.FloorToInt((time - second) * 1000f);
        _time.text = string.Format("{0}:{1:000}", second, millisecond);
    }
}

TimeViewでは経過時間を元に経過時間表示を行います。

using UniRx;
using UnityEngine;

public class TimePresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private TimeView _view;

    // Model
    private TimeModel _model;

    // 初期化
    public void Initialize()
    {
        _model = new TimeModel();

        Bind();
    }

    // 手動Update
    public void ManualUpdate(float deltaTime)
    {
        AddTime(deltaTime);
    }

    // 時間を進める
    private void AddTime(float deltaTime)
    {
        _model.SetTime(_model.Time + deltaTime);
    }

    // Bind処理
    private void Bind()
    {
        _model.TimeProperty.Subscribe(_view.SetTime).AddTo(this);
    }
}

TimePresenterはTimeModelの経過時間の値を監視し、結果をTimeViewに反映します。

高速シミュレーションの実装

シンボルの定義

以降は、高速シミュレーションを行うためにコードを変更していきます。

まず、「Edit > Project Settings…」でProject Settingsウィンドウを開きます。そして「Player」タブを開き、「Other Settings」内の「Scripting Define Symbols」に高速シミュレーション用の「FAST_PLAY」というシンボルを定義します。

Viewの処理を呼ばないようにする

GamePresenterの変更分のコードを次に示します。

    // Bind処理
    private void Bind()
    {
#if !FAST_PLAY
        _model.IsFinishedProperty
            .Subscribe(_view.SetGoalVisible)
            .AddTo(this);
#endif
    }

CharacterPresenterの変更分のコードを次に示します。

    // 初期化
    public void Initialize(float speed)
    {
#if !FAST_PLAY
        _view.Initialize();
#endif
        _model = new CharacterModel(speed);

        Bind();
    }
    // 進行距離が変更された際に呼ばれます
    private void OnDistanceChanged(float distance)
    {
        OnDistanceChangedCallback?.Invoke(distance);
#if !FAST_PLAY
        _view.SetDistance(distance);
#endif
    }

TimePresenterの変更分のコードを次に示します。

    // Bind処理
    private void Bind()
    {
#if !FAST_PLAY
        _model.TimeProperty.Subscribe(_view.SetTime).AddTo(this);
#endif
    }

シミュレーション時に見た目は不要となるため、Viewの初期化や更新処理を呼ばないようにします。これらの修正により、Modelの変更に応じてViewが更新されなくなります。また、CharacterPresenterからCharacterViewの初期化も呼ばないよう変更したため、ユニティちゃんの3Dモデルも生成されなくなりました。

Update処理を高速に回す

次にUpdate処理を高速に回すための変更を行います。

変更後のGamePresenterのコードを次に示します。

    // Start処理
    private void Start()
    {
        _model = new GameModel();

        _character.Initialize(_model.GameSetting.CharacterSpeed);
        _time.Initialize();

        Bind();
        SetEvents();

#if FAST_PLAY
        FastPlay();
#endif
    }

    // 高速プレイ
    private void FastPlay()
    {
        while (_model.IsFinished == false)
        {
            ManualUpdate(Time.fixedDeltaTime);
        }
        // 結果確認用の処理
        Debug.LogFormat("Time:{0}", _time.Model.Time);
    }

#if !FAST_PLAY
    // Update処理
    private void FixedUpdate()
    {
        var deltaTime = Time.fixedDeltaTime;
        if (_model.IsFinished == false)
        {
            ManualUpdate(deltaTime);
        }
    }
#endif

    // 手動Update
    private void ManualUpdate(float deltaTime)
    {
        _character.ManualUpdate(deltaTime);
        _time.ManualUpdate(deltaTime);
    }

まず、Update時に行いたい処理自体をManualUpdateメソッドに切り出します。そして、高速シミュレーション時には、FixedUpdateメソッドが走らないようにし、代わりにFastPlayメソッドを呼ぶようにします。FastPlayメソッドの中では、終了するまでwhile文でdeltaTimeを渡しつつManualUpdateを呼び出し続けます。これにより、実際のdeltaTimeを待たずに高速にUpdate処理を回すことができるようになります。

変更後のTimePresenterのコードを次に示します。

using UniRx;
using UnityEngine;

public class TimePresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private TimeView _view;

    // Model
    public TimeModel Model { get; private set; }

    // 初期化
    public void Initialize()
    {
        Model = new TimeModel();

        Bind();
    }

    // 手動Update
    public void ManualUpdate(float deltaTime)
    {
        AddTime(deltaTime);
    }

    // 時間を進める
    private void AddTime(float deltaTime)
    {
        Model.SetTime(Model.Time + deltaTime);
    }

    // Bind処理
    private void Bind()
    {
#if !FAST_PLAY
        _model.TimeProperty.Subscribe(_view.SetTime).AddTo(this);
#endif
    }
}

ログを表示するためのみの処理ですが、TimePresenterのModelをpublicで公開するようにします。こちらもFAST_PLAYで切り分けても良いのですが、今回はシンプルに公開してしまいます。

以上で高速シミュレーション自体は行うことができるようになりました。

現状の高速シミュレーションの設計を図で表すと次のようになります。

Model側にもツリー構造を作成する

高速シミュレーションを行うことができるようになったものの、今回のようにViewの処理を全て呼ばないように切り分ける手法は大規模ゲームでは現実的ではありません。そこで次のようにツリー構造を見直します。

この変更により、GameとCharacterとTimeのModelとPresenterが大きく変更されます。

詳細は後ほど見ていきますが、大きな変更点は次の点になります。

  • GameModelのコンストラクタでModelのツリー構造を生成
  • ロジックの処理をPresenterからModelに移動
  • CharacterPresenterやTimePresenterは初期化時にModelを生成していたが、初期化時に生成済みのModelを渡す形に変更
  • Update処理を高速に回す処理をGamePresenterからGameModelに移動

それでは各々の変更後のコードをModelとPresenterのみ紹介します(Viewには変更は入りません)。

変更後のGame

変更後のGameのModelとPresenterのコードを次に示します。

using UniRx;

public class GameModel
{
    // キャラクターのModel
    public CharacterModel CharacterModel { get; private set; }

    // 経過時間のModel
    public TimeModel TimeModel { get; private set; }

    // Game設定
    public GameSettingModel GameSetting { get; private set; }

    // ゴールまでの距離
    public float GoalDistance { get; private set; }

    // ゴールしたかどうか
    private ReactiveProperty<bool> _isFinishedProperty;
    public IReadOnlyReactiveProperty<bool> IsFinishedProperty
    {
        get { return _isFinishedProperty; }
    }
    public bool IsFinished
    {
        get { return _isFinishedProperty.Value; }
    }

    // コンストラクタ
    public GameModel()
    {
        GameSetting = new GameSettingModel();

        CharacterModel = new CharacterModel(GameSetting.CharacterSpeed);
        TimeModel = new TimeModel();

        GoalDistance = GameSetting.GoalDistance;
        _isFinishedProperty = new ReactiveProperty<bool>();

        SetEvents();

#if FAST_PLAY
        FastPlay();
#endif
    }

    // 高速プレイ
    private void FastPlay()
    {
        while (IsFinished == false)
        {
            // 今回は説明のために下のログで依存させてしまっているが、
            // UnityEngineに依存させないために、deltaTimeを自前で計算する
            ManualUpdate(1f / 60f); // 60fps
        }
        // 今回はシンプルにログを出すためUnityEngineにアクセスする
        UnityEngine.Debug.LogFormat("Time:{0}", TimeModel.Time);
    }

    // 手動Update
    public void ManualUpdate(float deltaTime)
    {
        if (IsFinished)
        {
            return;
        }
        CharacterModel.ManualUpdate(deltaTime);
        TimeModel.ManualUpdate(deltaTime);
    }

    // イベント設定
    private void SetEvents()
    {
        CharacterModel.OnDistanceChangedCallback = OnDistanceChanged;
    }

    // 進行距離が変更された際に呼ばれる
    private void OnDistanceChanged(float distance)
    {
        if (distance >= GoalDistance)
        {
            SetIsFinished(true);
        }
    }

    // ゴールしたかを設定
    public void SetIsFinished(bool isFinished)
    {
        _isFinishedProperty.Value = isFinished;
    }
}

GameModelにはGamePresenterからロジックの処理を移動し、Update処理を高速に回す処理も移動します。また、コンストラクタ内でCharacterModelとTimeModelを生成し、Modelのツリー構造を生成します。

using UnityEngine;
using UniRx;

public class GamePresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private GameView _view;

    // Model
    private GameModel _model;

    // キャラクターのPresenter
    [SerializeField]
    private CharacterPresenter _character;

    // 経過時間のPresenter
    [SerializeField]
    private TimePresenter _time;

    // Start処理
    private void Start()
    {
        _model = new GameModel();

#if FAST_PLAY
        return;
#endif

        _character.Initialize(_model.CharacterModel);
        _time.Initialize(_model.TimeModel);

        Bind();
    }

#if !FAST_PLAY
    // Update処理
    private void FixedUpdate()
    {
        var deltaTime = Time.fixedDeltaTime;
        _model.ManualUpdate(deltaTime);
    }
#endif

    // Bind処理
    private void Bind()
    {
        _model.IsFinishedProperty
            .Subscribe(_view.SetGoalVisible)
            .AddTo(this);
    }
}

GamePresenterはロジックの処理や高速Updateの処理をGameModelに移動したためシンプルになりました。そして、CharacterPresenterやTimePresenterを初期化する際に、GameModel内で生成したCharacterModelとTimeModelを渡すように変更します。

また、ロジックの処理がModelで完結しているため、高速シミュレーション時はCharacterPresenterやTimePresenterは不要になります。そこで、Start処理でGameModelを生成した後、FAST_PLAYだったらreturnするようにし、高速シミュレーション時は、CharacterPresenterやTimePresenterの初期化も行わないようにします。これにより、Presenter内でのViewの処理チェックも行う必要がなくなりました。

変更後のCharacter

変更後のCharacterのModelとPresenterのコードを次に示します。

using System;
using UniRx;

public class CharacterModel
{
    // 速度
    public float Speed { get; private set; }

    // 進行距離
    private ReactiveProperty<float> _distanceProperty;
    public IReadOnlyReactiveProperty<float> DistanceProperty
    {
        get { return _distanceProperty; }
    }
    public float Distance
    {
        get { return _distanceProperty.Value; }
    }

    // 進行距離が変更された際に呼ばれるCallback
    public Action<float> OnDistanceChangedCallback;

    // コンストラクタ
    public CharacterModel(float speed)
    {
        Speed = speed;
        _distanceProperty = new ReactiveProperty<float>();
    }

    // 手動Update
    public void ManualUpdate(float deltaTime)
    {
        Move(deltaTime);
    }

    // 移動
    public void Move(float deltaTime)
    {
        var distance = Distance + Speed * deltaTime;
        SetDistance(distance);
    }

    /// 進行距離の値を設定
    public void SetDistance(float distance)
    {
        if (Distance != distance)
        {
            OnDistanceChangedCallback?.Invoke(distance);
        }
        _distanceProperty.Value = distance;
    }
}

CharacterModelにはCharacterPresenterからロジックの処理を移動します。

using UniRx;
using UnityEngine;

public class CharacterPresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private CharacterView _view;

    // Model
    private CharacterModel _model;

    // 初期化
    public void Initialize(CharacterModel model)
    {
        _view.Initialize();

        _model = model;

        Bind();
    }

    // Bind処理
    private void Bind()
    {
        _model.DistanceProperty.Subscribe(_view.SetDistance).AddTo(this);
    }
}

CharacterPresenterはロジックの処理をCharacterModelに移動したためシンプルになりました。そして、初期化時にCharacterModelを生成していた処理を、初期化時に渡された生成済みのCharacterModelを使用するように変更します。また、高速シミュレーション時にそもそも初期化が行われなくなったため、Presenter内でのViewの処理チェックは行う必要がなくなりました。

変更後のTime

変更後のTimeのModelとPresenterのコードを次に示します。

using UniRx;

public class TimeModel
{
    // 経過時間
    private ReactiveProperty<float> _timeProperty;
    public IReadOnlyReactiveProperty<float> TimeProperty
    {
        get { return _timeProperty; }
    }
    public float Time
    {
        get { return _timeProperty.Value; }
    }

    // コンストラクタ
    public TimeModel()
    {
        _timeProperty = new ReactiveProperty<float>();
    }

    // 手動Update
    public void ManualUpdate(float deltaTime)
    {
        AddTime(deltaTime);
    }

    // 時間を進める
    private void AddTime(float deltaTime)
    {
        SetTime(Time + deltaTime);
    }

    // 経過時間を設定
    public void SetTime(float timer)
    {
        _timeProperty.Value = timer;
    }
}

TimeModelにはTimePresenterからロジックの処理を移動しました。

using UniRx;
using UnityEngine;

public class TimePresenter : MonoBehaviour
{
    // View
    [SerializeField]
    private TimeView _view;

    // Model
    private TimeModel _model;

    // 初期化
    public void Initialize(TimeModel model)
    {
        _model = model;

        Bind();
    }

    // Bind処理
    private void Bind()
    {
        _model.TimeProperty.Subscribe(_view.SetTime).AddTo(this);
    }
}

TimePresenterはロジックの処理をTimeModelに移動したためシンプルになりました。そして、初期化時にTimeModelを生成していた処理を、初期化時に渡された生成済みのTimeModelを使用するように変更しました。また、高速シミュレーション時にそもそも初期化が行われなくなったため、Presenter内でのViewの処理チェックは行う必要がなくなりました。

最終的な高速シミュレーションの設計を図で表すと次のようになります。

変更結果に関して

改めてFAST_PLAYのチェック処理を行なっている箇所を見直してみると、次の位置のみになります。

  • GameModelのUpdate処理の切り替え
  • GamePresenterのStart内のreturn処理
  • GamePresenterのFixedUpdateメソッドを呼ばれなくする処理

今回、ロジックを全てModelに寄せて、Presenter側のツリー構造の頂点にあたるGamePresenterのStart処理の頭で処理を切ったため、GamePresenter以外のPresenterではそもそも高速シミュレーションを考慮しなくてよくなりました。つまりこれは、インゲームのコードが大規模化したとしても、FAST_PLAYのチェック処理が増えないことを表しています。

そして、今回はログを表示するため一箇所でUnityEngineを参照してしまいましたが、ModelはMonoBehaviourを継承していないため、Unityから切り出してシミュレーションを回すことも可能です。ただ、その際は当たり判定を自前実装したり、シングルトンもMonoBehaviourを継承しない形で生成したりと、少しUnityから外れた実装をする必要が生じる可能性はあります。

また、今回はユーザー操作のないインゲームの高速シミュレーションを題材としましたが、ユーザー操作が入ってくる場合には別途ユーザー操作の代わりとなるAIを用意する必要も出てきます。

今回は高速シミュレーションを1回だけ回す処理をFastPlayメソッドとして用意しましたが、インゲームに乱数が入ってくる場合は、FastPlayメソッドを複数回呼ぶことで、乱数によって結果がどのくらい変わるかを確認することができます。

そして、FastPlayを呼ぶたびにデッキ編成や装備の組み合わせを変えることで、複数パターンのシミュレーションも一気に回すことができます。

ちなみに、乱数を固定してシミュレーションを回せるようにしておくと、普段の実装で機能追加やリファクタリングを行なった際に、処理が変わったりしていないかもシミュレーションを回すことでザックリですが確認することができて便利です。

おわりに

本記事では、MVPアーキテクチャを用いて、高速シミュレーションを回せるようにするためのインゲーム設計を紹介しました。

私は普段の実装では、アウトゲームは最初にあげた例のPresenterのツリー構造で作ることが多く、インゲームは今回のようなPresenterとは別でModel側でもツリー構造を用意して作ることが多いです。

ただ、これに関しては私も正解が分かっていないため、こういう考え方もある、程度に捉えてください。

本記事が皆さまの今後の実装に少しでも役に立てば幸いです。

ユニティちゃんライセンス

※本作品はユニティちゃんライセンス条項の元に提供されています

【そのほかの記事をみる】

おすすめ記事