こんにちは。エンジニアの安藤です。
クライアントとサーバーがAPIによって機能が切り分けられているように
デザインとクライアントもAPIで機能を切り分けたいなと思ったことはありませんか?
本記事ではInGame(ソーシャルゲームでいうバトル画面)の演出の実装をAPIで切り分ける手法を解説していきたいと思います。
記事はZenjectによって依存性が解決されたInGameAPIの実装とInGameAPIを使ったBoltの実装の2つにわけてご紹介していきます。
環境
MacOS 10.15.6
Unity 2019.4.3f1
Bolt 1.4.12
Zenject 9.2.0
DOTween 1.2.420
サンプル
実際にBolt+Zenjectで作ったサンプル動画になります。
素材はこちらを使わせてもらっています。
サンプル設計概要
サンプルは入力したコマンドをRepositoryに保存しBoltがそれらを解析してキャラクターを操作する流れになってます。
このBoltからRepositoryにアクセスしたりキャラクターに命令を与えるところをInGameAPIとして提供しています。
Zenjectによって依存性が解決されたInGameAPIの実装
機能の実装
Zenjectで依存性注入する機能を実装します。
実装する機能はフィールド上にあるキャラクターをコントロールするクラスです。
※詳細な実装は後日GitHubで公開します
- FieldViewの実装
ViewはSceneにあるオブジェクトの参照や生成を持ちます。
FieldViewではキャラ(Unit)の生成とキャラを生成する階層を渡しています。
using UnityEngine;
public interface IFieldView
{
IUnitView CreateUnitView(bool isPlayer, Transform parent);
Transform Contents { get; }
}
public class FieldView : MonoBehaviour, IFieldView
{
[SerializeField] PlayerView _playerPrefab = null;
[SerializeField] EnemyView _enemyPrefab = null;
public Transform Contents => this.transform;
public IUnitView CreateUnitView(bool isPlayer, Transform parent)
{
IUnitView view = null;
if (isPlayer)
{
view = Instantiate(_playerPrefab);
}
else
{
view = Instantiate(_enemyPrefab);
}
view?.Transform.SetParent(parent, false);
return view;
}
}
FieldViewのSceneとInspectorです。
Project内にあるPrefabの参照を持っています。
- FieldPresenterの実装
PresenterはViewを介してSceneの操作を行います。
ユーザー(ライブラリを使うプログラマ)はインタフェースで切られたPresenterを使い機能の実装を行います。
FieldPresenterではキャラの初期化と生成したキャラのPresenterListを渡しています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
public interface IFieldPresenter
{
void Init(IReadOnlyList<IUnitEntity> entityList);
IReadOnlyList<IUnitPresenter> UnitPresenterList { get; }
}
public class FieldPresenter : IFieldPresenter
{
IFieldView _view = null;
public FieldPresenter(IFieldView view)
{
_view = view;
}
List<IUnitPresenter> UnitPresenterList { get; } = new List<IUnitPresenter>();
IReadOnlyList<IUnitPresenter> IFieldPresenter.UnitPresenterList => UnitPresenterList;
public IUnitPresenter PlayerPresenter => UnitPresenterList.FirstOrDefault(_ => _.Entity.IsPlayer);
public IReadOnlyList<IUnitPresenter> EnemyPresenterList => UnitPresenterList.Where(_ => !_.Entity.IsPlayer).ToList();
public void Init(IReadOnlyList<IUnitEntity> entityList)
{
foreach (var entity in entityList)
{
var unitView = _view.CreateUnitView(entity.IsPlayer, _view.Contents);
var unitPresenter = new UnitPresenter();
unitPresenter.Init(unitView, entity);
UnitPresenterList.Add(unitPresenter);
}
}
}
Zenjectの環境設定
次にZenject導入を行います。
-
AssetStoreからZenjectをダウンロード&インポート
Extenjectとタイトルに出ますが中身はZenjectなのでご安心ください。
バージョンは9.2.0を使っています。
-
Installerの実装
Installerとは依存性注入を行うクラスを定義するクラスになります。
先程作ったFieldPresenterをBindします。
Bindされたクラスは[Inject]アトリビュートで受け取ることができます。
抽象化されたGlobalInstanceと思ってもらえたら良いかなと思います。using UnityEngine; using Zenject; public class Installer : MonoInstaller { [SerializeField] FieldView _fieldView = null; public override void InstallBindings() { var fieldPresenter = new FieldPresenter(_fieldView); Container.Bind<IFieldPresenter>().FromInstance(fieldPresenter); } }
-
Installerの呼び出し設定
SceneContextと先程作成したInstallerをScene上に配置したGameObjectにAddComponentしてください。
Installerには先程実装したFieldViewの参照もつなげてください。
(スクリーンショットではUIと次章でご紹介するBoltの参照もつなげてあります)
InGameAPIの実装
FieldAPIを実装します。
[Inject]アトリビュートで依存性注入されたFieldPresenterをIFieldPresenterとして受け取ります。
インタフェースによって実装が抽象化されているのでAPIクラスの内部に集中して実装することができます。
余分なコンストラクタ引数も必要ないのでGameObjectを生成したあとに初期化メソッドを呼び出す必要もありません。
IGameRepositoryは今回の説明では割愛しますがキャラクターのデータやコマンドを保持するクラスです。
FieldPresenterと同様の手順で依存性注入されています。
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Zenject;
public class FieldApi : MonoBehaviour
{
[Inject] IFieldPresenter FieldPresenter { get; }
[Inject] IGameRepository GameRepository { get; }
int _currentHandleId = 0;
List<bool> _completeList = new List<bool>();
public int MoveToTarget(int invokeUnitId, int targetUnitId)
{
var invoke = FieldPresenter.UnitPresenterList.FirstOrDefault(_ => _.Entity.Id == invokeUnitId);
var target = FieldPresenter.UnitPresenterList.FirstOrDefault(_ => _.Entity.Id == targetUnitId);
var handleId = _currentHandleId;
_completeList.Add(false);
invoke.MoveToTarget(target, () =>
{
_completeList[handleId] = true;
});
return _currentHandleId++;
}
// boltにcallbackの実装がないため苦肉の策
public bool IsPlaying(int handleId)
{
return !_completeList[handleId];
}
}
おわりに
いかがでしたでしょうか?
Zenjectの便利さが少しでも伝わりましたら幸いです。
次回はInGameAPIを使ってBoltで実装するところまで解説していきたいと思います。