NOOB UNITY

初心者がUnityでなんかしちゃうぞBlog

Instantiateするならば、一度は夢見るキャッシングという言葉

時代はエコ。それはUnity業界にも

今回作るのはこんな感じのものです。 クリックするとInstantiateされるのですが、ヒエラルキーを見るとひたすら新規にクローンを作るわけでなく非アクティブのものがあれば再利用していることがわかります。

f:id:yuu9048:20190414195538g:plain

Instantiate連発は避けたい事情

Instantiate を使ってゲームオブジェクトのクローンを作成するのはゲームを作っているとよくありますね。 たとえば、シューティングゲームで自機から発射する弾などは入力があったらInstantiateを行って弾を生成して飛ばす、なんていう処理をしますよね

もちろんひたすらクローンを作る処理でも動くは動きます。しかし無秩序にInstantiateを連発するのはあまりよろしくありません。 入力があるたびにクローンを作っていては、それこそドラえもんバイバインのように無限に増え続けていく恐ろしい事態になってしまいます。いずれは深刻な宇宙の栗まんじゅう汚染が起こってしまうでしょう。

f:id:yuu9048:20190414192251j:plain

オブジェクト数の問題だけならひたすらDestroyすれば良いという話になるのですが、Instantiate自体が重い処理なので再利用できるならそっちを使おうっていう感じの考え方ですね!

解決方法はいろいろある

生成数を最初からキメておく

それを解決するためのアプローチとしては、「作成したオブジェクトを可能な限り再利用する」ということが考えられます。 まず、先程のシューティングゲームであれば「プレイヤーが発射できる弾は10発まで」と明示的に決めてしまう方法が考えられます。 この場合であればまず最初に10回Instantiateで生成して確保しておけばあとはInstantiateをしないでひたすら使い回せばOKです。

動的に生成量を管理する

それ以外に、「入力量に応じて動的に確保数を決定する」という考え方も可能です。 これは、たとえばプレイヤーが20回発射したとします。まず初回はオブジェクトが無いのでInstantiateで1個作ります。このときこのオブジェクトをListなどに入れて管理します。 2発目を発射する際には、Listに入れて管理しているオブジェクトをチェックして「再利用可能」かどうかをチェックします。再利用可能であれば最初に作ったものを再利用し、再利用可能でなければ新たにInstantiateして作成します。

再利用可能かどうかというのは、フラグをもたせて管理するのが良いでしょう。たとえばシューティングゲームだったら「敵にあたった弾」や「画面外に出た弾」などはもう使用済みですよね。非表示にして再利用待機状態にまわしておき、再び発射するときにこの弾を再利用します。

とりあえずサンプルを書いてみよう

今回は、後者のListを使った動的なキャッシングに挑戦します。といってもサンプルプログラムはCubeをただ落とすだけのものです。

1. PrefabのCubeの準備

まずは、Unityプロジェクトを作成し「Cube」を作ります。 作成した「Cube」に「RigidBody」をアタッチしておきましょう。 また、下記「CubeController.cs」をC#Scriptで作成しCubeにアタッチしておきます。

CubeController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeController : MonoBehaviour
{
    void Update()
    {
        if(gameObject.transform.position.y <= -10) {
            GetComponent<Rigidbody>().isKinematic = true;
            gameObject.SetActive(false);
        }
        
    }
}

y座標が-10以下になったら、Kinematicかつ、自身を非表示にするだけのシンプルな動きです。

あとは、このCubeをPrefab化しておきます。

2. Cubeを作成するスクリプトの作成

下記コードでスクリプトを作り、適当な空のゲームオブジェクトにでもアタッチしておきます。

Test.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour
{
    [SerializeField] GameObject prefab;
    List<GameObject> cubeMaster = new List<GameObject>();

    // Update is called once per frame
    void Update()
    {

        if (Input.GetMouseButtonDown(0)) {

            bool spawnFlag = true;

            // リストに登録されているならチェック
            if(cubeMaster.Count != 0) {

                // リストの中身をすべてチェック
                for(int i=0; i<cubeMaster.Count; i++) {

                    // 非アクティブなら再利用
                    if (!cubeMaster[i].activeInHierarchy) {
                        cubeMaster[i].transform.position = Vector3.zero;
                        cubeMaster[i].GetComponent<Rigidbody>().isKinematic = false;
                        cubeMaster[i].SetActive(true);
                        spawnFlag = false;
                        break;
                    }
                }
            }

            if (spawnFlag) {
                var obj = Instantiate(prefab) as GameObject;
                cubeMaster.Add(obj);
            }
        }

    }
}

画面をクリックするとPrefabを作るというだけのシンプルなものです。 ヒエラルキーからPrefabにさきほどのCubeのPrefabをアタッチしておきましょう。

3. 実行してみよう

f:id:yuu9048:20190414195538g:plain

ヒエラルキーに注目してもらうと、クリックしたときに非アクティブのCubeがあれば再利用していることがわかります。動画の後半ではクリック数を増やしていますが、再利用可能なタイミングであればクローンを作らずに再利用してInstantiateを避けるようになっています。

Listに再利用可能なものがない場合は新しく作成していますが、それも含めてListに追加するので一時的に連打した場合でもその後は再利用可能なオブジェクトのキャパが増えるのでよっぽど連打しない限りはInstantiateを避けて再利用するようになります。

このようにユーザー側の入力にあわせてキャッシュするオブジェクトの数が変わるのが、動的なキャッシングです。

4. 結論

このようにキャッシュして再利用することを意識することで、無駄な処理を防ぐことができます。今回は非アクティブなら再利用というやり方になっていますが、内部にbool型で再利用可能かどうかのフラグをもたせてそれで管理するのでも良いでしょう。 Instantiateを乱発すると処理も重くなってしまいますので、再利用できそうな処理であればキャッシュして使っていくやり方をぜひ試してみましょう!

ゲーム画面クリックと、画面上のボタンクリックをきっちり分けて処理をする方法

お疲れ様です。ユウです。今日は自分が詰まった部分について書いていきたいと思います。とはいえ、いろんなサイトからの受け売りなので備考録代わりです。

画面クリックを取得したいけど、uGUIにも反応しちゃう悔しい!

よくあるやり方でGetMouseButtonDown(0)とかで画面がクリックされたら処理を行うっていうのをやるかと思うのですが、これは画面のどこをクリックしても反応して便利なんですが、たとえばUIのボタンなども反応してしまいます。

f:id:yuu9048:20190327003118g:plain

ちょっと分かりづらいですが、画面をクリックするとき、ボタンをクリックするときで文字が変わっています。

しかし、ボタンを押すためにクリックしたときに画面をクリックしたときの処理が実行されてしまっています。 gifでいう3回目の文字の変更部分です。つまり間に違う処理が入ってきてしまっているのでこれはよろしくありません。この例だと文字が上書きするのでダメージは少ないですが、ゲームとかを作っていると処理が全然違う内容が予期せぬタイミングで走ったりとバグの温床になりかねません!

ちなみにgifで使ってるのソースコードはこちら。ボタンを押すとOnClickButtonが呼ばれています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0)) {
            GameObject.Find("ThisIsText").GetComponent<Text>().text = "Screen Pushed";
        }

    }

    public void OnClickButton () {
        GameObject.Find("ThisIsText").GetComponent<Text>().text = "Button Pushed";
    }
}

Rayさんで解決

いろいろやり方はあるかと思いますが、今回はRayを発射して判定していきたいと思います。クリックしたところにオブジェクトがあるかどうかを判断して画面かどうかをチェックしてみます。

まずは、画面上に配置するものでクリック判定を取りたいものにコライダーを貼ります。

f:id:yuu9048:20190327004400p:plain

そのままだと当たり判定に引っかかる可能性もなきにしもあらずということでTriggerにしておきます。

f:id:yuu9048:20190327004538p:plain

そしてさきほどのソースコードを書き換えます。Rayを発射してそこにオブジェクトがあるかの判定を取りましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0)) {

            // Rayを発射!
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit2D hit2d = Physics2D.Raycast((Vector2)ray.origin, (Vector2)ray.direction);

            // Rayで何もヒットしなかったら画面クリックと考える
            if (!hit2d) {
                GameObject.Find("ThisIsText").GetComponent<Text>().text = "Screen Pushed";
            }

        }

    }

    public void OnClickButton () {
        GameObject.Find("ThisIsText").GetComponent<Text>().text = "Button Pushed";
    }
}

これでクリックした位置にRayを飛ばしてそこにコライダーの当たり判定があるかをチェックしてくれます。今回の場合、そこにボタンがあったら処理をしてほしくないのでクリックした位置に何もなかったら画面クリックとして考えることにしました。 hit2dにRayを飛ばした結果が入っているのでそれをif文にかいてあげてチェックしていきます。

f:id:yuu9048:20190327005125g:plain

gifだと分かりづらいですが、ボタンを押下中でも画面をクリックした判定にはなっておらず、きちんと画面とボタンのクリック処理がわけて実現することができていますね。

このやり方を使えば、たとえば画面をクリックしたらキャラクターがジャンプするゲームでも、画面上部にUIとしてボタンを配置しておきボタンがクリックされたときはキャラクターをジャンプさせない。といった処理を行うことができます。

でもまだ詰めが甘い

さて、これにて解決かと思いましたがまだまだ荒い仕様です。このままだとコライダーの貼ってあるオブジェクトが多数登場してきたときに判定が取れなくなる危険があります。 画面タップでキャラクターがジャンプするはずなのに、ステージやコインなどをタップしたらRayが反応して処理が進まない。みたいなことですね。これはいけません。

いろいろ回避方法はあるかと思いますが、今回のやり方でUIに限る場合は、UI関連のゲームオブジェクトにUI用のタグをつけて管理する。というアプローチが良いかも知れません。

f:id:yuu9048:20190327010722p:plain

UIとして扱いたいオブジェクトにはtagとして「UIObject」を指定してあげます。タグがない場合はAdd Tagで追加してあげましょう。

ここで設定したタグがついているObjectは無視するようにソースコードを少し変更してみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0)) {
            // Rayを発射!
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit2D hit2d = Physics2D.Raycast((Vector2)ray.origin, (Vector2)ray.direction);

             // Objectが無い or あったとしてもUIオブジェクトではなければ画面クリックとして認識
            if (!hit2d || hit2d.transform.gameObject.tag != "UIObject") {
                GameObject.Find("ThisIsText").GetComponent<Text>().text = "Screen Pushed";
            }
        }
    }

    public void OnClickButton () {
        GameObject.Find("ThisIsText").GetComponent<Text>().text = "Button Pushed";
    }
}

さぁ、実行してみましょう! わかりやすいようにタグをつけていないボタンも右に追加してみました。

f:id:yuu9048:20190327011534g:plain

押す順番を間違えて分かりづらくなってますが、ちゃんと画面クリック、タグつきボタンクリック、タグなしボタンクリックで処理が分かれています!

何もない場所 or 「UIObject」タグのついていないオブジェクトをクリックすると画面クリックと認識します。

「UIObject」タグのついているオブジェクトに関しては画面クリックとは認識されていません。

ちょっとまどろっこしいやり方ではありますが、こんなやり方でも画面クリックとボタンクリックをきっちり処理をわけて実現することができます。素敵なUIを作ってゲームをより便利なものにしましょう!

参考サイト tech.pjin.jp