top of page

3D脱出ゲーム編

Lesson2 アイテムを実装しよう

2-1

2-1 基底クラス

2-1 基底クラス

1

​基底クラスとは

 ここからはゲームの肝であるアイテムを作っていきます。どうしても少しややこしい部分になってしまいますが、頑張って作っていきましょう。

 このゲームには調べられるものがたくさんあります。例えば台や戸棚、出口のボタンなどです。これらはそれぞれ調べた時の処理は違いますが「カーソルが合った時の処理」は全て同じになります。例えばこのゲームでは本でも戸棚でもカーソルが合ったときは共通して「赤いアウトラインを表示する」「アイテム名をUIに表示する」といった処理が行われます。

 こういった共通の処理やパラメータがある時は基底クラスを使ってみましょう。

​ ItemObjectクラスにカーソルが合った時の処理を書き、ItemObjectクラスを継承した派生クラスに調べた時の固有の処理を書きます(戸棚が開く、ボタンを押すなど)ItemObjectクラスをいう基底クラスを継承することによって、カーソルが合った時の処理を何度も書く必要がなくなります。

2

​基底クラスを作成

 基底クラスの恩恵は現時点ではわかりにくいと思いますが、Lesson3の終盤あたりになってくるとわかりやすくなるはずです。難しい人はあまり気にしなくても構いません。

 

 それではまず基底クラスであるItemObjectクラスを作成しましょう。いつも通りItemObjectスクリプトを作成して、以下のように入力してください。

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

 

public class ItemObject : MonoBehaviour
{
    [SerializeField]
    string Name;        // カーソルを合わせた時に表示する名前
    [SerializeField, Multiline(3)]
    string Explanation;        // 調べた時に表示されるメッセージ

 

    Outline m_outline;
    const float SELECT_OUTLINE_WIDTH = 16.0f;

 

    void Awake()
    {
        // アウトラインの初期化
        m_outline = GetComponent<Outline>();
        m_outline.OutlineMode = Outline.Mode.OutlineVisible;
        m_outline.OutlineColor = Color.red;
        m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH;
        m_outline.enabled = false;
    }

 

    // アイテムを調べた時の仮想関数
    public virtual void ItemCheck()
    {
        // 調べた時の処理を行う
        ItemGet();
    }
    // アイテムを調べた時の基本処理
    public void ItemGet()
    {

        // デバッグ用 アイテム名と説明文をコンソールに出力
        Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation);

    }

    // アイテムを使用した時の仮想関数
   public virtual void Ite
mUse() { }

 

    // 自分にカーソルが合った時
    public virtual void StartSelect()
    {

        // アウトラインを表示する
        m_outline.enabled = true;
    }

    // 自分がカーソルから外れた時
    public virtual void EndSelect()
    {

        // アウトラインを削除する
        m_outline.enabled = false;
    }

}

【プログラムの解説】

​・Multiline(3)はアトリビュートの一種で、インスペクターにstring型を表示する際に指定した行数表示できるようにするものです。今回ではインスペクターに3行表示できるようになっています。

​ あくまでインスペクターでの表示を制御するものであって、4行以上入力できないわけではないので注意してください。

・const のついた変数は定数として扱われます。定数は宣言した時のみ値を指定でき、処理の途中で値を変更することはできません。プログラムの途中にうっかり値を変えてしまわないように、固定の値は定数にしておくといいでしょう。

・public virtual void ItemCheck など virtual がついている関数がいくつかあります。

 これは仮想関数というもので、基底クラスを継承した派生クラスで関数の中身を再定義することができる関数です。現時点では何を言ってるのかわからないかもしれませんが、Lesson3で詳しく説明するので今はあまり気にしなくて構いません。C++でも継承、仮想関数の授業はやっているはずなので詳しくはC++の教材を確認してください。

¥nは改行文字で、string内で入力することで改行することができます。​

 このサイトでは¥を直接半角入力できないので、全角で表示しています。実際に記述するときは半角にしてください

3

黄色い本を追加

​ ここまで書けたら保存してください。試しに黄色い本を配置してみましょう。

​ プロジェクトウィンドウ内の「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Miscellaneous」→「Books」を選択し、book_singleをドラッグ&ドロップで食堂の机の上に配置してください。

 座標はX=61.4 Y=5.2 Z=-120で、回転はX=0 Y=-90 Z=90に設定します​。

4

タグを追加&設定

 配置したbook_singleの名前を「Item_YellowBook」にして、新しいタグ「Item」を追加して設定してください(タグの作り方は3Dアクションゲーム編の1-6を確認してください)​

5

マテリアルを変更

 本の表紙の色を変更します。Mesh RendererのMaterials内のElement1をgoldに変更してください。​表紙のマテリアルが変わったらOKです。

6

オブジェクトを
​まとめる

 黄色い本以外にも今後様々なアイテムやギミックを設置していきます。ヒエラルキーで見やすいように1つの親オブジェクトにまとめてしまいましょう。

​ 新しい空オブジェクトGimmickを作成して、Item_YellowBookを子オブジェクトにしてください。​以降も追加したギミックは随時Gimmickの子オブジェクトにしていきます。

7

プレハブ化を
​解除する

 Item_YellowBookのプレハブ化を解除しておきましょう。Item_YellowBookを右クリック→「Prefab」→「Unpack Completely」を選択してください。

8

アウトラインを
​適用

 黄色い本に先ほど作成したItemObjectコンポーネントと同梱したOutlineコンポーネントをアタッチしてください。OutlineはC#スクリプトの方を選択してください。

9

名前と説明文を
​入力する

 インスペクターに先ほど追加したNameとExplanationが表示されているので、適当に名前と説明文を入力してください。これはテスト用なので適当でも構いません。

​ Outlineには何もしなくてOKです。

10

球体を発射して
​カーソルを実装

 次はアイテムを調べるためのカーソルを実装しましょう。

​ UnityにはSphereCastという機能があります。これは「透明な球体の当たり判定を指定した方向に発射して、最初に衝突したオブジェクトの情報を取得する」というものです。

 これでカメラの前方向に球体を発射して、衝突したオブジェクトの情報を取得することで前方にあるアイテムを調べられるようにしましょう。

 Unityには他にも球体ではなく線を発射するRaycastという機能もありますが(2Dランゲーム編1-6で解説)線ではなく球体を発射した方が判定を広く取れるため、今回はSphereCastを使用します。

 それではPlayerItemスクリプトを作成して、以下のように入力してください。

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

 

public class PlayerItem : MonoBehaviour
{
    GameObject m_cameraObject;  /
/ メインカメラ
    GameObject m_hitObject;     // 選択中のオブジェクト

 

    const float SPHERE_RADIUS = 0.8f;           // SphereCastで発射する球体の半径
    const float SPHERE_MAX_DISTANCE = 16.0f;    // SphereCastで球体を発射する距離

 

    void Awake()
    {
        // メインカメラを取得する
        m_cameraObject = Camera.main.gameObject;
    }

 

    void Update()
    {
        // 球体を発射する
       
RaycastHit hit;
        if (Physics.SphereCast(m_cameraObject.transform.position, SPHERE_RADIUS,
            m_cameraObject.transform.forward, out hit, SPHERE_MAX_DISTANCE))
        {
            // 今見ているオブジェクトと違う場合は選択終了
            if (m_hitObject != hit.collider.gameObject && m_hitObject != null)
            {
                EneSelect();
            }

 

            // 衝突したオブジェクトを取得
            ItemObject itemObject = hit.collider.gameObject.GetComponent<ItemObject>();
            if (itemObject != null)
            {
                // 選択中の処理
                itemObject.StartSelect();
                m_hitObject = hit.collider.gameObject;

 

                // 決定時の処理
                if ((Input.GetKeyDown("joystick button 0") || Input.GetKeyDown(KeyCode.Return)))
                {
                    // アイテムに応じた処理
                    itemObject.ItemCheck();
                }

 

                // アイテム使用時の処理
                if ((Input.GetKeyDown("joystick button 2") || Input.GetKeyDown(KeyCode.I)))
                {
                    // アイテムに応じた処理
                    itemObject.ItemUse();
                }
            }

        }
        else
        {
            // どのオブジェクトにもヒットしていないので選択終了
            if (m_hitObject != null)
            {
                EneSelect();
            }
        }

    }
 

    // 選択終了
    void EneSelect()
    {
        m_hitObject.GetComponent<ItemObject>().EndSelect();
        m_hitObject = null;
    }
}

11

プレイヤーへ
​アタッチ

【プログラムの解説】

​・RaycastHit はSphereCastを行ってヒットしたオブジェクトの情報を格納する場所になります。


・Physics.SphereCast は前述の通り、球体を発射して衝突したオブジェクトの情報を取得する関数です。

​ 第一引数に発射の始点、第二引数に発射する球体の半径、第三引数に発射する方向ベクトル、第四引数に衝突したオブジェクト情報の格納先(RaycastHit)、第五引数に球体を発射する距離を指定します。

 SphereCastを行って何かにヒットした場合はtrueを返し、しなかった場合はfalseを返します。if文の中でSpheteCastを行っているのは、何かにヒットした時だけ処理を行うためです。

・(Input.GetKeyDown("joystick button 0") || Input.GetKeyDown(KeyCode.Return) でゲームパッドのAボタンが押されたときかエンターキーが押された時の判定を行っています。

​ プログラムが書けたら保存して、プレイヤーにPlayerItemをアタッチしてください。

12

カーソル画像を
​追加する

 今のままではどこを注目しているかわかりにくいので、カーソルを表示しましょう。

​ 「UI」→「Image」を選択してください。

13

Canvasを調整

 自動でCanvasが追加されるので、画面サイズが変わってもCanvasの大きさを自動調整するようにしましょう。UI Scale Modeを「Scale With Screen Size」にして、Xを800、Yを600に設定してください。

14

カーソル画像の
​調整

 追加したImageの座標を中心にして、WidthとHeightを50に設定してください。Source ImageはCursorにしましょう。

 Canvasの中央に赤いカーソルが表示されたら完成です。​

 それではゲームを実行して、先ほど設置した黄色い本にカーソルを合わせてみてください。

​ 赤いアウトラインが表示されたらSphereCastで判定が取れているということになります。カーソルを合わせた状態でAボタン(Enterキー)を押して、ログが出力されることも確認してください。

2-2

2-2 アイテムを拾う(前編)

2-2 アイテムを拾う(前編)

1

アイテムリストを
​作成

 カーソルの判定が取れたので、次はアイテムを拾ってみましょう。このゲームではアイテムを3つまで持ち歩くことが可能です。所持しているアイテムを管理するために、それぞれのアイテムにID(番号)を割り振り、配列に入っているIDで所持アイテムを管理するようにしましょう。

 まずはアイテムリストを作成しましょう。

 ItemDataスクリプトを作成して、以下のように入力してください。

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

 

// アイテム用のデータベース
[CreateAssetMenu(fileName = "ItemDataBase", menuName = "CreateItemDataBase")]
public class ItemData : ScriptableObject
{
    // アイテム情報の構造体
    [System.Serializable]
    public struct Item
    {
        public string ItemName;         // アイテム名
        [Multiline(2)]
        public string ItemExplanation;  // アイテム欄で表示する説明文
        public GameObject ItemPrefab;   // アイテムを捨てた時に生成するオブジェクト

    }
 

    // アイテムリストの可変長配列
    public Item[] Items;
}

 見慣れない要素がたくさんあって戸惑うかもしれませんが、そこまで難しい内容ではないので1つずつ確認していきましょう。

 

【スクリプトの解説】

​・CreateAssetMenu はクラス名の前に記述することで、スクリプトからアセットを作成できるようにするアトリビュートです。

 

 [CreateAssetMenu(fileName = "アセットのデフォルト名", menuName = "Createメニューに表示される内容")]


​ 使用するクラスには public class ItemData : ScriptableObject のようにScriptableObjectクラスを継承する必要があるので注意しましょう。

 作成したアセットは他のスクリプトから参照することができます。今回のようなアイテム一覧であったり、キャラクターのステータスの管理などに活用できます。
公式リファレンス

​・public struct Item は構造体です。構造体は複数の変数を1つにまとめたものです。

 今回はアイテム名、説明文、捨てた時に生成するオブジェクトという3つの変数をまとめたものを「Item」と名付けています。

 構造体についてはC++の教科書なども参考にしてください(参考資料

​・public Item[] Items; は前述したItem構造体の可変長配列です。可変長配列についてもC++の教科書を参考にしてください。

​ 配列の0番に黄色い本の情報、4番に赤の宝玉の情報…といった風に各番号にアイテムの情報が入っています。この番号が所持アイテムを管理する際のアイテムIDになります。

2

データベースを
​作成する

 それではデータベースを作成しましょう。

​ プロジェクト内で右クリックして「Create」→「CreateItemDataBase」を選択してください。アイテムの情報を管理するアセットが追加されます。

3

データベースを
​設定する

 インスペクターには先ほど作成したアイテムリストが表示されています。

 Itemsの+ボタンを押すと要素数を増やすことができます。要素数を増やしておきましょう。

 まずはElement0に黄色い本のデータを記述しましょう(アイテム名や説明文はお好みで調整しても構いません)

【サンプルの記入例】

・ItemName    :黄色い本

・​ItemExplanation :黄色い表紙の本
           中身は美術書のようだ

・ItemPrefab     :(未設定)

4

データベースに
​対応させる

 次は先ほどの黄色い本にアイテム番号とアイテムリストの情報を設定します。

​ ItemObjectスクリプトを開いて赤い部分のコードを追加してください。

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

 

public class ItemObject : MonoBehaviour
{
    [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")]
    int ItemID = -1;        // アイテム識別用の番号
    [SerializeField]
    ItemData ItemDataBase;  // アイテムリスト

 

    [SerializeField]
    string Name;        // カーソルを合わせた時に表示する名前
    [SerializeField, Multiline(3)]
    string Explanation;        // 調べた時に表示されるメッセージ

 

    Outline m_outline;
    const float SELECT_OUTLINE_WIDTH = 16.0f;

 

    void Awake()
    {
        // アウトラインの初期化
        m_outline = GetComponent<Outline>();
        m_outline.OutlineMode = Outline.Mode.OutlineVisible;
        m_outline.OutlineColor = Color.red;
        m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH;
        m_outline.enabled = false;
    }

 

    // アイテムを調べた時の仮想関数
    public virtual void ItemCheck()
    {
        // 調べた時の処理を行う
        ItemGet();
    }

    // アイテムを調べた時の基本処理
    public void ItemGet()
    {
        if (ItemID == -1)
        {
            // アイテムを調べた

 

            // デバッグ用 アイテム名と説明文をコンソールに出力
            Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation);
        }
        else
        {
            // アイテムを取得

 

            // デバッグ用 獲得したアイテム名をコンソールに出力
            Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得");
            // 自身を削除する
            Destroy(gameObject);
        }


    }

 

​~後略~

【プログラムの解説】

​・Tooltip("文章") はアトリビュートの一種で、インスペクター内で注釈を表示することができます。変数名にマウスカーソルを合わせると確認できます。用途を忘れそうなパラメータに記述しておくと良いでしょう。

5

パラメータを
​設定する

・ItemDataBase.Items[ItemID].ItemName でアイテムリストの配列にアクセスして、アイテムリスト0番の名前を表示しています。同じように説明文やプレハブにもアクセスできます。

 コードが書けたら保存して、黄色い本のインスペクターを確認してみましょう。アイテムIDとアイテムリストを指定する項目が増えているはずです。

 アイテムIDは先ほど黄色い本の情報を入れた場所、つまり今回は0番になります。ItemDataBaseは先ほど作成したItemDataBaseをプロジェクトからドラッグ&ドロップしてください。

​ アイテム名と説明文はItemDataBaseから引っ張ってくるので、空白にして構いません。

 ゲームを実行して、黄色い本を調べてみてください。黄色い本が取得でき(ているように見え)たらOKです。

 現時点では取得したアイテムを管理する処理がないためオブジェクトが消えるだけですが、次は所持アイテムを管理する機能を実装しましょう。

2-3

1

GameManager

スクリプトを作成

 今回使用したScriptableObjectをより管理しやすいように、エディタ拡張で編集メニューを作ることができます。特に大量の項目を扱いたい時はデフォルトの表示では少々使いにくいため、用途に合わせた自作ウィンドウを用意するのがオススメです。

 コードもあまり難しくないため、よりステップアップしたい人はぜひ挑戦してみてください。

Unity Tips!

2-3 アイテムを拾う(後編)

2-3 アイテムを拾う(後編)

 カーソルの判定が取れたので、次はアイテムを拾ってみましょう。2-2ではアイテムのオブジェクトが消えるだけでしたが、次は取得したアイテムを所持アイテムとして記憶するようにします。

 GameManagerスクリプトを作成してください。歯車のアイコンになりますがこれはUnityの仕様で、動作に違いはありません。

 GameManagerスクリプトには以下のように入力してください。少し長いですが、アイテム管理のベースとなる部分なので頑張って入力しましょう。

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

 

public class GameManager : MonoBehaviour
{
    // アイテムデータ
    [SerializeField]
    ItemData Item_Data;
    public ItemData GetItemData()
    {
        return Item_Data;
    }

 

    // 選択中のアイテム番号(アイテム欄配列の番号)
    [SerializeField]
    int SelectItemNo = 0;
    public int GetSelectItemNo()
    {
        return SelectItemNo;
    }

 

    // アイテム欄
    [SerializeField]
    int[] ItemID;
    // 引数番スロットのアイテムを取得
    public int GetItemID(int no)
    {
        return ItemID[no];
    }

 

    // アイテムを取得する
    // アイテム欄に空きがあったらtrue なかったらfalseを返す

    public bool GetItem(int getItemID)
    {
        int selectID = SelectItemNo;

 

        // 順番にアイテム欄を確認していって、空いている場所にIDを格納
        for (int i = 0; i < ItemID.Length; i++)
        {
            if (ItemID[selectID] == -1)
            {
                // 空きがあるのでアイテムIDを格納
                ItemID[selectID] = getItemID;

 

                return true;
            }

 

            selectID++;

            if (selectID > ItemID.Length - 1)
            {
                // オーバーしたので0に戻す
                selectID = 0;
            }
        }

 

        // 空きがなかった
        return false;
    }

 

    void Update()
    {

        // 選択アイテムの変更
        if ((Input.GetKeyDown("joystick button 4") || Input.GetKeyDown(KeyCode.Alpha1)))
        {
            SelectItemNo++;
            if (SelectItemNo > ItemID.Length - 1)
            {
                SelectItemNo = 0;
            }
        }
        if ((Input.GetKeyDown("joystick button 5") || Input.GetKeyDown(KeyCode.Alpha2)))
        {
            SelectItemNo--;
            if (SelectItemNo < 0)
            {
                SelectItemNo = ItemID.Length - 1;
            }
        }

 

    }
 

}

【プログラムの解説】

・​GetItem関数内でアイテムの取得処理を行っています。引数には獲得するアイテムのIDを指定します。

 一見難しく見えるかもしれませんが、アイテム欄の配列を順番に見ていって-1の場合、そこにアイテムIDを格納するというだけの関数です。

 アイテム欄に空きが見つかってアイテム欄にIDを格納できた場合、GetItem関数はtrueを返します。逆にアイテム欄がいっぱいだった場合はfalseを返します。

2

管理用の
​オブジェクト作成

 空オブジェクトを作成して、GameManagerスクリプトをアタッチしてください。

 タグは「GameController」にしておきます(Unityがデフォルトで用意してくれているタグ)

 

 Item_DataにはプロジェクトからItemDataBaseをドラッグ&ドロップしてください。

 int[] ItemID; の部分がアイテム欄を格納するためのint型の配列になります。

 インスペクターでアイテム欄を3つに拡張して、値を-1にしておきましょう。値が-1の時は空欄(アイテムなし)として扱います。

3

アイテムを取得

 後はアイテムを取得するときにGetItem関数を呼び出すだけになります。

​ ItemObjectスクリプトを開いて、赤い部分のコードを追加してください。

 青い部分は穴埋めになります。3Dアクションゲーム編も参考にして埋めてみましょう。

​【ヒント】① はタグでオブジェクトを検索して、コンポーネントを取得する処理です。

​【ヒント】② は先ほど作成したGetItem関数を使用します。引数にはアイテムIDを使います。

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

 

public class ItemObject : MonoBehaviour
{
    [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")]
    int ItemID = -1;        // アイテム識別用の番号
    [SerializeField]
    ItemData ItemDataBase;  // アイテムリスト

 

    [SerializeField]
    string Name;        // カーソルを合わせた時に表示する名前
    [SerializeField, Multiline(3)]
    string Explanation;        // 調べた時に表示されるメッセージ

 

    Outline m_outline;
    const float SELECT_OUTLINE_WIDTH = 16.0f;

 

    // ゲームマネージャー
    GameManager m_gameManager;

 

    void Awake()
    {
        // アウトラインの初期化
        m_outline = GetComponent<Outline>();
        m_outline.OutlineMode = Outline.Mode.OutlineVisible;
        m_outline.OutlineColor = Color.red;
        m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH;
        m_outline.enabled = false;
        // ① ゲームマネージャーを取得
        // 【ヒント1】GameManagerのタグ名は「GameController」です
        // 【ヒント2】コンポーネントを取得する関数はGetComponentです

        m_gameManager = (ここに入力)
    }

 

    // アイテムを調べた時の仮想関数
    public virtual void ItemCheck()
    {
        // 調べた時の処理を行う
        ItemGet();
    }
    // アイテムを調べた時の基本処理
    public void ItemGet()
    {
        if (ItemID == -1)
        {
            // アイテムを調べた

 

            // デバッグ用 アイテム名と説明文をコンソールに出力
            Debug.Log("アイテム名:" + Name + "¥n説明文:" + Explanation);
        }
        else
        {
            // ② アイテムを取得
            // 【ヒント】ゲームマネージャーのGetItem関数を使おう

            bool isGet = (ここに入力)

 

            // アイテム欄に空きがあったかどうかで分岐
            if (isGet)
            {

                // アイテムを獲得できた

 

                // デバッグ用 獲得したアイテム名をコンソールに出力
                Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得");

 

                // 自身を削除する
                Destroy(gameObject);
            }
            else
            {

                // アイテム欄がいっぱいだった

 

                // デバッグ用
                Debug.Log("アイテム欄がいっぱいです");

            }

        }

    }

​~後略~

 コードが書けたら保存して、ゲームを実行してみましょう。

 インスペクターにGameManagerを表示した状態で、黄色い本を拾ってみてください。本を拾った瞬間にItemIDのElement 0が-1から0に変化したらOKです。コンソールにも「黄色い本を取得​」と出力されます。

 アイテム欄の0番が-1(空欄)から0(黄色い本のID)に変化したことで「現在黄色い本を持っている」ことをGameManagerが覚えたことがわかります。

 今はアイテム欄のUIがないのでわかりにくいかもしれませんが、これでアイテムを取得する処理は完成です。

 アイテム欄がいっぱいだった時の処理も正常に動作するか確認してみましょう。

 Prefabフォルダを作成して、Prefabフォルダ内にItem_YellowBook(黄色い本)をドラッグ&ドロップしてプレハブ化してください。アイテム欄をいっぱいにするため、本は3冊追加します。

 ゲームを実行して、黄色い本を全て拾ってみてください。

 3冊目まではIDを格納できますが、4冊目を拾おうとしても拾えず、コンソールに「アイテムがいっぱいです」と出力されるはずです。

 これでアイテムが3つまでしか持てないことを確認できました。動作を確認できたら、追加した3冊の黄色い本は削除しておいてください

2-4

2-4 アイテムを捨てる

2-4 アイテムを捨てる

1

プレハブを登録

 ここまでの内容でアイテムを3つまで持つことができるようになりました。

 ですが、このゲームにはギミック解除に使わないダミーのアイテムがあります。ダミーのアイテムがアイテム欄を3つとも占有してしまうとギミックを解けない、いわゆる「詰み」の状態になってしまいます。

​ 詰み防止のため、持っているアイテムを捨てる処理を実装しましょう。アイテムを持っているときにBボタンが押されたら、アイテムのオブジェクトを生成して前方向に飛ばします。同時にアイテム欄の対応した場所のIDを-1(空欄)に戻しましょう。

 黄色い本は前回のLessonでプレハブ化したので、これをアイテムデータベースに登録しましょう。

 まずはItemDataBaseを選択して、右上の鍵アイコンをクリックしてロックしましょう。ロックすることで、他の場所をクリックしてもインスペクターが切り替わらないようになります。この後、黄色い本のプレハブを選択した時にインスペクターが切り替わらないようにロックをかけておきましょう。

 Prefabフォルダ内の黄色い本のプレハブを、黄色い本の項目のItemPrefabにドラッグ&ドロップしてください。

​ プレハブの設定ができたらインスペクターのロックを外しましょう。

2

アイテムを生成

3

アイテムに
​物理演算を適用

 次は「アイテムを持っているときにBボタン(0キー)が押されたら、プレハブを生成して前方に発射する」という処理を書いてみましょう。

​ まずは発射される側であるItemObjectスクリプトに、自身を発射する関数を追加します。赤い部分のコードを追加してください。

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

 

public class ItemObject : MonoBehaviour
{
    [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")]
    int ItemID = -1;        // アイテム識別用の番号
    [SerializeField]
    ItemData ItemDataBase;  // アイテムリスト

 

    [SerializeField]
    string Name;        // カーソルを合わせた時に表示する名前
    [SerializeField, Multiline(3)]
    string Explanation;        // 調べた時に表示されるメッセージ

 

    Outline m_outline;
    const float SELECT_OUTLINE_WIDTH = 16.0f;

 

    const float ITEMDROP_POWER = 20.0f;     // アイテムを捨てる時にかける力
    const float ITEMDROP_TORQUE = 60.0f;    // アイテムを捨てる時にかける回転量

 

    // ゲームマネージャー
    GameManager m_gameManager;

 

    void Awake()
    {

~後略~

~前略~

            else
            {
                // アイテム欄がいっぱいだった

 

                // デバッグ用
                Debug.Log("アイテム欄がいっぱいです");
            }

        }
    }

 

    // アイテムを捨てた時の処理
    public void ItemDrop(Vector3 playerVelocity)
    {

        // リジッドボディの取得
        Rigidbody rb = GetComponent<Rigidbody>();
        // 物理演算を有効にする
        rb.isKinematic = false;
        // カメラの前方向に飛ばす
        rb.AddForce((Camera.main.transform.forward * ITEMDROP_POWER) + playerVelocity,
            ForceMode.Impulse);

        // ランダムに回転
        rb.AddTorque(Random.onUnitSphere * Random.Range(-ITEMDROP_TORQUE, ITEMDROP_TORQUE));  
    }

 

    // アイテムを使用した時の仮想関数
    public virtual void ItemUse() { }

~後略~

 Rigidbodyクラスの関数であるAddForce(引数ベクトルの力を加える)とAddTorque(引数ベクトルの回転を加える)を使って、オブジェクトを捨てる演出を実装しました。

【プログラムの解説】

​・isKinematic は物理演算が有効かどうか設定するパラメータになります。

Camera.main.transform.forward でメインカメラの前方向を取得できます。

​ メインカメラの前方向​に飛ばす力(ITEMDROP_POWER)を乗算して、プレイヤーの移動力と合わせることで発射の方向を決めています。

​ 第二引数であるForceModeで力の加え方を決めることができます。例えば今回使用したForceMode.Impulseは瞬間的に力を加えるモードです(参考サイト

​・Random.onUnitSphere は半径1の球体の表面上のランダムな1点を返します。これによってランダムな方向のベクトルを生成しています。

​ コードが書けたら保存して、黄色い本のプレハブをダブルクリックしてください。

 黄色い本のプレハブを開いたらインスペクターの「Add Component」からRigidbodyをアタッチしてください。

4

物理演算の
​パラメータ調整

 追加したRididbodyのパラメータを設定します。

​ Is Kinematicにチェックを入れると、物理演算が行われないようにできます。チェックを外すといつでも物理演算を再開できます。普段は物理演算を無効にしておいてオブジェクトを捨てた時だけ有効にするため、ここではチェックを入れておきましょう。

5

捨てる処理を
​実行する

 後はボタンが押された時にオブジェクトを捨てる処理を実装するだけになります。

​ GameManagerスクリプトを開いて、赤い部分のコードを追加してください。

~前略~
 

            selectID++;
            if (selectID > ItemID.Length - 1)
            {
                // オーバーしたので0に戻す
                selectID = 0;
            }
        }

        // 空きがなかった
        return false;
    }

 

    // 引数番スロットのアイテムを捨てる
    void ItemDrop()
    {
        // アイテムがあるか確認
        if (ItemID[SelectItemNo] == -1)
        {
            Debug.Log("【エラー】" + SelectItemNo + "番にアイテムがありません!");
            return;
        }

 

        // プレイヤーの移動量を取得
        Rigidbody playerRb =
            GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody>();
        Vector3 velocity = playerRb.velocity;
        velocity.y = 0.0f;

 

        // 捨てるアイテムを生成
        Vector3 itemPos = Camera.main.transform.position;
        GameObject dropItem = Instantiate(Item_Data.Items[ItemID[SelectItemNo]].ItemPrefab,
            itemPos, Camera.main.transform.rotation);
        // 前方に発射
        dropItem.GetComponent<ItemObject>().ItemDrop(velocity);

        // アイテム欄のIDをリセット
        ItemID[SelectItemNo] = -1;
    }

 

    void Update()
    {
        // Bボタンで捨てる
        if ((Input.GetKeyDown("joystick button 1") || Input.GetKeyDown(KeyCode.Alpha0)))
        {
            ItemDrop();
        }

 

        // 選択アイテムの変更
        if ((Input.GetKeyDown("joystick button 4") || Input.GetKeyDown(KeyCode.Alpha1)))
        {
            SelectItemNo++;
            if (SelectItemNo > ItemID.Length - 1)
            {
                SelectItemNo = 0;
            }
        }

~後略~

【プログラムの解説】

​・Rigidbodyのパラメータであるvelocityは「対象にかかっている力」を表すパラメータです。例えばプレイヤーが前方に移動しながらアイテムを投げると、棒立ちで投げた時と比べてより強い力で前方に発射されることになります。

・Instantiate関数はオブジェクトを生成する関数になります。3Dアクションゲーム編では大砲の弾を発射する時に使用しました。

​ 第一引数に生成するゲームオブジェクト、第二引数に生成する座標、第三引数に回転を指定します。

​ コードが書けたら保存して、ゲームを実行してみてください。アイテムを拾った状態でBボタン(0キー)を押すとアイテムを捨てることができます。

 アイテムを捨てるとGameManagerのアイテム欄の0が-1に変化することを確認してください。

 アイテムを持っていない状態でアイテムを捨てようとするとエラー文がコンソールに出力されます。

6

レイヤーを追加

 これで一通り完成ではあるのですが、捨てたアイテムがプレイヤーのコライダーと衝突してしまう点が少し気になります。特に壁際でアイテムを捨てると、不自然な挙動になってしまいます。

 Unityではレイヤーを使うことで、特定のオブジェクトとの当たり判定を無視することができます。捨てたアイテムがプレイヤーに衝突しないように設定してみましょう。

 まずはアイテムとプレイヤーを区別するレイヤーを作成しましょう。

​ 適当なオブジェクトのLayerをクリックして「Add Layer」を選択してください。

 レイヤーの名前を入力する項目が開くので、User Layer7に「Player」、User Layer8に「DropItem」と入力してください。

 プレイヤーを選択してLayerを「Player」に変更してください。

 子オブジェクトのレイヤーも変更するかどうか確認されますが、変更しなくて構いません。

 黄色い本のプレハブを開いて、レイヤーを「DropItem」に変更しましょう。

7

レイヤー同士の
​衝突判定を設定

 レイヤー同士の衝突判定を設定します。

 「Edit」→「Project Settings…」→「Physics」を選択してください。​下の方に「Layer Collision Matrix」という、どのレイヤー同士が衝突判定を行うか指定する項目があります。DropItemとPlayerは衝突判定をしないようにチェックを外しておいてください。

8

アイテムの取得に
​待ち時間を設ける

 ゲームを実行して、プレイヤーと捨てたアイテムが衝突しないようになったか確認してみましょう。

 最後にささいなことですが、捨てた直後のアイテムを空中ですぐ拾えないように、捨ててから1秒立つまでは拾えないようにしましょう。

​ ItemObjectスクリプトを開いて、赤い部分のコードを追加してください。

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

 

public class ItemObject : MonoBehaviour
{
    [SerializeField, Tooltip("-1は調べられるアイテム 0以上は取得できるアイテム")]
    int ItemID = -1;        // アイテム識別用の番号
    [SerializeField]
    ItemData ItemDataBase;  // アイテムリスト

 

    [SerializeField]
    string Name;        // カーソルを合わせた時に表示する名前
    [SerializeField, Multiline(3)]
    string Explanation;        // 調べた時に表示されるメッセージ

 

    Outline m_outline;
    const float SELECT_OUTLINE_WIDTH = 16.0f;

 

    const float ITEMDROP_POWER = 20.0f;     // アイテムを捨てる時にかける力
    const float ITEMDROP_TORQUE = 60.0f;    // アイテムを捨てる時にかける回転量

 

    [SerializeField]
    bool IsCheck = false;    // 調べることが可能かどうか
    public void SetIsCheck(bool flag)
    {
        IsCheck = flag;
    }

 

    // ゲームマネージャー
    GameManager m_gameManager;

 

    void Awake()
    {
        // アウトラインの初期化
        m_outline = GetComponent<Outline>();
        m_outline.OutlineMode = Outline.Mode.OutlineVisible;
        m_outline.OutlineColor = Color.red;
        m_outline.OutlineWidth = SELECT_OUTLINE_WIDTH;
        m_outline.enabled = false;

        // ① ゲームマネージャーを取得
        // 【ヒント1】GameManagerのタグ名は「GameController」です
        // 【ヒント2】コンポーネントを取得する関数はGetComponentです

        m_gameManager = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
    }

 

    // アイテムを調べた時の仮想関数
    public virtual void ItemCheck()
    {
        // 調べた時の処理を行う
        ItemGet();
    }
    // アイテムを調べた時の基本処理
    public void ItemGet()
    {
        // 調べられない状態
        if (IsCheck)
        {
            return;
        }

 

        if (ItemID == -1)
        {
            // アイテムを調べた

 

            // デバッグ用 アイテム名と説明文をコンソールに出力
            Debug.Log("アイテム名:" + Name + "\n説明文:" + Explanation);
        }
        else
        {
            // ② アイテムを取得
            // 【ヒント】ゲームマネージャーのGetItem関数を使おう

            bool isGet = m_gameManager.GetItem(ItemID);

 

            // アイテム欄に空きがあったかどうかで分岐
            if (isGet)
            {
                // アイテムを獲得できた

 

                // デバッグ用 獲得したアイテム名をコンソールに出力
                Debug.Log(ItemDataBase.Items[ItemID].ItemName + "を取得");

                // 自身を削除する
                Destroy(gameObject);
            }
            else
            {
                // アイテム欄がいっぱいだった

 

                // デバッグ用
                Debug.Log("アイテム欄がいっぱいです");
            }

 

        }
    }

 

    // アイテムを捨てた時の処理
    public void ItemDrop(Vector3 playerVelocity)
    {
        // リジッドボディの取得
        Rigidbody rb = GetComponent<Rigidbody>();
        // 物理演算を有効にする
        rb.isKinematic = false;
        // カメラの前方向に飛ばす
        rb.AddForce((Camera.main.transform.forward * ITEMDROP_POWER) + playerVelocity,
            ForceMode.Impulse);
        // ランダムに回転
        rb.AddTorque(Random.onUnitSphere * Random.Range(-ITEMDROP_TORQUE, ITEMDROP_TORQUE));

 

        // しばらく調べられないようにする
        IsCheck = true;
        // 1秒後に調べられるようになる
        Invoke("CheckWait", 1.0f);
    }
    // Invokeで呼び出す関数
    void CheckWait()
    {
        IsCheck = false;
    }

 

    // アイテムを使用した時の仮想関数
    public virtual void ItemUse() { }
 

    // 自分にカーソルが合った時
    public virtual void StartSelect()
    {
        // 調べられない状態
        if (IsCheck)
        {
            return;
        }

 

        // アウトラインを表示する
        m_outline.enabled = true;
    }

    // 自分がカーソルから外れた時
    public virtual void EndSelect()
    {
        // アウトラインを削除する
        m_outline.enabled = false;
    }

}

【プログラムの解説】

Invoke関数は指定した関数を一定秒数後に呼び出すことができる関数です。k2Engineではタイマーを設定するなどしていたと思いますが、Unityでは単純に一定秒数後に呼び出すだけならInvoke関数を使うと便利です。第一引数に関数名、第二引数に待機時間を設定します。

 Invoke(呼び出す関数の名前,何秒後に呼び出すか);

※ 関数名で検索しているので名前間違いに注意!
※ 2Dランゲーム編に
追加の解説があります


 

 これでアイテムを捨てた後、1秒間はカーソルが合わないようになりました。

 Invoke関数は呼び出す関数を名前で指定するため関数の名前を後から変えにくかったり、安定性に少し問題があったりとデメリットもありますが、簡単に一定秒数後に処理を実行したい場合には便利なので覚えておきましょう(代替手段としてコルーチンというものもあります)

 次はいよいよアイテムを使ったギミックを実装していきます。

まとめ

・public class Player : MonoBehaviour

{

 …

}

 → このクラスは「MonoBehaviour」クラスを継承している

・継承元のクラスを基底クラス 継承先のクラスを派生クラスと呼ぶ

​・継承をすることで、基底クラスのメンバ変数やメンバ関数を派生クラスで使うことができる

・関数に virtual を付けることで仮想関数を定義することができる

 基底クラスに定義した仮想関数は、派生クラスで関数の中身を上書き(オーバーライド)することができる

評価テスト

評価テスト

河原電子ビジネス専門学校
​ゲームクリエイター科

bottom of page