teriyaki note

好きなものはラーメンと将棋

Unityで脱出ゲームを作る part.2 / アイテムを組み合わせる編

< 前回 | 一覧 | 次回 >

脱出ゲームには特定のアイテムを組みわせて別のアイテムへと変化させたり、道具を使ってアイテムを分解し、新しいアイテムを入手することがある。

今回はそのような「アイテムを組み合わせる機能」を作ってみたいと思う。

前回のアイテムリストを元に作成していく。

成果物

f:id:teriyaki398:20181011161641g:plain

  • アイテムの詳細を表示
  • 選択している特定のアイテムと組み合わせられる

という機能を実装できた。

今回の内容もGitHubに公開しているが、一部アセットは再配布に該当する恐れがあるので、使用した画像とスクリプトだけ公開している。

github.com

前準備

詳細ボタン

前回のアイテムリストでは全てのボタンをアイテムを表示するボタンとして利用していたが、その中の一つをアイテムの詳細を表示するボタンとして利用したいと思う。

f:id:teriyaki398:20181011181148p:plain:w550

  • 名前をDetailButtonに変更した
  • ItemDetailというTagを作成し割り当てた
  • 画像をそれっぽいものに変更した

アイコンの素材は こちら からお借りした。

Tips

この時、ボタンの連番を崩さないようにする必要がある 例えば Button (7)DetailButton に変更した場合、7番のボタンが無くなってしまう。 そのため、一番後ろのボタンを持ってきて名前を変更するのが良い

描画するキャンバス

次にアイテムの詳細画像を表示する場所を用意する。

f:id:teriyaki398:20181011182213p:plain:w550

(下のPrefabは気にしないで)

作成したものは以下の通り(カッコ内はタグ名とオブジェクトの種類)

  • MainCanvas (Tag : Untagged, Canvas)
    • ItemDetailCanvas (Tag : ItemDetail, Empty Object)
      • CloseButton (Tag : ItemDetail, Button)
      • ItemDetailView (Tag : ItemDetail, Button)

とりあえずゲーム画面でUIを表示するためのMainCanvasを作成した。

ただアイテムの詳細UIは頻繁に ON/OFF するので子オブジェクトとしてまとめてある。

クリックした時に取得するのが簡単なのでButtonを使っているのだが、本当はImageなどを使うべきなのだろうか。というか、Buttonはクリックできる画像という認識なのだが、Imageの方が優れているポイントなどもあるのだろうか。その辺りはよく分かっていない。

Tips

キャンバスのCanvas ScalerコンポーネントUI Scale ModeScale With Screen Sizeに変更しておくと、画面サイズが変わった時にUIのサイズがおかしくならないので設定しておこう。

f:id:teriyaki398:20181011182923p:plain:w350

ゲーム画面はこんな感じ。

f:id:teriyaki398:20181011184831p:plain

青い部分がItemDetailViewで、赤い部分がCloseButtonとなっている。

つまり、青い部分にアイテムの詳細画像が表示され、赤い部分をクリックすると詳細ビューを解除できるような仕組みを作りたい。

このままでは見栄えが悪いので色を変更して調整しておく。ボタンはデフォルトで押す時に若干色が濃くなるようになっているので、その辺りも調整する。

Tips

Unity画面で編集しているときに真っ白な画像が画面を覆っていると、非常に開発しづらい。そういう時は透明な画像を貼り付けておくと楽になる。

組み合わせるオブジェクトの用意

今回は、Unity Asset Store で無料の Low Poly Survival Kitのオブジェクトを使ってみる。

assetstore.unity.com

非常に軽量なのですぐにダウンロードできた。

インポートしたのち、TorchMatch boxPrefabをシーンに適当に配置する。

f:id:teriyaki398:20181011201426p:plain:w550

  • Match boxにはRigidbodyが付与されていたので外した
  • Layerを用意した10 : Clickableに変更

燃える松明

デモでは、TorchMatch boxを用いて燃やす。ということがやりたい。

別のシーンを作成し、TorchオブジェクトとParticle Systemを活用して、以下のような燃えてる感じのシーンを作成した。

f:id:teriyaki398:20181011202450g:plain

Tips

Particle System の活用方法については、以下のサイトが詳しい。

その1 Unityのパーティクル「Shuriken」

画像の用意

それぞれのアイテムについて、アイテムリストに表示する用の画像と、詳細画像を用意する。

名前の付け方は前回と同じにしてある。

また、画像の撮影にはUnity Recorderを用いてちまちま撮影した。正直この工程が一番大変なので効率の良いやり方があったら教えて欲しい。

assetstore.unity.com

アイテム名 アイテムリスト用の画像
(256x256)
アイテムリスト選択時
(256x256)
詳細画像
(1400x900)
Torch ItemListIcon/Torch.png ItemListIcon/SelectedTorch.png ItemDetail/Torch.jpg
MatchBox ItemListIcon/MatchBox.png ItemListIcon/SelectedMatchBox.png ItemDetail/MatchBox.jpg
BurnedTorch ItemListIcon/BurnedTorch.png ItemListIcon/SelectedBurnedTorch.png ItemDetail/BurnedTorch.jpg

f:id:teriyaki398:20181011204413p:plain:w500

f:id:teriyaki398:20181011204430p:plain:w500

スクリプト

作成したソースコードは以下の4つ。

クラス 役割
TestManager ゲーム画面全体の監視
ItemList アイテムリストの管理
ItemDetail アイテム詳細画面の管理
Torch Torchアイテムに関する操作

ItemListクラスは前回のものにいくつか機能を追加しているので、1つずつみていこう。

ItemListクラス

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

public class ItemList{

    // Singleton
    private static ItemList itemList = new ItemList();

    // アイテムボタンオブジェクトのリスト
    public List<GameObject> btnList = new List<GameObject>();

    // 選択状態にあるアイテムの番号を保存する
    public int selectedItemId = -1;
    
    // 選択されている状態を示すキーワード
    public string selectedSymbol = "Selected";

    // 割り当て無し状態の画像名
    public string emptySymbol = "no_image";

    // アイテムリストの画像が置いてあるResources直下のディレクトリ
    public string itemListPath = "ItemListIcon/";


    private ItemList(){
        // 存在するボタンをリストに入れる
        int i = 0;
        while(GameObject.Find("Button (" + i.ToString() + ")") != null){
            btnList.Add(GameObject.Find("Button (" + i.ToString() + ")"));
            i++;
        }
    }

    public static ItemList getInstance(){
        return itemList;
    }


    // アイテムをリストに追加する
    public void add(string item_name){

        for(int i=0; i<btnList.Count; i++){
            string im_name = btnList[i].GetComponent<Image>().sprite.name;
            if(im_name == emptySymbol){
                btnList[i].GetComponent<Image>().sprite = Resources.Load(itemListPath + item_name, typeof(Sprite)) as Sprite;
                break;
            }
        }

    }

    // リスト内で選択されているアイテムを削除する
    public void removeSelectedItem(){
        
        if(selectedItemId == -1){  // 何も選択されていないなら何もしない
            return;

        } else if (selectedItemId == btnList.Count - 1){   // 一番最後のボタンなら最後だけ変える
            btnList[selectedItemId].GetComponent<Image>().sprite = Resources.Load(itemListPath + emptySymbol, typeof(Sprite)) as Sprite;
            selectedItemId = -1;

        } else {    // 途中だけ取りのぞく時
            // num番目のボタンから順番に、次のボタンの画像を割り当てる
            for(int i=selectedItemId; i<btnList.Count - 1; i++){
                btnList[i].GetComponent<Image>().sprite = btnList[i+1].GetComponent<Image>().sprite;
                selectedItemId = -1;

            }
        }
        
    }

    // アイテムリストを選択したときの処理
    public void click(GameObject btnObject){
        
        string im_name = btnObject.GetComponent<Image>().sprite.name;
        // 選択されたボタンの番号を取得
        int id = int.Parse(btnObject.name.Substring("Button (".Length, btnObject.name.Length - "Button ()".Length));

        // 既に選択状態なら選択状態を解除する
        if(id == selectedItemId){

            im_name = im_name.Substring(selectedSymbol.Length);
            btnObject.GetComponent<Image>().sprite = Resources.Load(itemListPath + im_name, typeof(Sprite)) as Sprite;
            selectedItemId = -1;

        } else if(im_name != emptySymbol){   // 何かのアイテムが割り当てられていたら

            // 他に選択状態のアイテムがあるなら非選択状態に変更
            if(selectedItemId != -1){
                string temp = btnList[selectedItemId].GetComponent<Image>().sprite.name.Substring(selectedSymbol.Length);
                btnList[selectedItemId].GetComponent<Image>().sprite = Resources.Load(itemListPath + temp, typeof(Sprite)) as Sprite;
                selectedItemId = -1;
            }

            // 選択状態にする
            btnObject.GetComponent<Image>().sprite = Resources.Load(itemListPath + selectedSymbol + im_name ,typeof(Sprite)) as Sprite;
            selectedItemId = id;
        }

    }

    // 選択されているアイテムの名前を返す
    public string getSelectedItemName(){

        if(selectedItemId != -1){
            string n_name = btnList[selectedItemId].GetComponent<Image>().sprite.name;
            n_name = n_name.Substring(selectedSymbol.Length);
            return n_name;
        }
        
        return emptySymbol;
    }


    // リスト内のアイテム currentItem を newItem に変更する
    public void changeItem(string currentItemName, string newItemName){
        for(int i=0; i<btnList.Count; i++){
            if(btnList[i].GetComponent<Image>().sprite.name == currentItemName){
                btnList[i].GetComponent<Image>().sprite = Resources.Load(itemListPath + newItemName, typeof(Sprite)) as Sprite;
                return;
            }
        }

        // return されなかった場合
        Debug.Log("指定されたアイテムが存在していない");
    }

}

追加したのは以下の2つのメソッド。

あと、アイテムリストに描画するアイコン画像までのパスをメンバ変数で持たせた。

getSelectedItemNameメソッド
// 選択されているアイテムの名前を返す
public string getSelectedItemName(){

    if(selectedItemId != -1){
        string n_name = btnList[selectedItemId].GetComponent<Image>().sprite.name;
        n_name = n_name.Substring(selectedSymbol.Length);
        return n_name;
    }
    
    return emptySymbol;
}

詳細ボタンが押されたときに選択状態のアイテムを詳細表示する。

というように実装したいので、今どのアイテムが選択状態かを他のクラスから知られるようにしたい。

このクラスは選択されているアイテムがあるならその名前を返し、選択されていなかったら何もないことを返している。

changeItemメソッド
// リスト内のアイテム currentItem を newItem に変更する
public void changeItem(string currentItemName, string newItemName){
    for(int i=0; i<btnList.Count; i++){
        if(btnList[i].GetComponent<Image>().sprite.name == currentItemName){
            btnList[i].GetComponent<Image>().sprite = Resources.Load(itemListPath + newItemName, typeof(Sprite)) as Sprite;
            return;
        }
    }

    // return されなかった場合
    Debug.Log("指定されたアイテムが存在していない");
}

ゲーム内でアイテムを組み合わせた場合、現在のアイテムを新しいアイテムに変更する必要がある。

このメソッドは今リストに存在しているアイテム名新しいアイテム名を引数として受け取り、リストを更新している。

ItemDetailクラス

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

public class ItemDetail{

    // Singleton
    private static ItemDetail itemDetail = new ItemDetail();

    // 詳細画像を表示する場所
    public GameObject itemDetailCanvas;

    // 現在表示しているアイテムの名前
    public string currentViewItemName;

    // アイテムの詳細画像が置いてあるResources直下のディレクトリ
    public string itemDetailPath = "ItemDetail";

    // ゲームスクリプト
    public ItemList itemList;

    // ゲームアイテムスクリプト
    public Torch torch;

    private ItemDetail(){
        itemDetailCanvas = GameObject.Find("ItemDetailCanvas");
        itemDetailCanvas.SetActive(false);

        itemList = ItemList.getInstance();
    }

    // アイテム関連のインスタンス生成は明示的に最後に行いたい
    public void createItemInstance(){
        torch = new Torch();
    }

    public static ItemDetail getInstance(){
        return itemDetail;
    }

    public void main(GameObject selectedGameObject){
        
        switch(selectedGameObject.name){
            // 詳細表示ボタンが押されたとき
            case "DetailButton":
                // 選択状態にあるアイテムがあるならそのアイテムの詳細を表示する
                if(itemList.selectedItemId != -1){
                    itemDetailCanvas.SetActive(true);
                    currentViewItemName = itemList.getSelectedItemName();
                    GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load("ItemDetail/" + currentViewItemName, typeof(Sprite)) as Sprite;
                }
                break;
            
            // 閉じるボタンが押されたなら詳細画面を閉じる
            case "CloseButton":
                itemDetailCanvas.SetActive(false);
                break;

            // 詳細画面のどこかがクリックされたとき
            case "ItemDetailView":
                
                switch(currentViewItemName){
                    case "Torch":
                        torch.main();
                        break;
                }
                break;
        }

    }
    
}
コンストラク
private ItemDetail(){
    itemDetailCanvas = GameObject.Find("ItemDetailCanvas");
    itemDetailCanvas.SetActive(false);

    itemList = ItemList.getInstance();
}
  • 詳細画像を描画するための場所を取得し、非表示にしている。
  • アイテムリストのインスタンスを取得する。
createItemInstance メソッド
public void createItemInstance(){
    torch = new Torch();
}

アイテムを操作するクラスのインスタンスを生成している。

ここでいうTorchクラスの中でもItemListItemDetailインスタンスを取得しており、クラスが呼び出される順番が複雑なので明示的に後からまとめてインスタンスを生成している。

正直ItemDetailItemListは静的クラスにしてしまった方が都合がいいのかもしれない。

mainメソッド
public void main(GameObject selectedGameObject){
    
    switch(selectedGameObject.name){
        // 詳細表示ボタンが押されたとき
        case "DetailButton":
            // 選択状態にあるアイテムがあるならそのアイテムの詳細を表示する
            if(itemList.selectedItemId != -1){
                itemDetailCanvas.SetActive(true);
                currentViewItemName = itemList.getSelectedItemName();
                GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load("ItemDetail/" + currentViewItemName, typeof(Sprite)) as Sprite;
            }
            break;
        
        // 閉じるボタンが押されたなら詳細画面を閉じる
        case "CloseButton":
            itemDetailCanvas.SetActive(false);
            break;

        // 詳細画面のどこかがクリックされたとき
        case "ItemDetailView":
            
            switch(currentViewItemName){
                case "Torch":
                    torch.main();
                    break;
            }
            break;
    }

}

メインの処理は全てこの部分に書いてある。

本当はもっと細かくメソッドを分けた方が良いのかもしれないが、それは必要になったらやろう...

このメソッドは簡単に言えば現在押されているボタンを引数として受け取っている。

その後、ボタンの名前で分岐し、

  • アイテムが選択されている状態で詳細ボタンが押されたなら詳細画像を表示
  • 閉じるボタンが押されたなら詳細画面を閉じる
  • 詳細画面が押されたなら、今表示しているアイテム名でさらに分岐
  • Torchアイテムなら専用のメソッドを呼び出す。

という風にしている。

実際のゲームではアイテム毎にやらせたい処理が異なるはずなので、それぞれスクリプトを用意する必要があるだろう。

Torchクラス

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

public class Torch{

    public ItemList itemList; 
    public ItemDetail itemDetail;

    public Torch(){
        itemList = ItemList.getInstance();
        itemDetail = ItemDetail.getInstance();
    }

    public void main(){
        
        // MatchBoxが選択されていたら松明を燃やす
        if(itemList.getSelectedItemName() == "MatchBox"){

            Debug.Log(itemDetail);
            // 詳細画像を燃えた松明に変更
            GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetail.itemDetailPath + "/BurnedTorch", typeof (Sprite)) as Sprite;

            // アイテムリストの画像を変更
            itemList.changeItem("Torch", "BurnedTorch");

            // マッチを消費するのでリストから削除
            itemList.removeSelectedItem();
        }
        
    }

}

このクラスはTorchアイテムに関する操作を記述したクラス。

と言っても今回のTorchの挙動はシンプルで、MatchBoxが選択されている状態でクリックされたらBurnedTorchへと進化するというだけ。

  • 詳細画像を新しい画像に変更する
  • アイテムリストのTorchBurnedTorchへと変更する
  • MatchBoxは消費されるアイテムなので、アイテムリストから削除する

という処理を行なっている。

これで簡単なアイテムの組み合わせ機能を実装することができた。

これを応用することでアイテムを分解して2つにするみたいな操作も簡単に実現できそうだ。

ただし、実際にはもっと複雑な操作が要求される場面がある。

例えば

  • アイテムをいろんな角度から見られるようにしたい
  • ある一部をクリックした時にだけ反応させたい

みたいな感じだ。

次回はその辺りを解決してみたいと思う。