概要
こんにちは!モバイルファクトリー Advent Calendar 2018 10日目担当のshioiyanです。
9日目の記事は@isaoekaさんの草ウィジェットを作った🤔でした。
本記事では@mattakさんが開発された、UnityでReduxのような状態管理が行えるようになるUniduxを使って簡単なゲームサイクルを実装してみます。
この記事を読んでわかること
- Uniduxを使った画面遷移が実装方法がわかる
- PageとSceneの紐付け方
- PageとSceneの状態管理のやり方
- PageとSceneの状態の監視方法
- Pageの変更のやり方
- Pageへのデータの持たせ方
※会社で開発しているアプリ/プロジェクトとは関係がありません。
動機
ゲームジャムなどで簡単なゲームを作っていると、いつもタイトル画面->ゲーム画面->リザルト画面->タイトル画面...という簡単な画面遷移を実装するのにそれなりの時間がかかっていることに気がつきます。
タイトル画面やリザルト画面はゲーム制作の本質的な部分ではなく、特に限られた時間で行われるゲームジャムでは、できれば時間をかけずに作りたいところです。
また、ゲームの規模が大きくなるに従い、少ない画面数ならなんとか管理できていたものがだんだん管理しきれなくなっていきます。
画面遷移の仕組みはどんなゲームを作るにしても必要です。
あらかじめ実装方法を確立しておき、ゲーム開発で本質的な部分に注力して開発できるようにしておきたいところです。
解決策
UniduxのSceneTransitionを使えば、拡張性の高い画面遷移が実装できます!
やや作業量は多いかもしれませんが、1度実装しておけばあらゆるアプリケーションで使えるはずです。
今回作るもの
- タイトル画面->ゲーム画面->リザルト画面->タイトル画面...と遷移する
- ボタンを押すと画面遷移が行われる
- タイトル画面で選択した難易度がゲーム画面で表示される
- ゲーム画面から遷移する時にスコアが渡されてリザルト画面でスコアが表示される
開発環境
- Unity v2018.2.16f1
- Unidux v0.3.4
- UniRx v6.2.2
実装
※ Uniduxの使用例にあるSceneTransitionの実装を参考にしています。
※ Unidux自体やUniRxの使い方についてはここでは触れません。
PageとSceneの紐付け
まずは使用するSceneを作成します。
今回は、どのPageにも常に存在するBaseというSceneと、Pageによって切り替わるTitle、Game、Resultの3つのSceneを作成します。
Uniduxでは1つの画面のことをPageと表現します。
Pageは1..n個のSceneの集まりです。
作成したSceneはenumで同じ名前で定義しておきます。
今回はこれらのSceneを使って3つのPage(タイトル画面、ゲーム画面、リザルト画面)を作るので、こちらもenumで定義しておきます。
次にSceneConfig.csというスクリプトを作り、各PageにどのSceneが必要かマッピングしていきます。
SceneCategoryというパラメータがあり、Permanentに設定されたSceneは常に表示されるようになります。
今回は各PageにBaseと対応するそれぞれのSceneが表示されればいいので、CategoryMapでBaseのSceneCategoryをPermanentに設定し、PageMapで各PageとSceneをマッピングします。
もしHeaderやFooterといったSceneが必要ならそれぞれPageに紐付けると良いでしょう。
SceneとPageの状態を管理
ゲームの状態を管理するクラスにSceneStateとPageStateを持たせます。
SceneWatcherとPageWatcherの作成
SceneStateとPageStateの変更を監視して実際にUnity上のSceneを変化させるSceneWatcherとPageWatcherを作成します。
ここで使われているSceneUtilでUnityお馴染みのSceneManager.LoadSceneAsync()
が実行されています。
Pageを変更する
これでSceneStateやPageStateを変更するActionが発行されるとそのActionに応じてSceneやPageが変更されるようになりました。
次にButtonが押されたらPageが変更されるようにしてみます。
例えば、リザルト画面からタイトル画面に戻るボタンのスクリプトは以下のように実装します。
using GameCycleSample.Config; using Unidux.SceneTransition; using UnityEngine; using UniRx; using UnityEngine.UI; namespace GameCycleSample.UI { [RequireComponent(typeof(Button))] public class ReturnToTitleButtonDispatcher : MonoBehaviour { void Start() { this.GetComponent<Button>().OnClickAsObservable() .Select(_ => PageDuck<Page, Scene>.ActionCreator.Push(Page.TitlePage)) .Subscribe(action => Unidux.Dispatch(action)) .AddTo(this); } } }
PageDuck<Page, Scene>.ActionCreator.Push(Page.TitlePage)
というActionをDispatchすることでタイトル画面に必要なSceneを表示させています。
また、PageDuck<Page, Scene>.ActionCreator.Pop()
というActionも用意されており、前のPageへ戻ることも行えます。
Pageにデータを持たせる
UniduxではPageをPushする時にPageにデータを持たせることができます。
PageDuck<Page, Scene>.ActionCreator.Push()
の第二引数にIPageDataを継承したクラスを渡すことができます。
今回はタイトル画面で難易度を選択できるようにし、ゲーム画面で選択した難易度が表示されるようにしました。
まずはIPageDataを継承したクラスを実装します。
using System; using GameCycleSample.Type; using Unidux.SceneTransition; namespace GameCycleSample.Struct { [Serializable] public class GamePageData : IPageData { public DifficultyType DifficultyType; // ゲームの難易度 public GamePageData() { } public GamePageData(DifficultyType difficultyType) { this.DifficultyType = difficultyType; } } }
タイトル画面で難易度を選択するボタンのスクリプトは以下のようになります。
using GameCycleSample.Config; using GameCycleSample.Struct; using Unidux.SceneTransition; using UnityEngine; using UniRx; using UnityEngine.UI; namespace GameCycleSample.UI { [RequireComponent(typeof(Button))] public class TitleButtonDispatcher : MonoBehaviour { public GamePageData GamePageData; void Start() { this.GetComponent<Button>().OnClickAsObservable() .Select(_ => PageDuck<Page, Scene>.ActionCreator.Push(Page.GamePage, this.GamePageData)) .Subscribe(action => Unidux.Dispatch(action)) .AddTo(this); } } }
Inspector上で各ボタンに応じたGamePageData
(のDifficultyType
)を設定します。
あとはPageDuck<Page, Scene>.ActionCreator.Push(Page.GamePage, this.GamePageData)
と渡してあげるとPageState.Data
にGamePageData
が格納されます。
次にPageDataを参照してTextとして表示してみます。
表示したいTextGameObject(今回はTextMeshProUGUIを使っています)に以下のスクリプトをattachします。
using GameCycleSample.Business; using GameCycleSample.Struct; using TMPro; using UnityEngine; using UniRx; namespace GameCycleSample.UI { [RequireComponent(typeof(TextMeshProUGUI))] public class DifficultyTypeTextRenderer : MonoBehaviour { public TextMeshProUGUI DifficultyTypeText; void Start() { Unidux.Subject .Where(state => state.Page.IsReady && state.Page.IsStateChanged) .Where(state => state.Page.Data is GamePageData) // Page.Dataの型チェック .StartWith(Unidux.State) .Subscribe(state => this.Render(state)) .AddTo(this); } private void Render(State state) { GamePageData pageData = state.Page.GetData<GamePageData>(); this.DifficultyTypeText.text = pageData.DifficultyType.ToString(); } } }
state.Page.GetData<GamePageData>()
で指定した型のPageDataを取得できます。
GamePageData型のPageDataが存在することを保証するために.Where(state => state.Page.Data is GamePageData)
で現在のPage.Data
がGamePageData
である場合だけ参照するようにしています。
また、.Where(state => state.Page.IsReady && state.Page.IsStateChanged)
とすることで、ゲーム中に難易度が変更されても検知して再描画するようにしています。
同様にして、ゲーム画面からリザルト画面に遷移する時にスコアを渡して、リザルト画面でスコアを表示するようにしています。
まとめ
Uniduxを用いて簡単なゲームサイクルを実装できました。
画面遷移のロジックは実装者によってバラバラな実装になりがちですが、Uniduxを使うことで統一感のある実装になるため、複数人開発する場合にも特におすすめです。
UnityでReduxライクに状態管理が行えるUniduxはとても素晴らしいので、ぜひ皆さんも使っていきましょう!
モバイルファクトリーではゲームジャム部という部活があり、文字通りゲームジャムを開催して自由にゲームを作ったりしています。
モバイルファクトリー Advent Calendar 2018 11日目はid:toricorさんの記事です。
今年もテストの話のようです。楽しみですね!!