アールフォース流!Unityチーム開発 ~アーキテクチャ編~

目次


はじめに


はじめまして、アールフォース・エンターテインメント(以下アールフォース)でエンジニアをしております波多野です!

今回はR-FORSCHOOLの第一回目のテック記事を担当させていただきます。

さて今回のテック記事のお題ですが、『アールフォース流!Unityチーム開発とは~アーキテクチャ編~』となっています。

ゲーム開発エンジンといったらUnityと言われるほどメジャーになりましたが、アールフォースでもUnityを使った開発がメインになっています。

そこでアールフォースではどのようにUnityを使ってチーム開発をしているのかご紹介していきたいと思います!

今回の記事では開発アーキテクチャについてご紹介します。

※このテック記事ではUnityの初歩知識を有していることが前提になっておりますのでご注意ください。

チーム開発で気をつけたいこと


Unityでの開発は自由度が高いです。

だからこそ開発時にしっかりとした開発ルールを定めなければ、各開発者が統一性のない開発を行ってしまいます。

その結果、

  • 可読性の低いコーディング
  • メンテナンス性の悪い実装
  • 解決困難なバグが生まれる

といった開発に大きな影響を与えてしまうことがあります。

そこでアールフォースでは、より円滑にチーム開発を進めるためにどのような開発アーキテクチャでチーム開発を行っているのか、説明していきたいと思います。

主な開発アーキテクチャ


2020年2月現在のアールフォースのUnity開発では主にMV(R)Pパターンで実装を行っています。

MV(R)Pパターンとは簡単に説明すると一般的にMVPパターンとUniRxを組み合わせたアーキテクチャになります。 MVPパターンとは

  • Model(データ ViewもPresenterも知らない)
  • View (表示 ModelもPresenterも知らない)
  • Presenter (ModelとViewの仲介役 ModelとViewを監視している)

に分けてプログラムを組むアーキテクチャになります。

概念図はこうなります。

f:id:rf-blog-sagyo:20200217183856p:plain

UniRxは上記画像のModelの値の変更の検知に使用しています。

UniRxを追加した概念図はこんな感じです。

f:id:rf-blog-sagyo:20200217183911p:plain

MV(R)Pパターンの詳細について記載していくと、それだけで記事が埋まってしまうので割愛させて頂きますが、実際にどのような感じでUnityの実装に落とし込んでいるのか説明していきます!

例) +10ボタンを押したとき、0の値が10ずつ加算されるSampleのMV(R)Pの実装例

f:id:rf-blog-sagyo:20200217183930p:plain

/// 
///  @brief モデルクラス
/// 

using UniRx;

namespace RFORSCHOOL
{
    /// <summary>
    /// モデルクラス
    /// </summary>
    public class SampleModel
    {
        /// <summary>
        /// カウント
        /// </summary>
        private ReactiveProperty<int> _count;

        /// <summary>
        /// カウントのプロパティ
        /// </summary>
        public IReadOnlyReactiveProperty<int> Count { get { return _count; } }
        
        /// <summary>
        /// コンストラクタ(初期化処理)
        /// </summary>
        public SampleModel()
        {
            _count = new ReactiveProperty<int>(0);
        }

        /// <summary>
        /// カウントを追加する
        /// </summary>
        public void AddCount(int count)
        {
            _count.Value += count;
        }
    }
}
/// 
///  @brief ビュークラス
/// 

using System;
using UnityEngine;
using UnityEngine.UI;

namespace RFORSCHOOL
{
    /// <summary>
    /// ビュークラス
    /// </summary>
    public class SampleView : MonoBehaviour
    {
        /// <summary>
        /// 加算するカウントの定数
        /// </summary>
        private static readonly int AddCount = 10;
        
        /// <summary>
        /// 数字を表示しているテキスト
        /// </summary>
        [SerializeField]
        private Text _countText = null;
        
        /// <summary>
        /// +10ボタン
        /// </summary>
        [SerializeField]
        private Button _countUpButton = null;

        /// <summary>
        /// +10ボタンが押されたときのリスナー
        /// </summary>
        public Action<int> OnCountUpButtonClickedListener = null;

        /// <summary>
        /// 初期化
        /// </summary>
        public void Initialize()
        {
            OnCountUpButtonClickedListener = null;
            _countUpButton.onClick.AddListener(OnCountUpButtonClicked);
        }

        /// <summary>
        /// +10ボタンが押されたときのイベント
        /// </summary>
        private void OnCountUpButtonClicked()
        {
            OnCountUpButtonClickedListener(AddCount);
        }

        /// <summary>
        /// カウントの切り替えが呼ばれる
        /// </summary>
        public void OnChangeCount(int count)
        {
            //カウント表示を切り替える
            _countText.text = count.ToString();
        }
    }
}
/// 
///  @brief プレゼンタークラス
/// 

using UnityEngine;
using UniRx;

namespace RFORSCHOOL
{
    /// <summary>
    /// プレゼンタークラス
    /// </summary>
    public class SamplePresenter : MonoBehaviour
    {
        /// <summary>
        /// ビュー
        /// </summary>
        [SerializeField]
        private SampleView _view = null;

        /// <summary>
        /// モデル
        /// </summary>
        private SampleModel _model = null;

        /// <summary>
        /// Start処理
        /// </summary>
        private void Start()
        {
            Initialize();
        }

        /// <summary>
        /// 初期化
        /// </summary>
        private void Initialize()
        {
            _model = new SampleModel();
            _view.Initialize();
            Bind();
            SetEvent();
        }

        /// <summary>
        /// ViewとModelの紐付け
        /// </summary>
        private void Bind()
        {
            //Countの値に変更が入ったときにOnCountChangeが呼び出される
            _model.Count
                .Subscribe(OnCountChange)
                .AddTo(gameObject);
        }

        /// <summary>
        /// イベントを設定
        /// </summary>
        private void SetEvent()
        {
            //+10ボタンを押したときのイベントをセット
            _view.OnCountUpButtonClickedListener = OnCountUpButtonClicked;
        }

        /// <summary>
        /// Viewから送られてきたカウントでモデルを更新する
        /// </summary>
        private void OnCountUpButtonClicked(int count)
        {
            _model.AddCount(count);
        }

        /// <summary>
        /// Modelのカウントが切り替わったときに呼ばれる
        /// 引数はModelのCountと同じものが返ってくる
        /// </summary>
        private void OnCountChange(int count)
        {
            _view.OnChangeCount(count);
        }
    }
}

いかがでしょうか?

こちらの実装を概念図に当てはめるとこんな感じになります。

f:id:rf-blog-sagyo:20200217210812p:plain

一見クラス数も多くなり、余計にややこしくなったのでは?と思う方もいると思います。

しかし各クラスに要素を分けることによって、作業者のコーディングに個性が出にくくなり可読性が上がり、拡張性も広がります!

それによって、コードのメンテナンス性が高まり開発速度も向上します!!

自由度の高いUnityチーム開発では、いかに各作業者の個性をなくすことが重要となります。

Model、View、Presenterと、役割を明確にすることで、バグが発生したときの早期発見、早期解決や新規機能の追加のしやすさに繋がります。

また開発アーキテクチャだけでなく、Inspector上からPrefabを操作したり、値を操作するときは[SerializeField]をつけるようにするなど、コーディング規約をしっかりと定めることも重要な要因となります。

終わりに


今回はMV(R)Pパターンの説明でしたが、これはUnity開発における一つの回答であって、正解というわけではありません。

開発の一つの手法として参考にしていただけると嬉しいです。

引き続きアールフォースの文化をガンガン発信していきますので、是非またR-FORSCHOOLに足を運んでいただけると幸いです。

ここまで読んで頂きありがとうございました。