teriyaki note

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

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

< 前回 | 一覧 | 次回 >

前回はアイテムの組み合わせ機能の基礎を作った。

今回はそれを拡張して、詳細ビューで画像を切り替えたり、特定の場所をクリックすることでイベントを起こしたりという機能を実装してみたいと思う。

例えば、鍵を使用するときは鍵穴の部分をクリックしないとちゃんと箱が開かない。というような感じ。

成果物

左右を押すことでアイテムの角度を切り替えられる
f:id:teriyaki398:20181018131728g:plain:w500

正しい場所をクリックしないと反応しない
f:id:teriyaki398:20181018131750g:plain:w500

使用したアセット

今回もスクリプトと画像ファイルなどはGitHubで公開している。

github.com

前準備

ゲーム画面の味付け

前回みたいにデフォルトの skybox だと味気ないので、適当にそれっぽいオブジェクトをアセットストアからダウンロードして配置する。

山小屋の中にモダンなテーブルという全然それっぽくない感じになってしまったが、ただのデモなので気にしない。

f:id:teriyaki398:20181018133336p:plain:w550

画像の準備

今回のデモでは

  • 宝箱と鍵を入手
  • 鍵を使って宝箱を開ける
  • 宝箱を開けるとクロワッサンが入手できる

という流れを作ってみたい。

このとき、宝箱をいろんな角度から見られるようにしたいので、角度の分だけ画像を用意する。

アイテム名 アイテムリスト用アイコン
(256x256)
選択されたときのアイコン
(256x256)
詳細画像
(1400x900)
Key ItemListIcon/Key.png ItemListIcon/SelectedKey.png ItemDetail/Key.png
Croissant ItemListIcon/Croissant.png ItemListIcon/SelectedCroissant.png ItemDetail/Croissant.png
TreasureBox ItemListIcon/TreasureBox.png ItemListIcon/SelectedTreasureBox.png ItemDetail/TreasureBox.png
ItemDetail/TreasureBox01.png
ItemDetail/TreasureBox02.png
ItemDetail/TreasureBox03.png
OpenedTreasureBox ItemListIcon/OpenedTreasureBox.png ItemListIcon/SelectedOpenedTreasureBox.png ItemDetail/OpenedTreasureBox.png

f:id:teriyaki398:20181018134957p:plain:w400

f:id:teriyaki398:20181018135005p:plain:w400

スクリプト

クラス 役割
TestManager ゲーム画面全体の管理 (前回と同じ)
ItemListController アイテムリストの管理 (前回と同じ)
ItemDetailController アイテム詳細画面の管理
Item アイテムに関するクラスのスーパークラス
TreasureBox 宝箱の操作
OpenedTreasureBox 開いた宝箱の操作

前回からクラスの名前を少しだけ変えている。

  • ItemList -> ItemListController
  • ItemDetail -> ItemDetailController

Item クラス

脱出ゲームでは数多くのアイテムが必要になると思われるため、全てのアイテムで共通で必要になるメソッドなどをいちいち記述するのは大変。

そのため、Itemクラスに全体で必要そうなメソッド等を記述し、個々のアイテムクラスはItemを継承することにする。

アイテムがクリックされた場所を取得するためには座標を用いれば良いのだが、単純に〇〇px ~ 〇〇px の範囲内がクリックされたら〇〇するみたいな実装にしてしまうと、画面のサイズが変わったときに対応できない。

そのため、今回は下の図のように詳細画面を縦と横にいくつかに分割し、どの場所がクリックされたかを比率で判断したい。

また、アイテムによって分割の細かさは変えられるのが望ましい。

f:id:teriyaki398:20181018144234p:plain:w500

Scrren.widthScreen.heightで画面全体のサイズが取得できるので、詳細画面はそのサイズから比率を考えることで割り出せる。(ゲーム画面は全体の0.875倍と書いてるが、横は0.875倍だが縦は等倍。)

f:id:teriyaki398:20181018143148p:plain:w500

具体的なコードは以下の通り。

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

public class Item{

    // ゲーム画面のサイズ
    public Vector2 gameViewSize = new Vector2(Screen.width * 0.875f, Screen.height);

    // 詳細ビュー画面のサイズ
    public Vector2 detailViewSize = new Vector2(Screen.width * 0.875f, Screen.height) * 0.8f;

    // 分割する数
    public int w_div = 1;
    public int h_div = 1;

    // Controller関連
    public ItemListController itemListController;
    public ItemDetailController itemDetailController;

    // 詳細ビューのオブジェクト
    public GameObject detailView;


    public Item(){
        // Controller のインスタンスを取得
        itemListController = ItemListController.getInstance();
        itemDetailController = ItemDetailController.getInstance();
    }


    // クリックしたグリッド座標を返す
    public List<int> getClickedPos(){
        // 詳細画面の左下の座標
        Vector2 detailViewBottomLeft = gameViewSize * 0.1f;
        // 分割した1区画のサイズ
        Vector2 blockSize = new Vector2(detailViewSize[0] / w_div, detailViewSize[1] / h_div);
        // クリックした場所
        Vector2 clickedPos = new Vector2(Input.mousePosition[0], Input.mousePosition[1]);

        clickedPos = clickedPos - detailViewBottomLeft;
        
        return new List<int>() {(int)(clickedPos[0]/blockSize[0]), (int)(clickedPos[1]/blockSize[1])};
    }
    
}

一つずつ読み取っていこう。

メンバ変数

// ゲーム画面のサイズ
public Vector2 gameViewSize = new Vector2(Screen.width * 0.875f, Screen.height);

// 詳細ビュー画面のサイズ
public Vector2 detailViewSize = new Vector2(Screen.width * 0.875f, Screen.height) * 0.8f;

// 分割する数
public int w_div = 1;
public int h_div = 1;

// Controller関連
public ItemListController itemListController;
public ItemDetailController itemDetailController;

// 詳細ビューのオブジェクト
public GameObject detailView;

メンバ変数など。

ゲーム画面や詳細ビューのサイズはスクリーンのサイズとの比で計算できる。

コンストラク

public Item(){
    // Controller のインスタンスを取得
    itemListController = ItemListController.getInstance();
    itemDetailController = ItemDetailController.getInstance();
}

アイテムリストとアイテム詳細ビューを操作するためのインスタンスを取得しておく。

getClickedPos メソッド

// クリックしたグリッド座標を返す
public List<int> getClickedPos(){
    // 詳細画面の左下の座標
    Vector2 detailViewBottomLeft = gameViewSize * 0.1f;
    // 分割した1区画のサイズ
    Vector2 blockSize = new Vector2(detailViewSize[0] / w_div, detailViewSize[1] / h_div);
    // クリックした場所
    Vector2 clickedPos = new Vector2(Input.mousePosition[0], Input.mousePosition[1]);

    clickedPos = clickedPos - detailViewBottomLeft;
    
    return new List<int>() {(int)(clickedPos[0]/blockSize[0]), (int)(clickedPos[1]/blockSize[1])};
}

このメソッドはクリックされた場所が分割された区画のどこかを返すメソッド。

詳細ビューはゲーム画面の0.8倍なので、左下はゲーム画面のサイズの0.1倍の場所にある。

その点さえ注意すれば、あとは1区画あたりのサイズから、区画の座標が計算できる。

Tips

Unityのゲーム画面は左下が 座標(0,0) となっていることに注意!

TreasureBox クラス

操作を記述したいアイテムのクラスを考えていく。

先ほど作成したItem クラスを継承させる。

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

public class TreasureBox : Item{

    // 4方向からみた画像ファイル名
    public List<string> imageNameList = new List<string>() {"TreasureBox", "TreasureBox01", "TreasureBox02", "TreasureBox03"};

    public TreasureBox(){
        // 3x3 に画面を分割
        this.h_div = 3;
        this.w_div = 3;
    }

    public void main(){
        
        // クリックした場所を取得
        List<int> clickedPos = getClickedPos();

        // 現在表示している画像と、そのindexを取得
        string viewImageName = GameObject.Find("ItemDetailView").GetComponent<Image>().sprite.name;
        int imageId = imageNameList.IndexOf(viewImageName);

        string newImageName = "";
        switch(clickedPos[0]){

            // 画面左側がクリックされた
            case 0:
                if(imageId == 0){
                    newImageName = imageNameList[imageNameList.Count - 1];
                } else {
                    newImageName = imageNameList[imageId - 1];
                }
                GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/" + newImageName, typeof(Sprite)) as Sprite;
                break;

            // 画面中央がクリックされた
            case 1:

                if(clickedPos[1] == 1 && imageId == 0 && itemListController.getSelectedItemName() == "Key"){
                    GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/" + "OpenedTreasureBox", typeof(Sprite)) as Sprite;

                    itemListController.changeItem("TreasureBox", "OpenedTreasureBox");
                    itemListController.removeSelectedItem();

                    itemDetailController.currentViewItemName = "OpenedTreasureBox";
                }
                break;

            // 画面右側がクリックされた
            case 2:
                if(imageId == imageNameList.Count-1){
                    newImageName = imageNameList[0];
                } else {
                    newImageName = imageNameList[imageId + 1];
                }
                GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/" + newImageName, typeof(Sprite)) as Sprite;
                break;

        }
    }
}

メンバ変数

// 4方向からみた画像ファイル名
public List<string> imageNameList = new List<string>() {"TreasureBox", "TreasureBox01", "TreasureBox02", "TreasureBox03"};

90度ずつ回転させた画像のファイル名をリストに持たせておく。

コンストラク

public TreasureBox(){
    // 3x3 に画面を分割
    this.h_div = 3;
    this.w_div = 3;
}

宝箱は詳細画面を3x3 に分割する程度で大丈夫そうなので、そのように設定。

main メソッド

public void main(){
    
    // クリックした場所を取得
    List<int> clickedPos = getClickedPos();

    // 現在表示している画像と、そのindexを取得
    string viewImageName = GameObject.Find("ItemDetailView").GetComponent<Image>().sprite.name;
    int imageId = imageNameList.IndexOf(viewImageName);

    string newImageName = "";
    switch(clickedPos[0]){

        // 画面左側がクリックされた
        case 0:
            if(imageId == 0){
                newImageName = imageNameList[imageNameList.Count - 1];
            } else {
                newImageName = imageNameList[imageId - 1];
            }
            GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/" + newImageName, typeof(Sprite)) as Sprite;
            break;

        // 画面中央がクリックされた
        case 1:

            if(clickedPos[1] == 1 && imageId == 0 && itemListController.getSelectedItemName() == "Key"){
                GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/" + "OpenedTreasureBox", typeof(Sprite)) as Sprite;

                itemListController.changeItem("TreasureBox", "OpenedTreasureBox");
                itemListController.removeSelectedItem();

                itemDetailController.currentViewItemName = "OpenedTreasureBox";
            }
            break;

        // 画面右側がクリックされた
        case 2:
            if(imageId == imageNameList.Count-1){
                newImageName = imageNameList[0];
            } else {
                newImageName = imageNameList[imageId + 1];
            }
            GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/" + newImageName, typeof(Sprite)) as Sprite;
            break;

    }
}
  • 先ほどのgetClickedPos メソッドで区画座標を取得
  • 現在表示している画像と、そのid (リストの何番目か)を取得
  • クリックされた座標に応じて分岐
    • 左側 -> 左に90度回転させた画像に切り替え
    • 中央 -> 正面の画像 かつ 鍵が選択されていたら鍵を使って宝箱を開ける
    • 右側 -> 右に90度回転させた画像に切り替え

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

OpenedTreasureBox クラス

これは開けられた宝箱に関するクラスだが、こちらは回転させたりしないのでシンプル

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

public class OpenedTreasureBox : Item{

    public OpenedTreasureBox(){
        // 3x3 に画面を分割
        this.h_div = 3;
        this.w_div = 3;
    }

    public void main(){
        
        // クリックした場所を取得
        List<int> clickedPos = getClickedPos();

        // 画面の中央がクリックされていたら
        if(clickedPos[0] == 1 && clickedPos[1] == 1){
            
            GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/Croissant", typeof(Sprite)) as Sprite;
            itemListController.changeItem("OpenedTreasureBox", "Croissant");
            itemDetailController.currentViewItemName = "Croissant";
             
        }
    }
}

コンストラク

public OpenedTreasureBox(){
    // 3x3 に画面を分割
    this.h_div = 3;
    this.w_div = 3;
}

こちらも3x3 分割にする

main メソッド

public void main(){
    
    // クリックした場所を取得
    List<int> clickedPos = getClickedPos();

    // 画面の中央がクリックされていたら
    if(clickedPos[0] == 1 && clickedPos[1] == 1){
        
        GameObject.Find("ItemDetailView").GetComponent<Image>().sprite = Resources.Load(itemDetailController.itemDetailPath + "/Croissant", typeof(Sprite)) as Sprite;
        itemListController.changeItem("OpenedTreasureBox", "Croissant");
        itemDetailController.currentViewItemName = "Croissant";
            
    }
}

今度は画像の切り替えがないので、中央がクリックされていたらクロワッサンを取得するだけ。

ItemDetailController クラス

前回のものとほぼ同じで、今回追加したアイテムに対応させているだけ。

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

public class ItemDetailController{

    // Singleton
    private static ItemDetailController itemDetailController = new ItemDetailController();

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

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

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

    // Controller関連
    public ItemListController itemListController;

    // ここに利用するアイテムのスクリプトを追加する
    public TreasureBox treasureBox;
    public OpenedTreasureBox openedTreasureBox;

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

        itemListController = ItemListController.getInstance();
    }

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

    public static ItemDetailController getInstance(){
        return itemDetailController;
    }

    public void main(GameObject selectedGameObject){
        
        switch(selectedGameObject.name){
            // 詳細表示ボタンが押されたとき
            case "DetailButton":
                // 選択状態にあるアイテムがあるならそのアイテムの詳細を表示する
                if(itemListController.selectedItemId != -1){
                    itemDetailCanvas.SetActive(true);
                    currentViewItemName = itemListController.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 "TreasureBox":
                        treasureBox.main();
                        break;

                    case "OpenedTreasureBox":
                        openedTreasureBox.main();
                        break;

                }
                break;
        }

    }
    
}

具体的には

  • TreasureBoxOpenedTreasureBoxを利用するためにインスタンスを生成
  • TreasureBoxの詳細画面がクリックされたらTreasureBox.main()を実行
  • OpenedTreasureBoxの詳細画面がクリックされたらOpenedTreasureBox.main()を実行

これで無事に鍵付きの宝箱からクロワッサンが取得できたら成功。

Tips

区画分けした後の座標については、Unityのゲーム画面と同様に左下が(0,0)となるようにしてあるが、その辺りは自分の感覚に合ったものに変えるのが良さそう。

また、本質とは関係なかったので記述しなかったのだが、詳細画面を開くためにショートカットキーを用意した。

TestManager.cs の Updateメソッドを前回から次のように変更した。

// Update is called once per frame
void Update () {
    if(Input.GetMouseButtonUp(0)){

        selectedGameObject = eventSystem.currentSelectedGameObject;

        if(selectedGameObject == null){
            searchRoom();
        } else {
            switch(selectedGameObject.tag){
                case "ItemListButton":
                    itemListController.click(selectedGameObject);
                    break;
                
                case "ItemDetail":
                    itemDetailController.main(selectedGameObject);
                    break;
            }
        }
        
    }

    // dキーで詳細画面へのショートカット
    if(Input.GetKey(KeyCode.D)){
        selectedGameObject = GameObject.Find("DetailButton");
        itemDetailController.main(selectedGameObject);
    }
}

いちいちボタンを押さずに済むので、作業が楽になった。