Skip to content

UniRxを使ってゲームの進行を管理する手法

Posted in 技術記事

先日「第9回1週間ゲームジャム「あつい」に参加したよという話」という記事をアップしました。
その中でタイトルにある内容を紹介したのですが、概要しか書いてなかったのと、リクエストがあったということで詳しい解説をしようかと思います。

環境

Windows7 Professional 64bit
unity2017.4.9f1
UniRx Ver5.5.0
UnityHub 0.20.1

UniRxとは

UniRxとはReactive Extensions for Unityというライブラリの通称で、UnityのAssetStoreから無料でダウンロードできます。
Reactive Extensionsってなんぞやという感じですが、時間に関連する処理を簡単に書けるようになるやつ、位に最初は考えていればいいかなと思います。
今回は数ある機能のうち、ReactivePropertyというものを使います。

UniRxの導入方法

その前に、使えるようにする方法です。といっても、アセットストアからダウンロードしてインポート、UniRxを使いたいC#スクリプトの行頭でusingするだけです。簡単。

using UnityEngine;
using UniRx;
public class HogeManager : MonoBehaviour {

ReactivePropertyとは

ReactivePropertyとは、値の監視が簡単に出来るように拡張した変数というイメージです。次の例にあるように色々な種類が用意されており、必要に応じて従来の変数を置き換えて使えます。ReactiveCollection(ReactivePropertyのList版)なんてものもあります。

  • IntReactiveProperty…Int型のReactiveProperty
  • BoolReactiveProperty…Bool型のReactiveProperty
  • FloatReactiveProperty…Float型のReactiveProperty
  • etc

値の読み方

ReactivePropertyの値を読み書きする時は、以下のように直接ではなく.valueを使う必要があります。

IntReactiveProperty i;
i = 5; //NG
i.value = 5; //OK

UniRx導入前の例

通常のenumで監視をしてみる

一旦話は変わって、UniRxを使わないパターンを、自分が1週間ゲームジャムで投稿した作品を例に解説します。

プレイヤー(中央の丸)が死んだ瞬間のgif画像ですが、このタイミングは色々やることがあります。画像では死亡エフェクトだけですが、実ゲームでは壁の停止処理、ゲームオーバー表示、ツイートやランキングボタンの表示もあります。

プレイヤーが死んだ瞬間を把握する

これはプレイヤーと壁にコライダーをつけ、OnTriggerEnter2Dを使えば分かります。壁にぶつかったら死亡エフェクトを表示します。なお、簡素化のため死亡条件の判別は省略します。

Player.cs(抜粋)
private void OnTriggerEnter2D(Collider2D other)
{
DeathEffect(); //いい感じのエフェクト
}

その他の処理

同じタイミングで壁を停止させたりするので、ついここに書きたくなります。しかしそうするとPlayer.csがどんどん膨らんでしまい神となってしまいます。ミニゲーム程度なら問題ありませんが、神はメンテナンスも拡張も拒む厄介な存在なため、ここは勉強のため分割していくことにします。

Enumを導入

そこでゲームの進行状況を保持しておくEnumを作成、GameManagerでそのEnumを持ち、そこを他から監視して適切な処理を行います。

GameManager.cs(抜粋)
public class GameManager : MonoBehaviour
{
public GameState _GameState; //この値を見て皆がゲームの状態を知る
}
GameState.cs
public enum GameState
{
None,
Opening,
Gaming,
Death,
Result,
}

リザルトへの遷移

プレイヤーが死んだ場合は必ずリザルト画面に移動するので、Player.csでGameStateを変更します。

Player.cs(抜粋)
private void OnTriggerEnter2D(Collider2D other)
{
DeathEffect(); //いい感じのエフェクト
_GameManager._GameState = GameState.Result;
}

壁の動作

壁はGameStateがGamingの間は閉じる→閉じきったら開いてまた閉じるを繰り返すので、updateで処理します。

WallAction.cs(抜粋)
public class WallAction : MonoBehaviour
{
public GameManager _GameManager;
void Update()
{
if (_GameManager._GameState == GameState.Gaming) CloseWall(); //だんだん閉じる、閉じたら開く
}
}

プレイヤー死亡時の壁

GameStateがResultになるため、壁は自動で停止します。ただ初期位置に戻したいので、ResetWall()を追加…したいところですが、1回だけ処理したいのでUpdateで回すのは駄目です。かといってGameStateの監視が必要なのでUpdateから外せません。ということで、boolでフラグを1つ作り、1回だけ処理するようにします。

WallAction.cs(抜粋)
public class WallAction : MonoBehaviour
{
public GameManager _GameManager;
bool isActed = false;
void Update()
{
if (_GameManager._GameState == GameState.Gaming) CloseWall();
if (_GameManager._GameState == GameState.Result) ResetWall();
}
void ResetWall(){
if(isActed)return;//実行済みなら戻る
//いい感じにリセットする処理
isActed = true;
}
}

通常版実装完了

これで完了…ではなく、isActedをどこかでfalseにしなければいけません。今回のケースではGameStageがGamingの時にfalseにすれば良さそうです。
しかし、今後GameStateや機能が増えた時、例えばポーズ機能の追加やステージクリア型になった時など、フラグの処理タイミングが増えていきます。この例ではまだ1箇所2箇所ですが、リザルト表示や音の処理が加わると管理する数が乗算されていきます。
そうなるとバグが増えていくので、いい方法はないものか…といったところでReactivePropertyの出番です。

UniRx導入例

EnumReactiveProperty?

残念ながら、EnumReactivePropertyというものはありません。ただしEnumをReactivePropertyにする方法があるのでそれを利用します。ポイントは以下の通り。

  • using UniRx;を記述
  • 専用のC#ファイルを1つ作成する
  • GameStateReactiveProperty、GameStateは分かりやすい名前を付ければOK
  • Scene中に配置する必要はない
GameStateReactiveProperty.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
public enum GameState
{
None,
Opening,
Gaming,
Death,
Result,
}
[System.Serializable]
public class GameStateReactiveProperty : ReactiveProperty
{
public GameStateReactiveProperty() { }
public GameStateReactiveProperty(GameState initialValue) : base(initialValue) { }
}

監視の仕方

先に、GameStateを上で作成したGameStateReactivePropertyで書き換えます。
監視する場合はStart()にあるような書き方をします。また前述の通り、_GameStateを_GameState.valueに書き換える必要があります。

GameManager.cs(抜粋)
public class GameManager : MonoBehaviour
{
public GameStateReactiveProperty _GameState;
//この値を見て皆がゲームの状態を知る
}
WallAction.cs(抜粋)
public class WallAction : MonoBehaviour
{
public GameManager _GameManager;
void Start()
{
_GameManager._GameState
.DistinctUntilChanged()
.Where(x => x == GameState.Result)
.Subscribe(_ => ResetWall());
}
void Update()
{
if (_GameManager._GameState.value == GameState.Gaming) CloseWall();
}
void ResetWall(){
//いい感じにリセットする処理
}
}

isActedがなくなり、UpdateやResetWallがスッキリしました。ポイントはStart()内の記述です。ReactivePropertyはそのままではただの変数なので、このような記述で色々な動作を設定します。
_GameManager._GameState
.DistinctUntilChanged() //値が変化した時のみ動作する
.Where(x => x == GameState.Result) //値(x)がGameState.Resultの時動作する
.Subscribe(_ => ResetWall()); //以上の条件に当てはまればResetWall()を1度実行する

まとめると「_GameManagerの_GameStateがResultに変化した時ResetWall()を1度実行する」と言う設定をしたことになります。
この動作条件は他にも様々なものがあります。例えば最初の1回のみ実行、◯秒遅延させる、などです。詳しくは記事最後のリンク先をご覧ください。

副次的な効果

…コードの記述とやりたい処理(思考の流れ)がかなり近いことにお気づきでしょうか?isActedフラグを利用する場合、コードの全体像を把握出来ていないと、どんな意図で実装されているかがわかりにくくなります。単独小規模のスプリント開発なら問題になりませんが、数ヶ月放置したコードというのは自分でも読むのが案外大変です。
始めは慣れないかもしれませんが、この辺りもUniRxを利用するポイントの一つになってくると思います。

まとめ

ReactivePropertyを使うと値の変更を検出するのが簡単
UniRxは処理の意図が分かりやすい

最後に

いかがだったでしょうか。Enumでゲームの状態を管理、がトピックでしたが、HPやステータス、スコアの表示をするのにも使えたりします。ReactivePropertyを使って、計算と表示(uGUI,textの操作)を分離させるイメージです。特にRPGのステータスのような場合、計算式や表示場所が二転三転したりなど日常茶飯事なので、スクリプトを分割出来るとメンテがしやすくなります。
今回の内容はUniRxのほんの一部であり、細かい説明も全くしていません。というより勉強中なので分からない事だらけです。もし内容に誤り等ありましたら是非ご連絡ください。疑問ご意見マサカリ等々お待ちしております。

もっと詳しく知るには

以下のページが参考になります。
UniRxを導入するメリット ~こういう時にUniRxは使えるよ~
UniRx入門シリーズ
UniRx公式リポジトリ

Be First to Comment

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です