エンジニア

Bolt+ZenjectでInGameAPIを作ってみた①

投稿日:2020年10月8日 更新日:

こんにちは。エンジニアの安藤です。
クライアントとサーバーが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で公開します

  1. 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の参照を持っています。

  1. 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導入を行います。

  1. AssetStoreからZenjectをダウンロード&インポート
    Extenjectとタイトルに出ますが中身はZenjectなのでご安心ください。
    バージョンは9.2.0を使っています。

  2. 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);
    }
    }
  3. 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で実装するところまで解説していきたいと思います。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-, ,

© WonderPlanet Inc.