Android開発者がUnityの初心者向けレッスンをMVVM(UniRx + Zenject)で書き直してみる

5 min読了の目安(約4800字TECH技術記事

普段はKotlinでAndroidアプリをメインに開発しつつ、Pythonでバックエンド開発したり、JSでフロントエンド開発したりしています。

そして今回、Unityで開発をすることになりました。Unity(もといゲーム開発プラットフォーム)は初めてだったので、開発環境に慣れつつ、Unityでのプログラム設計を考えるために、Unityが公式で公開している「Roll A Ball」という初心者向けレッスンを自分なりの設計で書き直してみることにしました。

設計を考えるにあたっては、QiitaでUnityの記事をたくさん書かれている@toRisouPさんの記事やスライドが非常に参考になりました。特に参考になった記事を貼っておきます。

これらの記事を読むとわかるのですが、UnityにもReactiveProgrammingができるUniRxとDIができるZejectというライブラリがあります。どちらもLiveDataとDagger(あるいはKoin)を使っているととても馴染みやすい作りになっています。

ということで、UnityにもAndroidライクなMVVMが持ち込めるのではないかと思い、「Roll A Ball」をMVVMで書き直しました。

前置きが長くなりましたが、完成品はこちらになります。Unityプロジェクトとして読み込めばそのまま使えるようになっています。

dforest/unity-roll-a-ball-mvvm

MVVMとは?

まずはMVVMについて復習です。MVVMはModel-View-ViewModelの略で、ViewModelが持つ状態をViewが購読して、状態が変わったことをトリガーに最新の状態がViewに配信される仕組みを設計することで、ViewModel自身がViewに依存しないようになり(Modelも同様にViewModelに依存しない)、MVPに比べて疎結合にできる設計(という私の理解)です。Viewが状態によって変化するようなアプリケーションがよりスッキリと書けるようになります。

MVPとの大きな違いはViewModelがViewの存在を知らないことです。こうすることで、ViewModelは純粋に状態だけを管理できるようになり、Viewはその状態を受け取って(Subscribeして)、反映させれば良くなります。

ボールの移動をMVVMで書き直す

「Roll A Ball」で一番最初に実装することになる、ボール(プレイヤー)をMVVMで書き直してみた例です。まずはレッスンそのままのコードがこれです。

using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour {

   public float speed;
   private Rigidbody rb;

   void Start ()
   {
       rb = GetComponent<Rigidbody>();
   }

   void FixedUpdate ()
   {
       float moveHorizontal = Input.GetAxis ("Horizontal");
       float moveVertical = Input.GetAxis ("Vertical");
       Vector3 movement = new Vector3 (moveHorizontal, 0.0f, moveVertical);
       rb.AddForce (movement * speed);
   }

}

これをキーボード入力を取得する部分、キーボード入力値を持つViewModel、値をボールに反映させる部分に分けます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
using Zenject;
using RollABall.ViewModels;

namespace RollABall.UI {
   public class KeyboardInput : MonoBehaviour
   {
       [Inject]
       InputViewModel inputViewModel;

       void Start()
       {
           //XとZの入力をVector3に変換してViewModelに通知
           this.UpdateAsObservable()
               .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
               .Subscribe(vector => inputViewModel.UpdateAxis(vector));
       }
   }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

namespace RollABall.ViewModels {
   public class InputViewModel : MonoBehaviour
   {
       private readonly ReactiveProperty<Vector3> _axis = new ReactiveProperty<Vector3>();

       //ViewではIterfaceにキャストしたものを利用して、Subscribe専用とする
       public IReactiveProperty<Vector3> Axis => _axis;

       //Viewからの値の通知は必ずメソッドを通して行う
       public void UpdateAxis(Vector3 vector) {
           _axis.Value = vector;
       }
   }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
using Zenject;
using RollABall.ViewModels;
using RollABall.Configs;

namespace RollABall.Player {
   public class PlayerMove : MonoBehaviour
   {
       [Inject]
       InputViewModel inputViewModel;

       void Start()
       {
           var rig = GetComponent<Rigidbody>();

           //inputViewModel.AxisはUpdateで値が更新される
           //しかし、RigidbodyにはFixedUpdateで値を反映したいので、
           //WithLatestFromで最新の値だけ取り出してFixedUpdateで反映する
           this.FixedUpdateAsObservable()
               .WithLatestFrom(inputViewModel.Axis, (_, axis) => axis)
               .Subscribe(axis =>
               {
                   rig.AddForce(axis * gameConfig.playerSpeed);
               });
       }
   }
}

Inputの値をAddForceに与えるときに注意すべきはタイミングです。InputはUpdateのタイミングで取得していますが、RigidbodyはFixedUpdateで操作する必要があるため、WithLatestFromを使って、変換作業を行っています。これについては以下の記事に詳しく書かれています。

【UniRx】Update()タイミングのイベントをFixedUpdate()のタイミングに変換する - Qiita

こんな感じで、全体をMVVMの設計をベースに書き直したのがこのリポジトリの内容になっています。

dforest/unity-roll-a-ball-mvvm

UnityでMVVMがAndroid開発者には入りやすい

KotlinでAndroid開発している身としては、UniRx + Zenjectの組み合わせでMVVMで設計し、C#で開発するのはかなり入りやすいのではないかと思いました。

この設計方針でUnityでアプリを作ってみましたところ、非常に相性が良かったです。そのあたりは、Zennで本にできればいいなと思っています。

なにか質問や問題点の指摘などあれば、コメントかTwitter(@d_forest)にいただけると嬉しいです。