nimanji’s blog

20代の最後は手のひらに楽しいゲームを産み落とす。それができるまでを記した日記。

親GameObjectから子要素を取得してforeachしたい

レイヤーを決めながら進めると、どうしても親子関係の設定方法によってはしっくり来るコードが書けないんですよね。
今回はタイトルの通り、親のGameObjectから子要素を取得し、子要素をGameObjectとしてforeachをぶん回す方法を書いていきます。

1. 子をDictionaryに突っ込む

今回行った対処としては、子要素はDictionaryで保持しておき、いつでも使える状態にします。
これは実際にコードを見てもらったほうが早いです。

public class ParentChild
{
    private GameObject parent;
    private Dictionary<string, GameObject> children = new Dictionary<string, GameObject>();

    public ParentChild()
    {
        this.parent = GameObject.Find("ParentObject");
        Transform children = this.parent.transform;
        foreach (Transform child in children) {
            this.children[child.name] = child.gameObject; // ※1
        }
    }
}

これで子要素を取得できます。
(※1)のところでkeyを子GameObjectの名前にしていますが、これは普段職場でPHPを使用しているため、私は名称のほうが管理しやすいためです。このkeyについては、各々で管理しやすいものでいいかと思います。

2. Dictionaryの中身をforeachで取り出す

今回ネックになったのはこの工程です。
が、今の時代は便利ですね。Google検索で「unity dict」まで入れると、検索候補に「unity dictionary foreach」とありました。皆さん考えることは同じなんですね。(==;

foreachとDictionaryを絡ませる場合、取得先の型にGameObjectやTransformを入れると、「Cannot convert type 'System.Collections.Generic.KeyValuePair'」というエラーが出ると思います。
ここに書いてあるまんまですが、Dictionaryをforeachで使う場合はKeyValuePair型でないといけません。そしてその中身として指定する引数も、取得元のDictionaryと合わせる必要があります。
これを前提としてコードを見ていきます。

public class ParentChild
{
    private GameObject parent;
    private Dictionary<string, GameObject> children = new Dictionary<string, GameObject>();

    public ParentChild()
    {
        this.parent = GameObject.Find("ParentObject");
        Transform tmp = this.parent.transform;
        foreach (Transform child in tmp) {
            this.children[child.name] = child.gameObject;
        }
        foreach (KeyValuePair<string, GameObject> dic in this.children) {
            Debug.Log(dic.Value.name);
        }
    }
}

これを実行すると、Console上にはParentObjectの下にあるGameObjectの名前がずらりと表示されたと思います。

最後に

この手法を取り入れていくと、ほとんど親子間のGameObject取得に困ることはないかと思います。
ただ懸念点としてあるのは、Dictionaryが果たしてどれくらい重いのか、GameObjectのデータをずっと保持しておくことでメモリをどれくらい食うのか、そこら辺がまだわかっていないです。
おそらくゲームがひととおり完成したとき、この周辺でメモリ問題が起きるかなーという不安はありますが、ひとまずはこれで作っていくことにします。

GameObjectの取得と管理は、この先も大きな課題のひとつとして出てきそうです。

ゲームを作りやすいようにHierarchyを整理する

ゲームを作ったことがある、もしくは作ろうとしたという経験がある人は、あることで一度は挫折しかけた、挫折してゲームを作らなくなってしまった、というのがあるのではないでしょうか。
私の場合はゲームを作らなくなってしまうまではいきませんが、プロジェクトを何度も破棄しては新しく作成を繰り返しています・・・。

その「あること」というのは、日をまたいでプロジェクトを開くと、どういう構造になっていたか忘れてしまったというものです。・・・私だけでしょうか?
今回はそうならないために、きちんと整理して、見ただけである程度は思い出せるようにするよう、私なりのHierarchy整理方法をまとめます。

設置するObject単位のCanvasの座標を整理する

Hierarchyではいろいろ整理するときに親子関係を使いますが、私はそこにZ座標での管理を取り入れています。いつも作っているのは2Dのゲームですが、それでもこのZ座標管理は重要です。
まずプロジェクトつくりたての状況のHierarchyをみてみます。今回はScene名をHierarchyArrangeとしています。

f:id:nimanji:20180412144256p:plain

はい、まっさらです。
次にここに、ゲームで使う要素を入れていきます。今回はこの3つです。

  • UI
  • キャラクター
  • 背景

この3つに対応するGameObjectを作成し、それぞれに以下の設定のCanvasを持たせます。

f:id:nimanji:20180412145101p:plain

注意する点としては、実際に表示するときの重なりを意識する必要があるため、Plane Distanceの値を各GameObjectごとに変更します。変更する数値はそれぞれこんな感じです。

  • UI ... 10
  • キャラクター ... 20
  • 背景 ... 30

これを設定したあと位置関係を視覚的に把握するため、Scene表示を2Dから3Dに変更するとこのように表示されます。
(今回は説明のため、UIを赤、キャラクターを緑、背景を青にしています)

f:id:nimanji:20180412145458p:plain

これで今後は、キャラクターを作るときにはこのGameObject、背景はこのGameObjectと見やすくなり、レイヤー関係も一目でわかるようになりました。

スクリプトからGameObjectを作るときの注意点

前回の記事では、Resources配下の画像を元にしてGameObjectを作成していましたが、この方法を用いる場合だと座標がおかしなことになってしまいます。
試しに以下のスクリプトで、キャラクターレイヤーにGameObjectを生成してみます。

public class TestScript : MonoBehaviour
{
    void Awake()
    {
        // 親となるGameObjectを取得
        GameObject parent_object = GameObject.Find("Character");
        // Resourcesディレクトリから画像を取得
        Object[] resources_images = Resources.LoadAll("", typeof(Sprite));
        // 取得した画像を1個ずつGameObjectとして生成する
        foreach (Sprite sprite in resources_images) {
            // GameObjectを読み込んだSprite名で生成
            GameObject instance_object = new GameObject(sprite.name);
            // GameObjectの親子関係、アンカー位置などを設定
            instance_object.transform.parent = parent_object.transform;
            instance_object.AddComponent<RectTransform>().anchoredPosition = new Vector2(0, 0);
            instance_object.GetComponent<RectTransform>().localScale = new Vector2(10, 10);
            // GameObjectSpriteを適用し、アスペクト比と画像サイズの設定を行う
            instance_object.AddComponent<Image>().sprite = sprite;
            instance_object.GetComponent<Image>().preserveAspect = true;
            instance_object.GetComponent<Image>().SetNativeSize();
        }
    }
}

作成したスクリプトを、スクリプト設置用に作成した空のGameObject(今回はTestScript)に入れて実行すると、このような結果になります。

f:id:nimanji:20180412151132p:plain

このように、Characterレイヤーに設定したはずなのに、画像はUI上に表示されてしまいました。
このとき、画像の座標は(0, 0, -1600)となっており、明らかにおかしいです。

原因は、スクリプト内で作成したGameObjectの親を指定しているこの部分です。

instance_object.transform.parent = parent_object.transform;

これの解決方法をいろいろ探していると、この記事を見つけました。

www.hildsoft.com

この記事を参考に、GameObjectの親の指定方法を以下に変更します。

instance_object.transform.SetParent(parent_object.transform, false);

これで実行すると、きちんとCharacterレイヤーに画像が表示されました。画像の座標(0, 0, 0)となっており、Characterレイヤーの中心点を基準として配置されています。

f:id:nimanji:20180412152057p:plain

さいごに

レイヤーごとに分けたり親子関係を築いたりすると多少スクリプトの修正も必要ですが、日をまたいで作業を再開したとしても、3D表示とHierarchyの構成がスッと頭に入ってきて思い出せるかと思います。
また、今回使用したプロジェクトデータはGitHubに公開していますので、実際にHierarchyがどう構成されているかを見たい場合は以下をCloneして展開してみてください。

github.com

重なったGameObjectからどのGameObjectがクリックされたかを判別する

普段はサーバエンジニアとして飯を食っていますが、家では「なんか面白いモノ作りたい」と思いつつ、仕事とは逆のクライアント側の開発を独学中にふと思ったこと。
「重なったGameObjectのうち、どのGameObjectがクリックしたか判別したい」
これをやってみました。

今回作るにあたって参考になったサイトさま:
Unityでスクリプトから画像を読み込んで表示する【uGUI】 | 神代のかなた・ラクガキ
naichilab.blogspot.jp

前提条件:

  • 使う画像はすべて同じサイズであること
  • 使う画像をすべて重ねたとき、ほかの画像と干渉していないこと

環境:

  • Unity version 2017.1.1f
  • UniRx

その1:GameObjectを取り込んだSpriteから作成する

はじめにSceneを作成します。今回はプレイ画面で使うため、PlaySceneとして作成しています。
Hierarchyには、右クリックして「UI>Text」を選択し、作成されたCanvas内のTextを削除したものを用意します。
f:id:nimanji:20180310191711p:plain
次に、Cavnasに設定するスクリプトを作成していきます。
今回はUniRxを使用するため、Canvasに設定するPlayScenePresenterにはAwakeStartを使い、AwakeでGameObjectの生成を記述し、Startにはクリックしたときの動作を記述します。

class PlayScenePresenter : MonoBehaviour
{
    private PlaySceneModel _model;

    void Awake ()
    {
        this._model = new PlaySceneModel();
    }

    void Start ()
    {
        // === ここにクリック時の処理とかを書いていく
    }
}

Awake内に生成用のスクリプトを記載する方法もありますが、それだとごちゃごちゃしてしまうので、別にPlaySceneModelを作成し、インスタンス生成時にGameObjectをHierarchyに登録するように作ります。

public class PlaySceneModel
{
    // 読み込んだSpriteを保存する
    private Object[] load_sprite_list;

    /// <summary>
    /// インスタンス
    /// </summary>
    public PlaySceneModel ()
    {
        // ResourcesディレクトリにあるSpriteをすべて取得
        this.load_sprite_list = Resources.LoadAll("", typeof(Sprite));

        // 取得したSprite1個ずつ読み込む
        foreach (Sprite sprite in this.load_sprite_list) {
            // GameObjectを読み込んだSprite名で生成
            GameObject instance_object = new GameObject(sprite.name);

            // GameObjectの親子関係、アンカー位置などを設定
            instance_object.transform.parent = GameObject.Find("Canvas").transform;
            instance_object.AddComponent<RectTransform>().anchoredPosition = new Vector2(0, 0);
            instance_object.GetComponent<RectTransform>().localScale = new Vector2(1, 1);

            // GameObjectSpriteを適用し、アスペクト比と画像サイズの設定を行う
            instance_object.AddComponent<Image>().sprite = sprite;
            instance_object.GetComponent<Image>().preserveAspect = true;
            instance_object.GetComponent<Image>().SetNativeSize();
        }
    }
}

これで、Resourcesディレクトリの中にpng形式の画像を入れて生成されたSpriteからGameObjectを画面中央に生成することができました。
foreach内でいろいろやっていますが、ここではコード内のコメントのみにしています。各行でどういうことをしているかについては、こちらの記事(Unityでスクリプトから画像を読み込んで表示する【uGUI】 | 神代のかなた・ラクガキ)に記載されていますので、気になるかたはこちらも読んでみることをおすすめします。

そして、作成したPlayScenePresenterをHierarchyのCanvasに設定して実行すると、このような表示になります。
f:id:nimanji:20180310194952p:plain
全体が白い画像ですが、今回は以下の画像を使っているため、すべて白く見えます。画像の名前が違いますが、取り込んだあとに変更しています。
f:id:nimanji:20180310195226p:plain

その2:クリックした位置にあるGameObjectの名前をログに出力する

クリックの処理自体はUniRxを使っているのでけっこう簡単にできる。PlaySenePresenterのStartに以下を追記します。

    void Start ()
    {
        var click_handle = Observable.EveryUpdate().Where(_ => Input.GetMouseDown(0));
        clock_handle.Subscribe(_ => this._model.getObjectNameForMousePosition(Input.mousePosition.x, Input.mousePosition.y));
    }

左クリックしたらPlaySceneModelのgetObjectNameForMousePositionを呼び出すという簡単なもの。getObjectNameForMousePositionの中身は以下のとおりです。

    public function getObjectNameForMousePosition (Vector2 position(
    {
        // 保存されているSpriteのテクスチャのうち、引数の座標のアルファ値が1(不透明)のものを探す
        Sprite target_sprite = null;
        foreach (Sprite sprite in this.load_sprite_list) {
            if (1.0f == sprite.texture.GetPixel((int)position.x, (int)position.y).a) {
                target_sprite = sprite;
                break;
            }
        }
        Debug.Log(target_sprite.name);
    }

これを実行してゲーム画面右上あたりをクリックすると、クリックされた箇所のGameObjectの名前が出力できました。
f:id:nimanji:20180311184130p:plain

最後に:これからの課題

今回はクリックで実装しましたが、やっぱりスマホで遊ぶゲームを作るとなるとタッチ動作の確認をしないといけないという課題が残りました。
調べてみると、どうもタッチ操作とGetPixelは相性が悪い(?)ようで、いろいろ苦戦するであろう記事が多かったので、それができたらまた記事にしようと思います。