LoginSignup
5
2

More than 5 years have passed since last update.

SPA 脳で Unity 入門

Last updated at Posted at 2019-04-05

最近 Unity を勉強し始めました。
私自身は普段 Web 開発が中心なので Unity 開発を Web フロントエンド開発に喩えて理解しようとしましたが、「Web も Unity も同じなんだ!」という境地に至ることは出来ませんでした。しかし Web フロントエンドと Unity で近い点、近づけられる点、違う点という軸で考えることである程度理解を進めることが出来たので、その観点で開発を振り返ってみようと思います。

当記事では開発した Unity プロジェクトを基に上記観点を踏まえながら開発の要所を解説します。開発手順をなぞってはいきますが、完全なチュートリアルにはなってないことをご承知下さい。比較先の Web フロントエンド開発とは React.js による SPA を念頭に置いています。

作ったもの

最初に何を作るべきか迷いましたが、Web フロントエンドとの比較という観点で EC サイト的なアプリケーションを題材にしました。具体的なテーマは Blender 入門時にみんな作ったであろうマグカップの販売店舗。

リポジトリ: https://github.com/223kazuki/MugMarket

Unity-2018.3.10f1-Personal-MainScene.gif

  1. 棚に並んだマグカップをクリックしてカートに入れる。
  2. カートの中身を確認し購入ボタンからを押す。
  3. 購入リクエストを送信し結果を表示。

開発準備

今回の開発 OS は Windows 10 にしました。現在は Mac でも Unity 開発は可能ですし Unity 自体マルチプラットフォームなゲームを開発出来ることが強みではありますが、クライアントとしてのシェアを考えるとやはり Windows での開発が無難だと思います。

VR ゲーム開発などでない限りはグラボ増設は必要ないと思いますが、別件の steamVR 開発で必要だったため増設しました。初めてのグラボ増設でよく理解せずグラボを購入してしまい結局電源も交換する事態になってしまったので、必要 PC スペックは事前によく確認することをお勧めします。

Unity インストール

記事執筆時点での最新版(Unity 2018.3.10)をインストールしました。Unity Download Assistant をダウンロードし、実行すれば C# 用の IDE として Visual Studio 2017 もインストールされるはずです。

 https://unity3d.com/jp/get-unity/download/archive

Choose Components ではマルチプラットフォームとしての Unity を試したいので WebGL Build Support にチェックを追加しておきます。Unity プランは無料の Personal を選択します。インストール完了には Unity アカウントが必要になるのでサインアップします。

複数バージョン Unity の共存

Unity 開発ではパッケージが Unity バージョンにセンシティブなことがあり、複数バージョンを使い分けないといけないことがあります。バージョンの共存についてはよい方法がなく、公式も「インストール先のフォルダ名で管理してね」と言っているので、最初から検討しておきましょう。

Unityを複数バージョンインストール

今回は別途試したい VRChat SDK が 2017.4.15f1 でしか動作しないため、下記の 2 バージョンを共存させました。

  • 2017.4.15f1 ... インストール先: C:\Program Files\Unity_2017
  • 2018.3.10f1 ... インストール先: C:\Program Files\Unity_2018

2019/4/9 追記
現在は Unity Hub を使えば複数バージョンの管理は簡単に出来るようです。

プロジェクト作成

Unity エディターを起動し、New から新規プロジェクトを作成します。

01.png

Template には 3D を選択します。

パッケージインストール

Unity でも開発する上で便利な様々なパッケージ(Asset や C# ライブラリなど)が公開されています。パッケージのインストール方法はいくつかありますが追加パッケージは基本的にプロジェクト以下に直接インストールされ、git repository にも追加して管理することになります。Unity 2018 からは依存関係を manifest.json というファイルで宣言的に管理する Unity Package Manager という仕組みが提供されていますが、まだ公開されているパッケージが少なく既存の管理方法を捨て去ることは難しそうです。

image.png

今回は下記の二つのパッケージを使用することにします。

Standard Assets は Unity エディター上で Asset Store からインストール出来ます。Unidux は unitypackage ファイル をダウンロードし、Unity メニューの "Assets" -> "Import Package" -> "Custom Package" から開くことでインストールできます。

git 管理

Unity プロジェクトにはビルド成果物や管理不要なファイルが作成されるため、適切に gitignore する必要があります。ただ詳しくないので下記の .gitignore を参考として追加しました。

前述のように追加したパッケージも git 管理下に含めます。(気持ち悪いけど)

シーン開発

Unity ゲームはシーン(.unity ファイル)と呼ばれる単位で開発します。シーンとはゲーム画面の 3D 空間モデルであり、そこに GameObject(モノや UI など)を配置してゲームを作り上げていきます。Web フロントエンドとの対比で捉えるのであれば DOM と DOM ノードにあたります。Unity ゲームはシーンを Main Camera というオブジェクトから見た映像として描画されます。3D 空間上では X, Z 軸が水平面(地面)方向、Y 軸が垂直(空)方向となります。

シーンの開発は Unity エディターから行います。開発には主に次の Hierarchy, Scene, Game, Inspector パネルを使用することになります。

  • Hierarchy ... 現在開いているシーン上の GameObject 一覧がツリー表示され、選択や追加が行える。
  • Scene ... シーンを 3D 表示上で直接操作できる。
  • Game ... Main Camera から見たシーン(つまりゲーム画面)を見ることが出来る。
  • Inspector ... 選択した GameObject など Asset の各種設定を行える。

image.png

Scene パネルの操作

Scene パネル上でのオブジェクトの操作は下記ツールを使い分けて行います。
image.png

左から、

  • Hand Tool ... Scene パネル上の視点移動
  • Move Tool ... GameObject の移動
  • Rotate Tool ... GameObject の回転
  • Scale Tool ... GameObject のスケール変更

まず、Hand Tool により操作対象オブジェクトが見やすい位置に視点を移動します。Altを押しながら画面をドラッグすると視点の角度も変更できます。しかし通常はいずれかの軸方向からの視点でないと操作が難しいため Scene パネル右上の三軸アイコンをクリックすることで適切な軸方向に視点を移動します。適切な視点を設定したら Tool を切り替え、対象オブジェクトを選択して移動、回転、スケールを加えて調整していきます。

GameObject の配置

マグカップを配置するテーブルとして Cube を追加してみましょう。Hierarchy パネル上で右クリックし 3D -> Cube で立方体が追加されます。前述の方法で Cube をカメラと光源の間あたりに移動しスケールを調整します。ゲーム上でどう見えるかは Game パネルで確認します。

オブジェクト表面の模様は Material というアセットをオブジェクトに追加することで設定できます。Project パネルから Asset/Materials というフォルダを作成し、その中で右クリック -> Create -> Material で新規作成します。Material を選択すると Inspector に各種設定が表示れるので Shader = Naturl/Speed Tree, Main Color = 茶などに設定してください。Material 作成後、それを Scene パネル上の Cube にドラッグすると Texture として貼り付けられます。

image.png

地面の作り方

地面はビルトインの Terrain というオブジェクトとして作成できます。Hierarchy パネルから作成すると巨大なサイズなのに中途半端な位置に作成されてしまいます。サイズ変更は Interceptor -> Terrain -> Mesh Resolution の Terrain Width, Length から変更できます。位置は Scene パネルを Y 軸方向から見てちょうどいい位置に置いてください。細かく設定するのであれば Terrain Width, Length = 500, 500 なら Transform Position は (X, Y, Z) = (-250, 0, -250) くらいで最初のオブジェクト群が中央に来ます。

image.png

メニュー画面の追加

UI オブジェクトを配置することで Unity 上に Web 画面的なものを表示させることができます。今回はこれを使ってメニュー画面を作りました。とりあえず文字を表示させるには Hierarchy パネル上から右クリック -> UI -> Text を選択。追加すると Canvas, Canvas/Text, EventSystem という三つのオブジェクトが作成されます。EventSystem は UI への入力などをつかさどる非表示オブジェクトです。Canvas が各 UI の親オブジェクトです。デフォルトで作成すると超巨大な Canvas が作成され、Main Camera に映ることができません。また Camera が水平方向を向いていない場合、文字も傾いて表示されることになってしまいます。これを直すためには Canvas の Inspector を開き、Canvas -> Render Mode を Screen Space - Camera, Render Camera に Scene 上の Main Camera, Plane Distance には 1 などを設定してください。こうすることで、Canvas はカメラに紐づき、常にカメラから見える位置に表示されるようになります。

image.png

UI とその他の GameObject を共存させようとすると開発時に Scene パネルで邪魔になることがあります。そういう時はパネル右上の Layers プルダウンから UI を無効化すれば表示を消すことが出来ます。

Prefab 作成

商品のマグカップはサーバから取得したデータなどを基にして動的に表示されるべきです。動的に GameObject を生成するためには Unity の Prefab という仕組みを使います。Prefab は GameObject の雛形で、オブジェクト指向言語でいうクラスの概念にあたり、スクリプトから GameObject 化することが出来ます。

今回は Blender で作成したマグカップのモデルを Prefab 化します。まず Blender からエクスポートした fbx ファイルを Scene パネルにドラッグ&ドロップすれば GameObject としてマグカップが作成されます。

image.png

更に作成されたオブジェクトを Hierarchy パネルから Project パネルにドラッグ&ドロップするとマグカップの Prefab ファイルが作成されます。

image.png

注意としては、fbx から生成した GameObject には Collider(当たり判定を行うための物理的実体)がついていないため、手動で追加する必要があります。これをやらないとスクリプトからクリック判定を行うこともできません。Mesh Collider という Collider を使えば、GameObject の Mesh から Collider を生成してくれます。

image.png

作成した Prefab のスクリプトへの紐付けは後述します。

スクリプト開発

Unity でユーザとのインタラクションを実現したりゲーム状態を導入するためにはスクリプトを書く必要があります。

スクリプト言語にはいくつか選択肢がありますが現時点では C# がデファクトスタンダードです。

2019/4/9 追記
現在は過去に使えた JavaScript と Boo は選択できないようになっているため C# 一択だそうです。

スクリプトは Web フロントエンドで喩えれば勿論 JavaScript にあたりますが実行され方が異なります。JavaScript ではスクリプトはブラウザに読み込まれたタイミングで評価されますが、Unity ではスクリプトは GameObject に紐づく属性 (コンポーネント)の一つとして定義されており、それぞれ紐づいた GameObject のライフサイクルに応じて実行されます。

実際に C# スクリプトを実行するには MonoBehaviour というクラスを継承したクラスを実装し、それを Unity エディター上でコンポーネントとして GameObject にアタッチする必要があります。そうするとゲーム実行時に該当クラスのインスタンスが作成され、GameObject のライフサイクルに応じたオーバライドメソッドが実行されます。

Execution Order of Event Functions

ライフサイクルと言われると React Component に喩えたくなりますが、ライフサイクルを持つ実体が React ではスクリプト側に存在するのに対し、Unity ではゲームエンジンが管理する GameObject であるため一緒に考えてしまうと混乱します。

image.png

Unidux

React.js との比較観点では Unity のスクリプトには VDOM のような便利なビューの差分更新インターフェースがないため、GameObject の更新は明示的に実装する必要があります。更に GameObject の物理演算はゲームエンジンが行ってしまうため、スクリプト上の状態と GameObject の対応はある程度のところで折り合いをつけなければなりません。このように GameObject の監視と更新が散在してしまうため、それらの相互作用を管理するのは難しくなります。

そこで Unidux を導入します。Unidux は UniRX という Unity の RX 実装を使って Unity に Redux パターンを導入できるパッケージです。これを使うことで散在する GameObject の監視と更新をシングルトンな状態を介して行えるようなることで簡潔化できます。
Unidux を使ったアプリケーションは JavaScript における Redux パターンと同様に下記の要素で構成されます。

  • Action
  • ActionCreator
  • Store
  • State
  • Reducer

React+Redux との違いで言えば、前述したように状態を監視して GameObject に反映させる部分は自力で実装しないといけないという点です。以降は Unidux を利用した Unity アプリケーションの開発にフォーカスして解説していきます。

スクリプト構成

以下が今回 Unidux を使って開発したプロジェクトのスクリプト構成です。

Assets/Scripts
   ├── Mug.cs                          // マグカップの Entity クラス
   ├── App.cs                          // Unidux アプリケーション : MonoBehaviour
   ├── State.cs                        // アプリケーション状態
   ├── Action.cs                       // アクションクラスとアクションクリエーター
   ├── Middleware.cs                   // HTTP 通信などの非同期処理を呼ぶためのミドルウェア
   ├── Reducer.cs                      // リデューサー
   ├── Dispatcher                      // GameObject を介したユーザー操作を受けて Action
   │   ├── CreateMugsDispatcher.cs     // をDispatch するためのクラス群 : MonoBehaviour
   │   ├── KeyInputDispatcher.cs       // 
   │   ├── MugClickDispather.cs        // 
   │   └── PurchaseButtonDispatcher.cs // 
   └── Renderer                        // State の更新を監視し、GameObject に反映するため
       ├── BaseListRenderer.cs         // のクラス群 : MonoBehaviour
       ├── CartRenderer.cs             // 
       ├── ICellRenderer.cs            // 
       ├── MenuRenderer.cs             // 
       ├── MessageRenderer.cs          // 
       ├── MugRenderer.cs              // 
       └── MugsRenderer.cs             // 

スクリプトと GameObject の対応関係

前述したように Unity におけるスクリプトは GameObject に紐付けられて初めて実行されます。そのため Entity や Action など別のクラスからインスタンス化されるクラス以外は GameObject にアタッチされる必要があります。

image.png

Unity エディタ上で GameObject にアタッチしたスクリプトには、public なメンバ変数にその他の Asset を指定することも出来ます。そのため GameObject とスクリプトの対応関係は柔軟に出来てしまいますが、スクリプトの性質と GameObject のライフサイクルから考える必要があります。今回のプロジェクト構成で言うと GameObject を監視、更新するのは Dispatcher と Renderer なので、これらはそれぞれ対象の GameObject にアタッチします。また、App.cs も GameObject を操作するわけではありませんがゲーム実行時にインスタンス化される必要があるため空の GameObject にアタッチする必要があります。

  • App.cs <-> GameMaster という名前の Empty GameObject
  • XxxDispatchet.cs <-> 基本的に Dispatcher を呼び出すユーザ操作を受け付ける GameObject
  • XxxRenderer.cs <-> 基本的に State の変更を反映させる GameObject

実装詳細

それでは各クラスの実装を詳細に見ていきます。

State

State は StateBase 継承クラスとして実装します。メンバとして StateElement 継承クラスを持たせることで、監視する際の更新判定を細分化することも可能です。StateElement は SetStateChanged メソッドを持つので、これを Reducer 内で呼び出すことで状態の変更を明示し、監視先でフィルタリング出来ます。

Assets/Scripts/State.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unidux;

namespace MugMarket
{
    [Serializable]
    public class State : StateBase
    {
        public bool MenuOpend;
        public MugsState MugsState = new MugsState(); // マグカップ一覧を管理する子状態
        public CartState CartState = new CartState(); // カートの状態を管理する子状態
    }

    [Serializable]
    public class MugsState : StateElement
    {
        public uint Index = 0;
        public List<Mug> Mugs = new List<Mug>();
    }

    [Serializable]
    public class CartState : StateElement
    {
        public Dictionary<uint, int> Cart = new Dictionary<uint, int>(); // MugId -> 個数
        public string PurchaseSuccessMessage = null; // 購入リクエスト送信時の成功メッセージ
    }
}

Dispatcher

Dispatcher はユーザからの操作などに応じて Action を発行するためのクラスです。React では React Component 内に関数として実装されますね。GameObject にアタッチし、GameObject の状態を監視することでユーザからの操作を受け付けます。対象の操作があったら Action を生成して Diapatch します。

Assets/Scripts/Dispatcher/PurchaseButtonDispatcher.cs
using UnityEngine;
using UnityEngine.UI;
using UniRx;

namespace MugMarket.Dispatcher
{
    [RequireComponent(typeof(Button))] // Unity エディタ上でアタッチされる GameObject を Button に制限
    public class PurchaseButtonDispatcher : MonoBehaviour
    {
        private Button Button;

        void OnEnable()
        {
            Button = this.GetComponent<Button>(); // アタッチされている GameObject の取得
            Button.OnClickAsObservable() // クリックを監視
                .TakeUntilDisable(this)
                .Subscribe(_ => this.Dispatch()) // クリックされたら実行
                .AddTo(this);
        }

        void Dispatch()
        {
            App.Store.Dispatch(ActionCreator.Purchase()); // Action を生成して Dispatch
        }

    }
}

Reducer

Reducer では Dispatch された Action に応じて State を更新して新しい State を生成します。状態の一部(StateElement)の更新を明示的に示したい場合は SetStateChanged で更新フラグを立てます。

Assets/Scripts/Reducer.cs
using Unidux;
using UnityEngine;
using System.Collections.Generic;

namespace MugMarket
{
    public class Reducer : ReducerBase<State, Action>
    {
        public override State Reduce(State state, Action action) // state と action を受け取って state を返す
        {
            switch (action.ActionType)
            {
                case ActionType.ToggleMenu:
                    state.MenuOpend = !state.MenuOpend;
                    state.CartState.PurchaseSuccessMessage = null;
                    state.CartState.SetStateChanged();
                    break;
                case ActionType.CreateMug:
                    var createMugAction = (CreateMugAction)action;
                    state.MugsState = AddMug(state.MugsState, createMugAction.Mug.Name, createMugAction.Mug.Price, createMugAction.Mug.Color);
                    break;
                case ActionType.AddToCart:
                    var addToCartAction = (AddToCartAction)action;
                    state.CartState = AddToCart(state.CartState, addToCartAction.MugId);
                    break;
                case ActionType.PurchaseSuccess:
                    var purchaseSuccessAction = (PurchaseSuccessAction)action;
                    if (purchaseSuccessAction.StatusCode == 200)
                    {
                        state.CartState = PurchaseSuccess(state.CartState);
                    }

                    break;
            }
            return state;
        }

        // StateElement の更新
        public static MugsState AddMug(MugsState state, string name, int price, string color)
        {
            state.Mugs.Add(new Mug(
                id: state.Index,
                name: name,
                color: color,
                price: price
            ));
            state.Index = state.Index + 1;
            state.SetStateChanged(); // 変更を明示
            return state;
        }
        // ...略
    }
}

Renderer

Renderer は State の変更を GameObject に反映するためのクラスです。反映先の GameObject にアタッチして開始ライフサイクル(Start)で State のサブスクライブを開始し、State の更新に応じて GameObject を更新するようにします。
Unidux が提供する App.Subject は UniRx のオペレータが使えます。そのため、監視対象の StateElement を絞る場合は IsStateChanged を使って Where フィルタリングしたり出来ます。

Assets/Scripts/Renderer/CartRenderer.cs
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace MugMarket.Renderer
{
    [RequireComponent(typeof(Text))] // アタッチされる GameObject を Text に制限
    public sealed class CartRenderer : MonoBehaviour
    {
        private Text Text;

        void Start()
        {
            Text = this.GetComponent<Text>(); // アタッチされた GameObject の取得
            App.Subject
                .StartWith(App.State)
                .Where(state => state.CartState.IsStateChanged) // 監視する状態をフィルタリング
                .Subscribe(state =>
                {
                    string cartText = "Cart\n\n";
                    int price = 0;
                    foreach (Mug m in state.MugsState.Mugs)
                    {
                        if (state.CartState.Cart.ContainsKey(m.Id))
                        {
                            var count = state.CartState.Cart[m.Id];
                            cartText += m.Name + " : " + m.Price + " : " + count + "\n";
                            price += m.Price * (int)count;
                        }
                        else
                        {
                            state.CartState.Cart.Add(m.Id, 1);
                        }
                    }
                    cartText += "\nTotal : " + price;
                    Text.text = cartText; // 状態変更を GameObject に反映
                })
                .AddTo(this);
        }
    }
}

スクリプトから GameObject 動的生成

今回、表示するマグカップの一覧は、State で管理する Mug Entity の一覧を監視して動的に GameObject 化します。これを実現するためには前述の Prefab を使います。下記が State で管理する Mug のリストから動的に Mug を GameObject として生成している Renderer です。実際には下記クラスを継承した MugsRenderer をテーブルにアタッチし、マグカップの Prefab をメンバとして指定して。

Assets/Scripts/Renderer/BaseListRenderer.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace MugMarket.Renderer
{

    public class BaseListRenderer<TRenderer, TRenderValue> : MonoBehaviour
    where TRenderer : MonoBehaviour, ICellRenderer<TRenderValue>
    {
        private List<TRenderer> objectPool = new List<TRenderer>();

        // ...略

        // 継承先からアタッチされた Prefab(Mug.prefab) が渡される
        private TRenderer AllocObject(Transform parentTransform, TRenderer prefab)
        {
            var _object = GameObject.Instantiate(prefab) as TRenderer; // prefab を指定して Instantiate で GameObject 生成
            _object.transform.SetParent(parentTransform, true);
            _object.gameObject.SetActive(false);
            objectPool.Add(_object); // 生成した GameObject への参照は objectPool として管理
            return _object;
        }

        // ...略
    }
}

GameObject を動的生成する上で難しい点は、生成した GameObject はあくまでもゲームエンジン側の実体であるため、再描画や更新に対応するためにはスクリプト側でもその参照を管理しないといけない点です。また、生成される GameObject クラスはスクリプト側から見て意味のあるエンティティの ID なども設定できないため、生成した GameObject とエンティティの対応方法も検討しないといけません。特にリスト要素の GameObject 化は複雑でなかなかよい実装が思いつかず、Unidux のサンプルほぼそのままになってしまいました。

非同期処理

Purchase ボタンを押したら最終的に購入情報を HTTP で POST することにします。HTTP 通信のような非同期処理は Redux パターンでは Middleware を使って実現するのが一般的です。Middleware は Reducer が呼び出される前後で呼び出される処理を設定するパターンで、App.cs 内で Unidux Store に設定しています。

Assets/Scripts/Middleware.cs
using System;
using System.Collections;
using Unidux;
using UniRx;
using UnityEngine;
using UnityEngine.Networking;
using MiniJSON;

namespace MugMarket
{
    public class Middleware
    {
        public GameObject InjectGameObject { get; set; }

        public Middleware(GameObject gameObject)
        {
            this.InjectGameObject = gameObject;
        }

        // HTTP リクエスト送信ミドルウェア
        public System.Func<System.Func<object, object>, System.Func<object, object>> Process(IStoreObject store)
        {
            return (System.Func<object, object> next) => (object action) =>
            {
                if (action is Action && ((Action)action).ActionType == ActionType.Purchase && this.InjectGameObject != null)
                {
                    var json = Json.Serialize(App.State.CartState.Cart);
                    // コルーチンの Observable 化
                    Observable.FromCoroutine<Action>(observer =>
                            Request(
                                "https://httpbin.org/post",
                                json,
                                data =>
                                {
                                    observer.OnNext(data);
                                    observer.OnCompleted();
                                },
                                error => observer.OnError(new Exception("Network error"))
                            )
                        )
                        .Subscribe(data => next(data), error => next(error))
                        .AddTo(this.InjectGameObject); // Observable を GameObject に紐付ける
                }

                return next(action);
            };
        }

        private IEnumerator Request(string url, string body, Action<Action> success, Action<string> error)
        {
            Debug.Log("POST: " + body);
            UnityWebRequest getRequest = UnityWebRequest.Post(url, body);

            yield return getRequest.SendWebRequest();

            if (getRequest.isNetworkError)
            {
                error.Invoke(getRequest.error);
            }
            else
            {
                // 成功時のコールバックで Action を Dispatch
                var entity = ActionCreator.PurchaseSuccess((int)getRequest.responseCode, getRequest.downloadHandler.text);
                success(entity);
            }
        }
    }
}

ビルド

最後に開発したアプリをビルドします。Unity エディタから File -> Build Settings を開き、せっかくマルチプラットフォームを試したいので WebGL を選択し Build を実行します。

image.png

生成された index.html をサーバに乗せれば一先ずは何も問題なく動きます。実際にはちゃんと気にしないといけないことは多いようですが。

「真のマルチプラットフォーム対応に成功しているのはゲーム界隈」という言葉を聞きましたが偽りはなさそうです。素晴らしい!

まとめ

当記事では Web 開発との比較を交えつつ Unity 開発を振り返りました。
Unity 開発と Web フロントエンド開発は似ている面もありますが同じ前提で捉えると理解を妨げる点が多いです。特に Unity の Monobehaviour と React.js の Component を同じようなものと捉えようとしたことは混乱を生みました。最初は Unity は Unity と割り切って入門した方が無難だったかもしれません。一方、アプリケーションの状態の管理という問題は React.js と同様に Redux パターンを導入することで簡潔に取り扱うことができました。Unity 開発には Web 以上の自由さを感じますし、無理に Web のパターンを適用することはないですが、複雑なゲームを開発する上で Web フロントエンドの考え方は Unity でも有効でした。

ある程度 Unity が理解できたので次は Clojure で Unity 開発ができる Arcadia を試してみたいと思います。

参考

5
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2