29
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Life is Tech ! #1Advent Calendar 2020

Day 6

UniRxをミニゲームを作りながら学ぶ

Last updated at Posted at 2020-12-05

はじめに

本記事はLife is Tech!Advent Calendar 6日目の記事です!
ぜひ最後まで目を通していってください〜よろしくお願いします!><

さて、タイトルにもある通り今回はUnityの便利ライブラリ、UniRxについての入門解説記事を書いていこうと思います。

ですが、僕自身もUniRxを初めて知った頃は、

  • 何から学べば良いのか分からない。
  • ネットの解説記事やコードを見ていても何が書いてあるかが分からない。

などなど何度も挫折しました。
ただやはりそんな中でも、無理やりでも書いていくと何となく分かるようになったので、今回は

  • Unityでゲームを少しでも作ったことがある人
  • UniRx学んでみたい・書いてみたいけど何すれば良いか分からない人

などの人でも読み進めていけて、書き方が何となくでも身につくことを目指す記事にしたいなと思います。
(事前にLINQなどの知識があるとより学びやすいです。)

ということで今回は簡単なミニゲームを作りながらUniRxを使用しない記法・使用した記法を並べ、主にUniRxでの書き方を身につけていきましょう。
細かい部分まで詳しく説明していくとキリがないので、本記事で詳しく触れていないよく分からない部分に関しては、自分でも随時調べながら進めていくと尚良しです!

UniRxは使えるようになるととても便利だと思うので、初心者や学びたての方でも、後々のために存在だけでも知っておいて欲しいなと思います。
それではいってみましょう。

今回作るゲーム

アドベントカレンダーっぽくサンタを登場させたかったので、
サンタがプレゼントを集めたらクリアのシンプルなゲームにしましょう。

last.gif

サンタがプレゼントを集める……非常に謎ですがそこは触れないでください。

実装

0. プロジェクトの準備

0-1. プロジェクトのダウンロード

もし今回の記事を自分でも書きながら進めたい人がいましたら、下のリンクからまずプロジェクトをダウンロードしてください。
サンタやステージのアセット(無料)は事前に入れてある & 今回触れないコードは少しですがすでに事前に書いてあります。
Unityのバージョンは 2019.1.14f を使用しています!

プロジェクトURL : https://github.com/kaku710/unirx_learn_project

0-2. Mainシーンを開く

Scenes/Mainを開いて、画像のようになっていたら大丈夫です!
スクリーンショット 2020-12-04 19.32.50.png

ここから実際に機能をつけていきます!

1. プレイヤーが動く処理

それではまずはサンタを動かしていきましょう。
Create→C# Scriptから、SantaController.csを作ります。

UniRx無しでシンプルに実装すると以下の感じでしょうか。

実装例 (UniRx無し)

SantaController.cs
using UnityEngine;

public class SantaController : MonoBehaviour
{
    float speed = 5;

    void Update()
    {
        Vector3 v = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        
        Move(v);
    }
    
    void Move(Vector3 v)
    {
        transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime);
    }
}

これをUniRxを用いて実装すると以下のように実装することができます。

実装例 (UniRx使用)

SantaController.cs
using UnityEngine;
using UniRx; // UniRx使用時は忘れずに
using UniRx.Triggers; // UpdateAsObservable使用に必要

public class SantaController : MonoBehaviour
{
    float speed = 5;
    
    void Start()
    {
        this.UpdateAsObservable()
            .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
            .Subscribe(v => Move(v));
    }

    void Move(Vector3 v)
    {
        transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime);
    }
}

軽く解説

ここやっていることとしては

  1. UnityのUpdate関数をStart内でUpdateAsObservable()を利用してストリーム (一連の処理の流れのようなもの) というものに変換
    ( ストリームについて詳しく知りたい人はこちら )
  2. Select()を利用して入力の値を取得
  3. SubScribe()で関数を登録する

というような流れです。

この実装だけでは何でストリームに変換する必要があるの?など、メリットがパッとしない人も多いと思いますが、これをしておくことで例えば今後

  • bool型のisGameOverがfalseの時だけ動かしたい

などの時には、

this.UpdateAsObservable()
    .Where(_ => !isGameOver) // この1行でMove関数を呼ぶ条件をフィルタリングできる
    .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
    .Subscribe(v => Move(v));

Where()を利用してこんな感じで簡単に記述できたり、更に条件としてよくあるような

  • 入力がない時はMove関数を実行したくない

などの時には、

this.UpdateAsObservable()
    .Where(_ => !isGameOver) 
    .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
    .Where(v => v.magnitude > 0f) // この1行でさらに入力があるかどうかをフィルタリングする
    .Subscribe(v => Move(v));

もう1度Where()を利用してこのようにフィルタリングすることで実装することができます。
Where()if()のような役割ですね。

ちなみにこのようなフィルタリングはUniRxにはとても便利なものが多く用意されています。ちなみに後でもいくつか出てくる。先に色々見たい方はこちら↓
逆引き記事 : UniRx オペレータ逆引き メッセージのフィルタ

UpdateAsObservable()を使用するメリット

個人的に感じる、UpdateAsObservable()を使用するメリットとしては、

  • Update内のネストが深くなるのを防げる
  • 複雑になればなるほど可読性が上がる

こんなところです。
メリットなどについてもっと詳しく知りたい!という方は、こちらの記事にもより詳しく書いてあるので、興味のある方はぜひ読んでみてください。

ここら辺はよく分からない方というも、今はとりあえずこんな感じで書き換える方法もあるんだ!くらいに思ってもらえればOKです。どんどん使ってけば慣れていくはず!

(コードが書けたらサンタにアタッチするのを忘れずに)
santa_walk.gif
ゲームを再生するとサンタが無事動いてくれました!(進行方向に向くコードは事前にSantaRotator.csに書いてあります。)
それでは次いきましょう。

2. プレゼントに当たった時の処理

サンタがプレゼントに当たったら、プレゼントを削除 → カウント用の変数を増やす、の流れでいきましょう。

Prefabsフォルダ配下に"Present"のプレハブがあるので、3つ程ステージ上に適当に配置してください。
また、タグを事前に"Present"に設定しています。

そしたら先ほど作成したSantaController.csにコードを追加していきます。

まずUniRx無しだと以下のような感じでしょうか。(追記分のみ記載)

実装例 (UniRx無し)

SantaController.cs

public class SantaController : MonoBehaviour
{
    int presentCount = 0; // プレゼントを数える変数

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Present"))
        {
            Destroy(other.gameObject);
            presentCount++;
        }
    }
}

これをUniRxを用いて以下のように実装します。

実装例 (UniRx使用)

public class SantaController : MonoBehaviour
{
    int presentCount = 0; // プレゼントを数える変数
    
    void Start()
    {
        this.OnTriggerEnterAsObservable()
            .Where(o => o.CompareTag("Present"))
            .Subscribe(o =>
            {
                Destroy(o.gameObject);
                presentCount++;
            });
    }
}

軽く解説

こちらもStart内でOnTriggerEnter関数OnTriggerEnterAsObservable()を利用してストリームに変換しています。
このようにUniRx.Triggersを使うとUnityが用意しているコールバックをストリームにし、全てをAwake/Start内にまとめて記述することが可能になります。

ちなみにUnityで用意されているコールバックイベントはほぼ全て用意されているそう。すごい。
Wiki : https://github.com/neuecc/UniRx/wiki/UniRx.Triggers

補足 : ストリームを終了したい時

例えばこの衝突関係のストリームをゲームの途中で停止したい時があったとします。
そんな時は以下のように実装することで終了することができます。

IDisposable disposable;
    
void Start()
{
    // ストリームをIDisposableに格納
    disposable = this.OnTriggerEnterAsObservable()
                     .Where(o => o.CompareTag("Present"))
                     .Subscribe(o =>
                     {
                         Destroy(o.gameObject);
                         presentCount++;
                     });
}

void StopSubscribeStream()
{
    disposable.Dispose(); // 停止したいタイミングでこのようにDispose()を実行
}

このような感じでいつでも停止することもできます。めっちゃ便利。補足でした。

それではゲームがきちんと動くか確かめましょう!
collect_present.gif
このようにサンタが当たったらプレゼントが消えていればOKです!

続いては、ここで用意した変数 presentCount を用いてゲームクリア条件をつけていきましょう。

3. ゲームクリア条件をつける

サンタがプレゼントを一定数(今回は3つ)集めたらクリアにしたいのですが、
UniRx無しで愚直に書くとこんな感じでしょうか。Clearシーンは事前に用意しています。

実装例 (UniRx無し)

SantaController.cs

public class SantaController : MonoBehaviour
{
    const int CLEAR_PRESENT_COUNT = 3;

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Present"))
        {
            Destroy(other.gameObject);
            presentCount++;
            // ここから追記
            if(presentCount >= CLEAR_PRESENT_COUNT)
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Clear");
            }
        }
    }
}

ReactiveProperty

上記のクリア条件をUniRxを用いて実装したいのですが、UniRxを用いると、

変数の監視→値の変更を検知してActionを実行

といったようなことが簡単に行えるようになります。例えば、HPを監視して変更を検知したらHPバーに反映させたりとか。
それを実現してくれるのがUniRxのReactivePropertyです。
これは本当に便利なので、使えるようになるとよりレベルアップできると思います。

とりあえず手を動かして動くことを確認してみましょう。

まず先ほどint型で宣言したpresentCountを**ReactiveProperty<int>**型で宣言しなおします。

ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);

またそれに伴って、presentCount++の部分がエラーになると思うので、以下のように書き換えてあげます。

presentCount++; → presentCount.Value++;

これで変数が監視できるようになったので、Start内で値の変更を監視するようにします!以下が実装例です。

実装例 (UniRx使用)

SantaController.cs
public class SantaController : MonoBehaviour
{
    const int CLEAR_PRESENT_COUNT = 3;
    ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);
    
    void Start()
    {
        presentCount
            .Where(x => x >= CLEAR_PRESENT_COUNT)
            .Subscribe(_ =>
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Clear");
            });
    }
}

こんな感じで実装することができます!とても便利ですね。

ReactivePropertyのメリット

これも色々あるかとは思いますが、個人的には

  • MVPパターンに非常に有用なこと

が1番のメリットかなと思います。(あくまで個人的意見です)
MVPパターンとはデザインパターンの1つで、この設計はゲームの設計 (だけじゃ無く他のプロダクトでも) をしていく上でとても便利なのでぜひ知っておくと良いと思います。
この記事内では長くなるので解説しませんが、もしUnityでのMVPパターンについて詳しく知りたい方は以下の神記事を読むと良いです。(マジで僕もめちゃくちゃ読んだ)

MVPパターンに関して今はあまり分からなくても、ReactivePropertyに関しては非常に便利なので、少しずつでも慣れていくと良いと思います。

それではゲームを確かめてみましょう!
to_clear.gif
このようにプレゼントを3つ集めたらClearシーンに遷移すればOKです!

4. リトライ機能をつける

最後はボタンを押した時のイベントについて。ゲーム的にもせっかくなのでClearシーンからMainシーンに戻れるようにしましょう。
ボタンは事前に置いていますが、クリックした時のイベントの設定はしていませんのでそれをUniRxを用いて行ってみましょう。

UniRx無しの実装は

  • publicで関数を定義してUGUIで紐付け
  • onClick.AddListner()を使用
    このあたりかなと。こちらの実装例は省略します。

適当にc#ファイルを新規で作成して、

実装例 (UniRx使用)

ClearPresenter.cs
using UniRx;
using UnityEngine;

public class ClearPresenter : MonoBehaviour
{
    public UnityEngine.UI.Button retryButton; // Inspectorで設定
    
    void Start()
    {
        retryButton.OnClickAsObservable()
            .Subscribe(_ =>
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Main");
            });
    }
}

こんな感じで書いてあげれば実装できます。

UniRxを用いてボタンのイベントを実装すると色々と便利で、例えば、1回押したら1秒は入力を受け付けないボタン、なども

button.OnClickAsObservable()
            .ThrottleFirst(System.TimeSpan.FromSeconds(1)) // 1秒間入力を受け付けない
            .Subscribe(_ =>
            {
               // 押した時の処理
            });

こんな感じで1行追加してあげるだけで実装することができます!UniRx使わないとなると結構めんどくさそうなので、とても便利ですね。他にも色々あるので気になる人は調べてみてください。

では最後にゲームが動くかの確認をしましょう!
last.gif
このようにボタンを押してMainシーンに戻ることができていたらOKです!

最後に念のため、実装した最低限のSantaController.csの全文も載せておきます。

SantaController.cs
using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class SantaController : MonoBehaviour
{
    float speed = 5;
    const int CLEAR_PRESENT_COUNT = 3;
    ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);
    
    void Start()
    {
        this.UpdateAsObservable()
            .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
            .Subscribe(v => Move(v));

        this.OnTriggerEnterAsObservable()
            .Where(o => o.CompareTag("Present"))
            .Subscribe(o =>
            {
                Destroy(o.gameObject);
                presentCount.Value++;
            });

        presentCount
            .Where(x => x >= CLEAR_PRESENT_COUNT)
            .Subscribe(_ =>
            {
                UnityEngine.SceneManagement.SceneManager.LoadScene("Clear");
            });
    }

    void Move(Vector3 v)
    {
        transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime);
    }
}

さいごに

UniRxについて初めて記事を書いてみましたが、いかがだったでしょうか。
今回紹介したUniRxの機能は本当にほんの一部で、まだまだ便利な機能がいっぱいあります!自分もまだまだ勉強中です。

もっとUniRxについて詳しく知りたい・学びたい方は、

などなど読んでみると良いと思います。ちなみに本記事の参考もここです。(toRisouP様様ですね。)

ぜひ今後のゲーム開発に少しでも取り入れるきっかけになれば幸いです。

ではでは明日以降のLife is Tech!Advent Calendarもお楽しみに!ありがとうございました〜!

29
10
0

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
29
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?