top of page

3D脱出ゲーム編

Lesson3 ギミックを実装しよう

3-1

3-1 土台の実装

3-1 土台の実装

1

本棚の追加

 いよいよゲームの「遊び」の部分である謎解きを実装していきます。前半はコードが多く大変かもしれませんが、後半はほとんどコードを流用するので頑張りましょう。

 まずは謎解きの鍵であるアイテムを置く台を実装します。

 アイテムを持った状態でXボタン(Iキー)を押すことで、持っているアイテムを台に置くことができます。台に置いたアイテムが正解と一致していた場合、ギミックを解除できます。

 書斎(玄関から一番遠い部屋)に台を置くための本棚を設置してください。本棚と本をCtrlキーでまとめて選んでからコピーしましょう。

3_3_2
3_3_3

 追加するのは本棚に限らず机などでも構いません。台を置く場所が作成できればOKです。

2

台の追加

 本棚のスペースに台を設置しましょう。

 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Miscellaneous」と選択して、granite_panel をシーン上にドラッグ&ドロップしてください。

 台を本棚に設置した場合は、本棚に収まるようにサイズを調整しましょう。

​ 黄色い本に対応していることがわかりやすいように、マテリアルは本と同じもの「gold」に変更しておきます。

 Xボタン(Iキー)が押されたらアイテムを使用する という処理はPlayerItemスクリプトに既に記述しています。

3

台の処理を実装

 後はアイテムを「使われる側」である土台のスクリプトを書いていきましょう。

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

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

 

public class ItemPlatform : ItemObject // 【重要!】ItemObjectを継承する
{
    [SerializeField, Header("土台用パラメータ")]
    int AnswerItemID = -1;  // 答えのアイテムID
    // 答えのIDを変更

    public void SetAnswerID(int id)
    {
        AnswerItemID = id;
    }

 

    bool m_isItemPlaced = false;    // アイテムが置かれているならtrue
    int m_placedItemID = -1;        // 今置かれているアイテムのID

 

    // 置かれているアイテムが正解ならtrueになる
    bool m_isAnswer = false;
    public bool GetIsAnswer()
    {
        return m_isAnswer;
    }

 

    // アイテムを使用した時の処理
    public override void ItemUse()
    {
        // アイテムが既に置かれているなら中断
        if (m_isItemPlaced)
        {
            return;
        }

 

        // ゲームマネージャーの取得
        GameManager gameManager =
            GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();

 

        // 選択中のアイテムを取得する
        int selectItemID = gameManager.GetItemID(gameManager.GetSelectItemNo());

 

        // 選択中のアイテムがないなら中断する
        if (selectItemID == -1)
        {
            return;
        }

 

        // アイテムを設置
        // ※後で記述

 

        // 置かれたアイテムのIDを保存して、アイテムが置かれているフラグを立てる
        m_placedItemID = selectItemID;
        m_isItemPlaced = true;

 

        // 答えが合っているか確認する
        if (AnswerItemID == m_placedItemID)
        {
            m_isAnswer = true;
        }
    }

 

    // アイテムを取る
    public void ItemTook()
    {
        // パラメータを初期化
        m_placedItemID = -1;
        m_isAnswer = false;
        m_isItemPlaced = false;
    }

 

}

【プログラムの解説】

​・クラスを ItemPlatform : ItemObject のように宣言することで、ItemPlatformがItemObjectを継承することができます。

 PlayerItemでItemUse関数を呼び出しているこの時点では、この関数を実行すると何が起きるかはわかっていません。「使う対象が何かは知らないけど、とりあえず持っているアイテムを使います!」と言っているイメージです。

 アイテムを使われた側が土台でも扉でも、アイテムを使われた側の挙動はアイテムを使われた側のスクリプトで決めます。

 ですが事前に「アイテムを使われた時の関数の名前」だけは決めておかないと、中身を知らずに呼び出そうにも呼び出せません。それを決めるのがItemObjectクラスのItemUse関数です。

 関数の前に virtual とついている点に注目してください。これがついている関数は仮想関数と呼び、この関数は継承先のクラスで関数の中身を上書きすることができます。

 また、関数の中身が一切ありませんが、これは派生先のクラスでそれぞれ中身を決めるので、ここでは空白になっています。このように宣言のみしてある関数を純粋仮想関数と呼びます。

 今回はItemObjectクラスを継承したItemPlatformクラスで、ItemUse関数の中身をオーバーライド(上書き)しています。

4

パラメータの設定

 例えば足場以外にも、アイテムを使うことでアクションが起きるオブジェクトを実装したい時に簡単に実装することができます。サンプルでは実装していませんが、オリジナルの謎解きを実装する際に活用してみてください。

 この時点ではよくわからないかもしれませんが大丈夫です。この後たびたび使うので実際に使って慣れていきましょう。

​ コードがかけたら保存して、土台にItemPlatformスクリプトをアタッチしてください。

 継承した場合、基底クラス(継承元)のパラメータも表示されます。

 ItemPlatformスクリプトはItemObjectを継承しているので、同じようにItemIDやNameなどのパラメータが表示されます。さらにその下にItemPlatformのパラメータであるAnswerItemIDが表示されます。

 NameとExplantionの内容は自由に入力してください。AnswerItemIDは黄色い本のIDである「0」にしておきましょう。

​ Outlineスクリプトも忘れずにアタッチしておいてください。

【サンプルの入力例】

Name    :黄色い台

Explanation :黄色い台がある…

       何か置けそうだ

AnswerItemID:0

 ゲームを実行して、黄色い土台が調べられることを確認してみてください。

​ アタッチしたのはItemPlatformスクリプトですが「カーソルが合った状態で調べると詳細表示」という、継承したItemObjectクラスの挙動をしています。これでItemObjectの挙動を引き継ぎつつ、AnswerItemIDという土台用のパラメータを追加することができました。

5

アイテムを置く
処理の実装

 // ※後で記述 となっていた、アイテムを置く処理を実装しましょう。

 まずは置かれる側であるアイテムが「自分が土台に置かれたとき、自分が置かれている台を記憶する」「土台に置かれている状態で取得されたとき、自分が置かれていた台の状態を初期化する」処理を実装します。

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

~前略~

    public void SetIsCheck(bool flag)
    {
        IsCheck = flag;
    }

 

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

 

    GameObject m_platformObj = null;    // 自分が置かれている土台
 

    void Awake()
    {
        // アウトラインの初期化
        m_outline = GetComponent<Outline>();

​~後略~

~前略~
 

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

 

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

 

                // 自分が台に置かれたアイテムだった場合、土台の設定も変更
                if (m_platformObj != null)
                {
                    m_platformObj.GetComponent<ItemPlatform>().ItemTook();
                    m_platformObj = null;
                }

 

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


​~後略~

~前略~
 

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

 

    // アイテムを置いた時の処理
    public void ItemPlaced(GameObject platform)
    {
        // 自分が置かれている土台を保存する
        m_platformObj = platform;
    }


}

 自分が土台に置かれたときにその台を記憶し、自分が取得されたときにm_platformObjがnullではない(= 自分が土台に置かれている)場合は、その土台にItemTook関数を実行させることで初期化しています。流れは少し複雑ですが、完璧に理解しなくても大丈夫です。

​ 次はアイテム設置の肝である「アイテムを生成する」処理を実装します。アイテムを捨てる処理と似ていますが、細かいところが違うので注意しましょう。

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

~前略~
 

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

 

    // アイテムを置く
    public void ItemPlaced(GameObject platformObj)
    {

        // 選んでいるアイテムIDを取得
        int id = ItemID[SelectItemNo];
 

        // 座標を決める
        Vector3 pos = platformObj.transform.position;

 

        // アイテムを生成 回転はプレハブの回転を使用
        GameObject placedItem = Instantiate(Item_Data.Items[id].ItemPrefab,
            pos, Item_Data.Items[id].ItemPrefab.transform.rotation);

 

        // 生成したアイテムに設置された土台を教える
        placedItem.GetComponent<ItemObject>().ItemPlaced(platformObj);

 

        // 生成したアイテムの親オブジェクトを土台にする
        placedItem.transform.parent = platformObj.transform;

 

        // 所持品から番号を削除
        ItemID[SelectItemNo] = -1;

    }
 

    void Update()
    {
        // Bボタンで捨てる


​~後略~

【プログラムの解説】

・transform.parent で対象の親オブジェクトの情報を取得、変更できます。

 ここでは生成したアイテムの親オブジェクトを土台に変更しています。子オブジェクトは親オブジェクトについていくため、土台が移動した時にアイテムも同じように移動するようになります。

​ 最後にItemPlaced関数を呼ぶ処理を追加しましょう。

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

~前略~
 

        // 選択中のアイテムがないなら中断する
        if (selectItemID == -1)
        {
            return;
        }

 

        // アイテムを設置
        gameManager.ItemPlaced(gameObject);

 

        // 置かれたアイテムのIDを保存して、アイテムが置かれているフラグを立てる
        m_placedItemID = selectItemID;
        m_isItemPlaced = true;

​~後略~

 コードがかけたら保存して、ゲームを実行してみてください。

 黄色い本を拾ってから、黄色い台にカーソルを合わせてXボタン(Iキー)を押してみましょう。黄色い本を設置できたらOKです。

(場所や回転がおかしいですが、今は気にしないでください)

6

デバッグモードを
​使う

 土台が正解のオブジェクト(黄色い本)が置かれたことを認識できているか確認してみましょう。

​ インスペクター右上にある3つの点があるボタンをクリックして、デバッグモードにしてみてください。デバッグモードではprivateな変数の中身を確認することができます。ただし変更はできません。

 デバッグモードでパラメータを確認しながら、土台にアイテムを置いてみてください。

 アイテムを置く前はIsItemPlaced(アイテムが置かれているかどうか)がfalseですが、アイテムを置くとtrueになります。

​ 置かれたアイテムが答えと一致しているならIsAnswer(置かれているアイテムが正解かどうか)もtrueになります。

7

座標補正用の
​パラメータを追加

 パラメータの変化を確認できたら、デバッグモードは解除しておいてください。デバッグモードは文字通りデバッグには便利ですが、項目が多いので普段は通常モードにしておくことをオススメします。

 これでアイテムを置いて、置かれたアイテムが正解かどうか判定する処理が完成しました。

​ しかし置かれたアイテムの位置や角度がおかしいので、調整できるようにしましょう。座標は土台側だけでなく、アイテム側でも個別に調整できるようにします。

​ 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 Vector3 ItemPivot;       // アイテムを置いたときに座標を補正する量

    }

 

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

 パラメータを追加したことで、座標の補正量をアイテム別に指定できるようになりました。黄色い本では使いませんが、後ほど他のアイテムを作るときに使います。

8

土台のパラメータ
​を追加

 次は土台側から調整できるようにしましょう。ItemPlatformスクリプトに赤い部分のコードを追加してください。

~前略~

    // 置かれているアイテムが正解ならtrueになる
    bool m_isAnswer = false;
    public bool GetIsAnswer()
    {
        return m_isAnswer;
    }

 

    [SerializeField]
    Vector3 AddPos, AddRot = Vector3.zero;  // アイテムを置く座標と回転に加算する値

 

    // アイテムを使用した時の処理
    public override void ItemUse()
    {

​~後略~

9

座標や回転を
​補正する

 次は座標や回転の補正をする処理を追加します。

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

~前略~
 

    // アイテムを置く
    public void ItemPlaced(GameObject platformObj, Vector3 addPos, Vector3 addRot)
    {
        // 選んでいるアイテムIDを取得
        int id = ItemID[SelectItemNo];

 

        // 座標を計算
        Vector3 pos = platformObj.transform.position + addPos;
        pos += Item_Data.Items[id].ItemPivot;

 

        // アイテムを生成 回転はプレハブの回転を使用
        GameObject placedItem = Instantiate(Item_Data.Items[id].ItemPrefab,
            pos, Item_Data.Items[id].ItemPrefab.transform.rotation);

 

        // アイテムを回転させる
        placedItem.transform.Rotate(addRot, Space.World);

 

        // 生成したアイテムに設置された土台を教える
        placedItem.GetComponent<ItemObject>().ItemPlaced(platformObj);

 

        // 生成したアイテムの親オブジェクトを土台にする
        placedItem.transform.parent = platformObj.transform;
        
        // 所持品から番号を削除
        ItemID[SelectItemNo] = -1;
    }

​~後略~

【プログラムの解説】

・Transform.Rotate関数は「オブジェクトを第一引数分回転を加算する」という簡単な関数です。

 第二引数でローカル座標とワールド座標、どちらを基準にするか決めることができます(よくわからない人はとりあえずワールド座標にしておくとわかりやすいはずです)

 ItemPlaced関数に引数を追加したので、ItemPlatformスクリプトでエラーが出ているはずです。

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

~前略~
 

        // 選択中のアイテムがないなら中断する
        if (selectItemID == -1)
        {
            return;
        }

 

        // アイテムを設置
        gameManager.ItemPlaced(gameObject, AddPos, AddRot);

 

        // 置かれたアイテムのIDを保存して、アイテムが置かれているフラグを立てる
        m_placedItemID = selectItemID;
        m_isItemPlaced = true;


​~後略~

10

パラメータの調整

 コードが書けたら保存して、各パラメータを調整しましょう。

​ 黄色い土台のAddPosを X=0 Y=0.12 Z=0.0 にしてください。これでアイテムが置かれる場所が少し上になります。

11

プレハブの調整

 回転は元からプレハブの回転を使用するようにしているので、プレハブの回転を調整しましょう。

​ 黄色い本のプレハブを開いて、Rotationを X=0 Y=0 Z=20 にしてください。

 プレハブを変更できたら、ゲームを実行して黄色い本が正常に設置されることを確認してみてください。

 これでアイテムを置ける台が完成しました。これが脱出のカギになっていきます。

3-2

3-2 絵画の実装

3-2 絵画の実装

1

絵画を追加

 「黄色い台に黄色い本が置かれたので正解!」というところまで進みました。

 次は対応した土台に置かれたアイテムが正解なら落ちる絵画を作成しましょう。

 「Model」→「Paintings」→「PaintingCollection1」→「Prefabs」と選択して、絵画のモデルをシーン上に追加してください(PaintingCollection2でも構いません。絵柄が違うだけです)

​ 追加した絵画のTransformを調整してください。

​【サンプルの入力例】

 Position X=-17.6 Y=9 Z=-9.8

 Rotation X=0 Y=90 Z=0

​ Scale X=15 Y=15 Z=15

 絵画はギミックなので、ヒエラルキー内でもGimmickの子オブジェクトに移動させておきましょう。

2

判定用の
​スクリプトを追加

 絵画は「カーソルを合わせると調べられる」部分は今までと同じですが「対応した土台に置かれているアイテムが正解なら落下する」という固有の処理があります。

 ItemPlatformと同じようにItemObjectクラスを継承して、アイテムを調べる処理は流用しましょう。

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

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

 

public class ItemGimmick : ItemObject   // 継承する
{
    [SerializeField, Header("正解判定オブジェクト用")]
    ItemPlatform[] AnswerPlatforms;     // 対応する土台
    [SerializeField, Multiline(3)]
   string AnswerExplanation;               // 正解時に説明文を変化させる

 

    // 正解時の反応の種類
   public enum AnswerPattern
   {
       enAnswer_Drop,  // 落下する
   }
   [SerializeField]
   AnswerPattern Answer_Pattern;   // 正解時の反応

    bool m_isAnswer = false;            // 正解済みかどうか
 

    void Update()
    {
        // 正解済みなら実行しない
        if (m_isAnswer)
        {
            return;
        }

 

        // 正解チェック
        bool answer = true;
        foreach (ItemPlatform platform in AnswerPlatforms)
        {
            // 1つでも不正解があるならこの先で正解処理を実行しない
            if (platform.GetIsAnswer() == false)
            {
                answer = false;
            }
        }

 

        // 正解なら設定した処理を行う
        if (answer)
        {
            Answer();
            m_isAnswer = true;
        }
    }

 

    void Answer()
    {
        // 正解時の処理
        switch (Answer_Pattern)
        {
            case AnswerPattern.enAnswer_Drop:
                // 物理演算の有効化
                Rigidbody rb = GetComponent<Rigidbody>();
                rb.isKinematic = false;
                // 前方に飛ばす
                rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse);
                // 回転をかける
                rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse);
                // 説明欄の更新
                // 後で入力

                break;
        }
    }

}

【プログラムの解説】

​・ItemPlatform[] AnswerPlatforms のように、対応した土台を指定する部分は可変長配列になっています。これは後ほど複数の土台を参照するギミックに対応するために配列にしています。

​ SerializeField がついているためインスペクターに表示されますが、配列は+ボタンを押す(あるいは要素数を直接入力する)ことで、要素数を増やすことができます。

3

アクセス修飾子を
​変更する

foreachはUnityではなくC++など様々な言語にあるループ文の一種です。

 配列の全ての要素にアクセスしたい時に使用します(Wikipedia

[サンプル]
 foreach (型 変数 in 配列) {
    処理
}

​ 最後の説明欄変更の // 後で入力 となっている部分を完成させましょう。ItemGimmickはItemObjectを継承していますが、ItemGimmickからItemObject内の変数であるExplanation(説明文)にアクセスしようとするとエラーが出てしまいます。

 派生クラスから基底クラスの変数や関数にアクセスしたい場合、対象にprotectedというアクセス修飾子をつける必要があります。

 protectedはpublicとは異なり、外部に公開されることはありませんが「自クラスか派生クラスからはアクセスできる」という特徴があります(privateでは自クラスからしかアクセスできない)

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

~前略~
 

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

 

    Outline m_outline;
    const float SELECT_OUTLINE_WIDTH = 16.0f;


​~後略~

4

エラーを修正する

 これでItemGimmickからでも説明文を変更することができます。ItemGimmickスクリプトを開いて、赤い部分のコードを追加してください。

~前略~
 

                // 前方に飛ばす
                rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse);
                // 回転をかける
                rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse);
                // 説明欄の更新
                Explanation = AnswerExplanation;
                break;
        }
    }


}

 コードが書けたら保存して、Paintingの子オブジェクトであるFrameに必要なコンポーネントをアタッチしていきましょう(Paintingにアタッチしないように注意!

5

物理演算を設定

 FrameにRigidbodyをアタッチしてください。

 絵画が勝手に落ちてしまわないように、Is Knematicにチェックを入れて物理演算を無効化しましょう。

 Collision Detection は移動の計算方式を設定できます。初期値はDiscreteですが、Continuousに変更することで少し正確な判定を取ることができます。オブジェクトが貫通しにくくなるなどの効果がありますが、Discreteより処理が重いので扱いには注意が必要です。

 FrameにBoxCollider、Outline、ItemGimmickをアタッチしてください。

6

絵画の
​パラメータを設定

 ItemGimmickのパラメータを設定しましょう。

 Name(名前)、Explanation(説明文)、AnswerExplanation(落下後の説明文)を設定してください。内容は自由です。
 

【サンプルの入力例】

Name            :絵画

Explanation     :印象的な絵画が飾ってある

AnswerExplanation:絵画が落ちてきた…

 AnswerPlatforms は要素数を1にして、ヒエラルキーから黄色い土台をElement0へドラッグ&ドロップしてください。

 これで落ちる絵画が完成しました。ゲームを実行して確かめてみましょう。

 調べるとコンソールに説明文が表示されます。黄色い台に黄色い本を置くと絵画が落下し、説明文が変化することを確認してください。

 絵画があった場所に扉のパスワードを隠しておく予定ですが、今は絵画が落下することを確認できればOKです。

3-3

3-3 アイテムの追加

3-3 アイテムの追加

1

黄色い本を
​複製する

 ItemPlatformとItemGimmickを使って2つ目のギミックを作ってみましょう。

​ 2つ目のギミックの構成は以下のようになっています。

・壁に貼られた絵の通りに4つの台にアイテムを置くと棚が動く

・3種類からランダムで答えが変わる

 まずはギミックの解除に必要なアイテムを作成しましょう。必要なアイテムは「青い本」「赤い本」「緑の本」「赤の宝玉」「青の宝玉」「黄の宝玉」「緑の宝玉」の7つです。数は多いですが、複製するので手間はあまりかかりません。

​ 黄色い本をベースに本の種類を増やしましょう。黄色い本を選んだ状態でCtrl+Dを押して複製しましょう。複製した本の名前をItem_BlueBookに変更します。

2

プレハブ化を解除

 3Dアクションゲーム編でも行った措置ですが、Item_BlueBookのプレハブ化を解除しておきましょう。現在のItem_BlueBookはPrefabフォルダ内の黄色い本のプレハブと紐づいた状態ですが、その関係を解除するものになります。

​ ヒエラルキー内のItem_BlueBookを右クリックして「Prefab」→「Unpack Completely」を選択してください。

3

本のマテリアルを
​変更

 MaterialsのElement1を「canvas_book_2_d」に変更してください。これで背表紙の色を青色に変更できます。

4

パラメータを設定

 黄色い本のItemIDは0なので、その次の1番を青い本のIDとして使用しましょう。ItemObjectコンポーネントのItemIDを1にしてください​。

 青い本の場所をお好みの座標に変更してください。黄色い本のある食堂とは違う部屋に置くのがオススメです。

【サンプルの入力例】

 Position X=120.6 Y=7.7 Z=56

 Rotation X=0 Y=0 Z=90

5

プレハブ化する

 青い本を置けたら、Prefabフォルダ内にItem_BlueBookをドラッグ&ドロップしてプレハブ化しましょう。

6

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

 ItemDataBaseを選択して、Element1に青い本のデータを記述しましょう(アイテム名や説明文はお好みで調整しても構いません)

【サンプルの記入例】

・ItemName    :青い本

・​ItemExplanation :青い表紙の本
           中身は漫画のようだ

・ItemPrefab     :Item_BlueBook(設定方法は右端の◎ボタンをクリックするか、2-4参照してインスペクターをロック)

 これで青い本の追加が完了しました。ゲームを実行して青い本を拾ったり捨てたりできるか確認してみましょう。青い本を拾うとGameManagerのItemIDに、青い本のIDである1が入ることが確認できます。

7

赤い本と緑の本を
​追加する

 同じ流れで赤い本と緑の本も追加してみましょう。

【本追加の流れ】

① 本のオブジェクトをコピー

② プレハブ化を解除

③ 表紙のマテリアルを変更(赤い本は「canvas_book_1_d」、緑の本は「canvas_book_3_d」)

④ IDを設定(他と被らないように!

⑤ 座標を変更

⑥ プレハブ化する

​⑦ ItemDataBaseを設定

【サンプルの記入例 赤い本】

・ItemName    :赤い本

・​ItemExplanation :赤い表紙の本
           中身は小説のようだ

・Position     :X=43 Y=3 Z=35

・Rotation     :X=0 Y=45 Z=90
​・Item ID      :2

【サンプルの記入例 緑の本】

・ItemName    :緑の本

・​ItemExplanation :緑の表紙の本
           中身は絵本のようだ

・Position     :X=137 Y=5.5 Z=-18

・Rotation     :X=0 Y=-30 Z=90
​・Item ID      :3

 既にプレハブ化されているオブジェクトをプレハブ化しようとすると、以下のようなウィンドウが表示されます。この場合「Original Prefab」を選択してください。

3_3_43

 ちなみに「Prefab Variant」は今あるプレハブをベースに新しいプレハブを作成するというもので、プレハブの派生を作るイメージです。派生したプレハブを編集してもオリジナルのプレハブに影響はありませんが、オリジナルのプレハブを編集すると派生したプレハブは影響を受けます。

Unity Tips!

 追加した赤い本と緑の本を正常に取得、捨てることができるか確認しておいてください。GameManagerのItemID内にそれぞれの本に対応したIDが表示されます。

8

プレハブの調整

 全ての本のプレハブを選択して座標を全て0、回転をX=0 Y=0 Z=20 にしておいてください

 アイテムを設置する際にプレハブの回転を参照しているため、全ての本で回転を合わせておかないと「黄色い本は正常に設置できるが、赤い本は異常な角度で設置される」といった現象を起こす可能性があります。

9

​青の宝玉を作成

 次は4色の宝玉を用意します。最初は「青の宝玉」を作りましょう。

​ 「3D Object」→「Sphere」を選択して、球体を追加してください。

 名前を「BlueSphere」にして、タグを「Item」にしておきます。​座標と大きさを調整してください。

【サンプルの座標】

 Position X=86 Y=5.7 Z=149

​ Rotation X=0 Y=0 Z=0

 Scale X=1.2 Y=1.2 Z=1.2

 アイテムがプレイヤーに衝突しないように、レイヤーをDropItemに変更しておいてください(レイヤーについては2-4参照)

10

マテリアルを作成

 青いマテリアルを作成します。

​ 新しく「Material」フォルダを作成してください。

 Materialフォルダ内に新しいマテリアルを作成してください。

​ Base Mapを青色に、Metallic Map(金属感)を0.8に、Smoothness(滑らかさ)を1に設定しましょう。設定ができたらBlueSphereにマテリアルをドラッグ&ドロップして貼り付けてください。

11

物理演算の設定

 BlueSphereにRigidbodyをアタッチしてください。

​ Drag(抵抗)を2にしましょう。Dragの値を上げることでオブジェクトに抵抗がかかるようになります。主に空気抵抗などの表現に使用します。今回はオブジェクトが球体なので、抵抗を設定することで遠くに転がっていかないようにしています(Dragの詳細

​ また、最初は物理演算を無効にしたいので Is Kinematic にチェックをいれておきます。

12

コンポーネントの
​アタッチ

 BlueSphereにOutlineとItemObjectをアタッチしてください。

​ ItemObjectのItemIDは本のIDと被らないように4にしておきましょう。ItemDataBaseにはプロジェクト内のItemDataBaseを設定します。

13

プレハブ化する

 これでアイテムの準備ができたので、BlueSphereをプレハブ化してください。

14

データベースを
​設定

 ItemDataBaseを開いて、青の宝珠の情報を入力してください。

【サンプルの記入例】

・ItemName    :青の宝珠

・​ItemExplanation :青色の宝玉
           ガラスでできている

​・Item Pivot    :X=0 Y=0.6 Z=0

 これで青の宝玉を拾ったり、捨てたりすることができます。ゲームを実行して確認してみましょう。

15

赤の宝玉を作成

 青の宝玉をベースに赤、黄色、緑の宝玉も作成しましょう。本の種類を増やした時と流れは同じです。


 BlueSphereをコピーして、名前を「RedSphere」に変更します。位置は青の宝玉の隣にしておきましょう。

【サンプルの座標】

​Position X=86 Y=5.7 Z=144

 青の宝玉のマテリアルを複製して色を変更した、赤の宝玉用のマテリアルを作成してください。マテリアルができたら、RedSphereにドラッグ&ドロップして貼っておきましょう。

 RedSphereにアタッチされているItemObjectのIDを5にしておいてください。

 ここまでできたらRedSphereをプレハブ化しましょう。RedSphereはBlueSphereをコピーして作ったため、BlueSphereのプレハブから派生したオブジェクトとして扱われています。RedSphereを右クリックして「Prefab」→「Unpack Completely」を選択して、BlueSphereとの関連付けを解除しましょう。

 RedSphereをPrefabフォルダ内にドラッグ&ドロップしてプレハブ化しましょう。

 青の宝玉を元に、ItemDataBaseに赤の宝玉のデータを入力してください。

【サンプルの記入例】

・ItemName    :赤の宝珠

・​ItemExplanation :赤色の宝玉
           ガラスでできている

​・Item Pivot    :X=0 Y=0.6 Z=0

16

黄色と緑の宝玉を作成

 赤の宝玉の追加と同じ手順で、黄色と緑色の宝玉も追加してください​。ItemIDの重複には注意しましょう。

【サンプルの記入例 黄の宝玉】

・ItemName    :黄の宝珠

・​ItemExplanation :黄色の宝玉
           ガラスでできている

​・Item Pivot    :X=0 Y=0.6 Z=0

・Position     :X=86 Y=5.7 Z=129
​・Item ID      :6

【サンプルの記入例 緑の宝玉】

・ItemName    :の宝珠

・​ItemExplanation :色の宝玉
           ガラスでできている

​・Item Pivot    :X=0 Y=0.6 Z=0

・Position     :X=86 Y=5.7 Z=123.8
​・Item ID      :7

3_3_60

 ItemDataBase内に今まで作成した8つのアイテムのデータがしっかり入力できているか確認しておきましょう。

 追加した4つの宝玉を拾う&投げる処理が正常に動作するか確認してみてください。

評価テスト

3-4 2つ目のギミック

3-4 2つ目のギミック

 追加した本と宝玉を使うギミックを作成しましょう。

 土台や正解判定の処理は1つ目のギミックで使ったものを流用するため、コード量は少なめです。

​ まずは土台を置くための棚を設置しましょう。この棚は隠し部屋の存在を隠す役割も持っています。

 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Storage」内の「gabinet.001」をシーン上にドラッグ&ドロップして、Transformを調整してください。

【Transformの設定】

Position  : X=89.5 Y=0.2 Z=136.75

Rotation   : X=0 Y=-90 Z=0

Scale    : X=3.04 Y=5.7 Z=4

 追加した棚のLayerを「Ignore Raycast」に変更してください。

 LayerをIgnore Raycastに変更したオブジェクトにはレイ(光線)がヒットしなくなります。アイテムを調べる判定にレイを用いていますが、土台よりも先に棚にレイがヒットしてしまうと、土台を調べることができなくなってしまいます。レイの邪魔になりそうなオブジェクトのレイヤーはIgnore Raycastにしておきましょう。

 棚に4つの台を設置しましょう。​1つ目のギミックで作成した黄色い台をコピーしてください。

 複製した台を棚の子オブジェクトにしてください。設置するアイテムが正解すると棚が移動するため、棚の移動に台がついていくようにしておきます。

 台のTransformやマテリアルを調整しましょう。

​【サンプルの入力例】

Position  : X=0.55 Y=1.58 Z=0.3

Rotation   : X=0 Y=90 Z=0

Scale    : X=0.15 Y=0.17 Z=0.4

​マテリアル(Element0): granite_2_d

 台にアタッチされているItemPlatformコンポーネントのパラメータを調整してください。

​ Answer Item IDは後ほどスクリプトでランダムに変更するのですが、まずはデバッグ用に4(青の宝玉のID)を設定しておきましょう。

【サンプルの入力例】

Name    :石の台

Explanation :石でできた台がある…

       何か置けそうだ

AnswerItemID:4

Add Pos        :X=0 Y=0.2 Z=0

Add Rot        :X=0 Y=180 Z=0

 ItemPlatformの設定が終わったら、石の台にカーソルを合わせてIキーを押すことでアイテムを設置できるか確認しましょう。

 残り3つの石の台も設置します。石の台を複製して、座標とAnswerItemIDを調整するだけです。

​【右上の台】

Position    : X=-0.55 Y=1.58 Z=0.3

AnswerItemID:5

​【左下の台】

Position    : X=0.55 Y=0.84 Z=0.3

AnswerItemID:6

​【右下の台】

Position    : X=-0.55 Y=0.84 Z=0.3

AnswerItemID:7

 4つの土台全てに正解のアイテムが置かれたら、棚が移動するようにしましょう。

​ 棚の移動はスクリプトで行っても良いのですが、復習も兼ねてアニメーションで移動させてみましょう(スクリプトを書くより簡単というのもあります)

​ Animationフォルダを開いて、Animationを作成してください。名前はGabinetWaitにしておきましょう。

 ヒエラルキー内にある棚のオブジェクトに対して、作成したGabinetWaitをドラッグ&ドロップしてください。棚と同じ名前のAnimator Controllerが自動で作成されます。

 GabinetWaitをダブルクリックしてAnimationウィンドウを開いてください。

​ Animationウィンドウを開いた状態で棚を選択すると「Add Property」ボタンをクリックできるようになります。

 「Add Property」を押して「Transform」→「Position」横の+ボタンをクリックしてください。

​ これでアニメーション内に座標の項目が追加されます。

 GabinetWaitは名前の通り待機中のアニメーションなので、初期座標のままで構いません。

 Animationウィンドウ左上のGabinetWaitと表示されている場所をクリックすると「Create New Clip」という項目を確認できます。ここをクリックして、新しいアニメーションを作成しましょう。

 ファイル名の項目にはアニメーション名を入力します。今回は移動のアニメーションなので「GabinetMove」にしておきます。名前を入力できたら保存ボタンをクリックしてください。

 GabinetMoveアニメーションには、4つの台に正解のアイテムが置かれた時に移動するアニメーションを設定します

​ 上から2番目のレーンを右クリックすると「Add Key」という項目が表示されます。キーを追加して、棚が奥へ移動するアニメーションを作ってみましょう。移動させるのはX座標だけです。

【サンプルの入力例】

0フレーム目  : X=89.5

30フレーム目: X=92

60フレーム目: X=111.5

 GabinetMoveのLoop Timeにチェックが入っていたら外しておきましょう。Loop Timeのチェックを外すとアニメーションがループしないようになります。

 Animator Controllerの設定をしましょう。

​ 先ほど自動で作成された棚のAnimator Controllerをダブルクリックして、Animatorウィンドウを開いてください。

 左上のParametersボタンをクリックして、+ボタンからTriggerを選択してください。名前はMoveにしておきましょう(後でスクリプトで参照するので打ち間違いに注意)

 Triggerパラメータは名前の通りトリガーのように一瞬だけ有効になるパラメータになります。一瞬だけtrueになって自動でfalseに戻るbool型をイメージすると分かりやすいかと思います。

 GabinetWaitを右クリックして「Make Transition」を選択してください。GabinetWaitから矢印を伸ばせるようになるため、GabinetWaitからGabinetMoveにTransition(矢印)を繋げましょう。

 作成したTransitionを選択して、以下の措置を行ってください。

・Has Exit Time(アニメーションの終了まで遷移を待つかどうか)のチェックを外す

・Transition Duration(補間率)を0にする

・Conditions(遷移条件)をMoveにする

 →Moveトリガーが有効になったらアニメーションが切り替わるようになる

 後は4つの台に置かれたアイテムが正解なら、Moveトリガーを有効にして棚を移動させるだけです。正解判定は既に作っているので、追加するのは正解時の動作だけになります。

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

~前略~
 

    [SerializeField, Multiline(3)]
    string AnswerExplanation;               // 正解時に説明文を変化させる

 

    // 正解時の反応の種類
    public enum AnswerPattern
    {
        enAnswer_Drop,  // 落下する
        enAnswer_Move,  // 移動する
    }

    [SerializeField]
    AnswerPattern Answer_Pattern;   // 正解時の反応

 

    void Update()
    {

​~後略~

~前略~
 

    void Answer()
    {
        // 正解時の処理
        switch (Answer_Pattern)
        {
            case AnswerPattern.enAnswer_Drop:
                // 物理演算の有効化
                Rigidbody rb = GetComponent<Rigidbody>();
                rb.isKinematic = false;
                // 前方に飛ばす
                rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse);
                // 回転をかける
                rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse);
                // 説明欄の更新
                Explanation = AnswerExplanation;
                break;

            case AnswerPattern.enAnswer_Move:
                // 移動アニメーション
                GetComponent<Animator>().SetTrigger("Move");
                break;

        }

    }

}

 棚に必要なコンポーネントをアタッチしましょう。ItemGimmickとOutlineをアタッチしてください。

 ItemGimmickコンポーネントのパラメータを調整しましょう。

 棚は調べられないオブジェクトにしたいので、Is Checkにチェックを入れて調べられないようにしてください。

 Answer Platformsの項目を4つにして、子オブジェクトである石の台4つをヒエラルキーからドラッグ&ドロップして指定してください。

​ Answer_Patternは正解時の動作を指定するので「En Answer_Move」に設定してください。

 ここまでできたらゲームを実行しましょう。

 左上に青の宝玉、右上に赤の宝玉、左下に黄の宝玉、右下に緑の宝玉を置いてください。4つの石の台に正しく宝玉を置くと棚が移動するか確認してみましょう。

 最後に4つの石の台の答えが複数パターンからランダムに切り替わるようにしましょう。先ほど作成した3冊の本もここで使用します。

 まずは答えの配置を示すヒントを壁に設置しましょう。

​ ヒエラルキーから「3D Object」→「Plane」を選択して、板状のオブジェクトを追加してください。

 名前は分かりやすいものにして、Gimmickの子オブジェクトにしておきましょう。

 壁に合うようにTransformを調整します。

【Transitionの入力例】

Position  : X=123.9 Y=9.6 Z=-63.74

Rotation : X=90 Y=0 Z=0

Scale      : X=1 Y=1 Z=0.6

 次にヒントに貼るためのマテリアルを作成しましょう。答えの配置を示す画像を3種類同梱してあるので、3種類マテリアルを作成します。

​ Materialフォルダ内に新しいマテリアル「Hint1」を作成してください。

 Hint1を選択したら、インスペクター右上の鍵アイコンをクリックしてインスペクターをロックしましょう。ここでインスペクターをロックしておかないと、テクスチャの画像を選択した時にインスペクターがテクスチャのものに切り替わってしまいます。

 Hint1のパラメータを設定しましょう。Base MapにSprite内のHint1のテクスチャを、Normal MapにNormalMapのテクスチャをドラッグ&ドロップしてください。

【サンプルの入力例】

Metallic Map  : 0.8

Smoothness   : 0.1

Normal Map   : 0.8

 設定が終わったらインスペクターのロック解除を忘れないようにしてください。

 

 Hint1のマテリアルを複製して、Base MapをHint2とHint3に差し替えただけのマテリアルを作成してください。

 ギミックを管理するGimmickManagerを作成しましょう。

 GimmickManagerにはゲーム開始時に壁のヒントのマテリアルを変更し、4つの石の台の答えをヒントに合うように設定してもらいます。

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

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

 

public class GimmickManager : MonoBehaviour
{
    // ギミック2
    [SerializeField, Header("ギミック2")]
    GameObject HintObject;      // ヒント用オブジェクト
    [SerializeField]
    Material[] HintMaterial;    // ヒントのマテリアル
    [SerializeField, Tooltip("左上=0 右上=1 左下=2 右下=3")]
    ItemPlatform[] Gimmick2_Platform;   // 石の土台
    int m_gimmick2No = 0;               // パターン番号

 

    // Startより先に実行される関数
    private void Awake()
    {
        // ギミック2の答えの乱数をふる
        m_gimmick2No = Random.Range(0, HintMaterial.Length);
        // ヒントオブジェクトのマテリアルを変更
        HintObject.GetComponent<MeshRenderer>().material =
            HintMaterial[m_gimmick2No];

 

        // 土台に答えを教える(アイテムIDの間違いに注意!)
        switch (m_gimmick2No)
        {
            case 0:
                Gimmick2_Platform[0].SetAnswerID(2);    // 赤い本
                Gimmick2_Platform[1].SetAnswerID(7);    // 緑の宝玉
                Gimmick2_Platform[2].SetAnswerID(4);    // 青の宝玉
                Gimmick2_Platform[3].SetAnswerID(6);    // 黄の宝玉
                break;
            case 1:
                Gimmick2_Platform[0].SetAnswerID(6);    // 黄の宝玉
                Gimmick2_Platform[1].SetAnswerID(5);    // 赤の宝玉
                Gimmick2_Platform[2].SetAnswerID(3);    // 緑の本
                Gimmick2_Platform[3].SetAnswerID(4);    // 青の宝玉
                break;
            case 2:
                Gimmick2_Platform[0].SetAnswerID(5);    // 赤の宝玉
                Gimmick2_Platform[1].SetAnswerID(1);    // 青い本
                Gimmick2_Platform[2].SetAnswerID(4);    // 青の宝玉
                Gimmick2_Platform[3].SetAnswerID(7);    // 緑の宝玉
                break;
        }
    }

 

}

【プログラムの解説】

​・Random.Range関数は第一引数から第二引数の範囲で乱数(ランダムな数)を生成する関数です。

 ただし注意点が1つあり、引数にfloat型を指定した場合は第二引数(最大値)は戻り値の範囲に含まれますが、int型を指定した場合は第二引数は戻り値の範囲に含まれません参考サイト

 int型で乱数を生成したい場合は、戻り値の範囲に注意してください。

・MeshRenderer のパラメータであるMaterialを変更することで、マテリアルを動的に変更できます。

​・ItemPlatform のSetAnswerID関数を使って土台の答えを変更しています。引数には答えとなるアイテムのIDを入れる必要があるのでItemDataBaseのIDと合うようにしましょう。

 GameManagerにGimmickManagerコンポーネントをアタッチしてください。

 HintObjectには先ほど作成したヒント用のPlaneオブジェクトを指定します。

​ HintMaterialは要素数を3にして、上から順番にHint1、Hint2、Hint3を指定してください。

 Gimmick2_Platformは要素数を4にして、左上の土台、右上の土台、左下の土台、右下の土台の順番に指定してください。

 長くなりましたがこれで2つ目のギミックが完成しました。ゲームを実行して確認してみてください​。

 壁のメモを確認して、メモに合うように石の台にアイテムを置いてみましょう。●は宝玉、□は本を表しています。

 壁のヒントは3種類の中からランダムで変化するので、何回かゲームを実行してギミックの変化をチェックしてみましょう。

​ ギミックがうまく動かない場合は、GimmickManagerスクリプト内で指定しているアイテムIDが一致しているかどうか、インスペクターに設定した情報が正しいかどうか確認してみてください。

3-5 3つ目のギミック

3-5 3つ目のギミック

 3つ目のギミックを作成しましょう。

 3つ目のギミックは銀のアイテムを左からしりとりになるように「リンゴ→ゴリラ→ラッパ→パイナップル」と並べることで宝箱が開き、その裏に扉の番号が書いてある、というものです。

 スクリプトを流用するので、コード量は少なめです。ギミックはあと少しなので頑張りましょう!

 まずはカギとなる銀のアイテムを作成しましょう。モデルや設定が違うだけで3-3と流れは同じです。

​ 「Model」→「Object」内のApple(リンゴのモデル)をシーン内にドラッグ&ドロップしてください。Transitionを調整して、タグを「Item」に変更しましょう。

【サンプルでの設定】

Position: X=41.2 Y=7 Z=128.5

Scale    : X=8 Y=8 Z=8

 オブジェクトのレイヤーをDropItemに変更してください。

 リンゴは子オブジェクトを持っているため、その子オブジェクトのレイヤーも変更するか選択するウィンドウが表示されます。「Yes, change children」を選択して、子オブジェクトのレイヤーも変更してください。

 リンゴを銀色にするために、銀色のマテリアルを用意します。

 新しいマテリアルを作成して、名前をSilverにしておきましょう。

 SilverマテリアルはBase Mapを灰色に設定して、Metallic MapとSmoothnessを高くすることで金属のような質感を出してみましょう。

 Silverマテリアルをリンゴにドラッグ&ドロップして貼っておきましょう。

 Unityには様々な形状のコライダー(当たり判定)があります。今回はメッシュの形状に合わせてコライダーを生成するMesh Colliderを使いましょう。

 Add Componentから「Mesh Collider」を追加してください。 

​ Mesh ColliderのConvexにチェックを入れておきましょう。

 Mesh ColliderのConvexにチェックを入れると、当たり判定の形状が簡略化されます

 Convexにチェックが入っていない状態だと、メッシュの形状に合わせて正確な当たり判定が生成されますが、以下のデメリットがあります。

・Mesh Collider同士で衝突することができない(貫通する)

・Rigidbodyを使用できない

​ これらの問題が起きた時はConvexにチェックを入れるようにしましょう。

 銀のリンゴにRigidbody、ItemObject、Outlineをアタッチしてください。

​ 

​ RigidbodyのIs Kinematicにチェックを入れて物理演算を無効にしましょう。

 ItemObjectのItem IDには銀のリンゴを割り当てるIDを指定します(特にオリジナルのアイテムを追加していない場合は8)

 Item Data Baseにはプロジェクト内にあるItemDataBaseを指定してください。

3_3_104

 銀のリンゴをプレハブ化しましょう。プレハブ化を解除していない場合、プレハブの形式を決めるウィンドウが表示されますが「Original Prefab」を選択してください。

 ItemDataBaseの項目を増やして、銀のリンゴの情報を入力してください。先ほどItemObjectに設定したIDと一致する場所に入力するようにしてください。

【サンプルの記入例】

・ItemName    :銀のリンゴ

・​ItemExplanation :銀でできたリンゴ
           食べられない

​・Item Pivot    :X=0 Y=0.54 Z=0

3_3_108

 これで銀のリンゴが完成しました。

 しかし、実際にゲーム画面で銀のリンゴを確認すると、銀とはいえない質感になっています。

 Unityには反射を表現するReflection Probe(リフレクションプローブ)という機能があります。

 公式マニュアルでは「全方向に向かってその周囲の球状のビューをキャプチャするカメラのようなもの」と説明されています。リフレクションプローブを用いることで、周囲の状況を反射してマテリアルに映すことができます。

 

 プレイヤーの子オブジェクトに「Light」→「Reflection Probe」を作成してください。

 Reflection Probeの座標を原点にしてください。

 Typeを「Realtime」に設定しましょう。Refresh Modeを「On Awake」にしておくと、ゲーム開始時に1度だけReflection Probeが初期化されます(「Every Frame」にすることで常に更新できますが、負荷がかなり高いので今回は行いません)

 Box SizeをX=100 Y=20 Z=100 に設定しましょう。ここではReflection Probeの影響を受ける範囲となる箱の大きさを指定しています。

​ Resolutionは映り込みテクスチャの解像度です。高いほど綺麗に映り込みますが、小さいアイテムの映り込みなので解像度は低めにしておきましょう。

 ゲームを実行するとアイテムに風景が映り込み、銀の質感が出ているはずです。

 Reflection Probeをゲーム開始時にのみ更新しているため、よく見ると最初の部屋が映っていますが、リアルタイムで更新するとかなりの負荷がかかるため、あえてこのままにしておきます。

 Reflection Probeをより正確にするためには、プレイヤーではなく銀のリンゴ側にReflection Probeを持たせるべきですが、常時更新すると負荷がかなり高いので今回はプレイヤーの子オブジェクトにしました。小さいアイテムのため、反射の雰囲気が出ていればOKとします。(これらの問題を完璧に解決する場合レイヤーの調整が必要ですが、手間がかかるので解説は割愛します)

 残りの銀のアイテムも作成しましょう。銀のラッパだけはヒントのため、台座に接着した状態で出現させます。そのため、必要なのは銀のゴリラと銀のパイナップルです。

​ 銀のリンゴと同じ手順でアイテムを作成してください。場所はどこでも構いません。

 

① 「Model」→「Object」内からモデルをシーン内にドラッグ&ドロップする

​② タグを「Item」に、レイヤーを「DropItem」にする

​③ 銀色のマテリアルを貼る

④ Mesh Colliderをアタッチし、Convexにチェックを入れる

⑤ Rigidbodyをアタッチし、Is Kinematicにチェックを入れる

⑥ ItemObjectをアタッチし、ItemIDとItemDataBaseを設定する(ID重複に注意)

​⑦ Outlineをアタッチする

3_3_104

⑧ オブジェクトをプレハブ化する

​⑨ ItemDataBaseに情報を追加する

【サンプルの記入例 ゴリラ】

・Item ID                : 9

・ItemName    :銀のゴリラ

・​ItemExplanation :銀でできたゴリラ
           動くことはない

​・Item Pivot    :X=0 Y=0 Z=0

【サンプルの記入例 パイナップル】

・Item ID                : 10

・ItemName    :銀のパイナップル

・​ItemExplanation :銀でできたパイナップル
           かなり重たい

​・Item Pivot    :X=0 Y=0 Z=0

 銀のゴリラのプレハブをダブルクリックして開いて、Y軸周りに90度回転させておいてください。

 銀のゴリラと銀のパイナップルを取得、投げる、台に置く処理がそれぞれ正常に動作するか確認しておきましょう。

 銀の台を設置しましょう。

​ 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Miscellaneous」と選択して、granite_panel をシーン上にドラッグ&ドロップして、Transitionを調整してください。

 

【Transformの設定】

Position  : X=109 Y=6.4 Z=-104.5

Rotation   : X=0 Y=0 Z=0

Scale    : X=1.5 Y=1 Z=1.5

 台に銀のマテリアルを貼りましょう。

 銀の台をコピーして、横に3つ並べてください。

 右から2番目の銀の台にはラッパを固定しておき、調べられるだけにします。

 「Model」→「Object」内のTrumpetをシーン上にドラッグ&ドロップし、右から2番目の台の子オブジェクトにしてください。

【Transformの設定】

Position  : X=0 Y=0.45 Z=0

Rotation   : X=-90 Y=-45 Z=0

Scale    : X=3 Y=3 Z=3

 右から2番目の銀の台にItemObjectとOutlineをアタッチしてください。​ItemObjectコンポーネントには名前と調べたときの説明文を入力しましょう。

【サンプルの入力例】

Name    :銀の台

Explanation :銀の「ラッパ」が置かれた台座だ
        外すことができないようだ…

 これでヒントとなるラッパが置かれた台は完成です。残りの台にはアイテムを置けるようにします。

 Ctrlキーを押しながら残り3つの台を選んで、ItemPlatformとOutlineをアタッチしてください。

 ItemPlatformにアイテム名と説明を入力しましょう。また、AddPosをX=0 Y=0.2 Z=0、AddRotをX=0 Y=180 Z=0 に調整してください。

【サンプルの入力例】

Name    :銀の台

Explanation :銀でできた台がある…
        何か置けそうだ

3_3_125

 AnswerItemIDだけは台ごとに異なるため、左から銀のリンゴ、銀のゴリラ、銀のパイナップルのIDになるように設定しましょう。

 これで銀の台が完成しました。実際にアイテムを置いて、台として動作するか確認してみましょう。

 銀の台に置いたアイテムが正解なら開く宝箱を作成しましょう。

 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Storage」を開いて、chest(宝箱)をシーン上にドラッグ&ドロップしてください。

 宝箱のTransitionを調整して、銀の土台の横に移動させてください。

【Transformの設定】

Position  : X=109 Y=6.25 Z=-119

Rotation   : X=0 Y=-90 Z=0

Scale    : X=3 Y=4 Z=4

 宝箱の子オブジェクトに宝箱の蓋があるため、回転を調整して宝箱を閉じてください。

【Transformの設定】

Rotation   : X=20 Y=0 Z=0

 宝箱が開くアニメーションを作成しましょう。2つ目のギミックで棚を動かすアニメーションを作ったときと流れは同じです。

​ 新しいアニメーションChestCloseを作成して、Chestにドラッグ&ドロップしてください。

 作成したChestCloseアニメーションをダブルクリックして、Animationウィンドウを開いてください。Animationウィンドウを開いた後にヒエラルキー内のChestを選択することで「Add Property」のボタンを押せるようになります。

 アニメーションには子オブジェクトの状態も対象にすることができます。

 「Add Property」をクリックしてから、chest_top→Transform→Rotation横の+ボタンをクリックしてください。

 ChestCloseアニメーションは閉じた状態のため、特に何か動きを設定する必要はありません。

 Animationウィンドウ左上にあるアニメーション名が表示されている部分をクリックして「Create New Clip」を選択してください。

 ファイル名にChestOpenと入力して、保存ボタンをクリックしてください。

 ChestOpenは宝箱が開くアニメーションのため、キーを使用してアニメーションを作成しましょう。

 ChestCloseと同じようにchest_topのRotationの項目を追加してください。

 Rotationの項目を追加したら、宝箱が開くアニメーションを作成してください。

【サンプルの入力例】

0フレーム目  :X=20

40フレーム目:X=-80

60フレーム目:X=-70

 ChestOpenを確認して、LoopTimeにチェックが入っていたら外しておいてください。

 chestにアニメーションをドラッグ&ドロップした時に自動で作成されたAnimator Controllerをダブルクリックしてください。

 Animatorウィンドウを開いたら左上のParametersを選択して、Trigger型のパラメータを作成しましょう。名前はOpenにしておいてください。

 ChestCloseステートを右クリックして「Make Transition」を選択し、ChestCloseからChestOpenに繋がるTransitionを作成してください。

 作成したTransitionの設定を行いましょう。

・Has Exit Timeのチェックを外す

・Transition Durationを0にする

・Conditions下部のプラスボタンを押して、遷移条件をOpenに設定する

 後は2つ目のギミックの棚と同じように、対応した土台全てに置かれたアイテムが正解ならアニメーションを再生するように設定するだけです。

​ ItemGimmickスクリプトを開いて、青い部分のコードを埋めてください。

【ヒント】棚を移動させるenAnswer_Moveの処理を参考にしましょう

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

 

public class ItemGimmick : ItemObject   // 継承する
{
    [SerializeField, Header("正解判定オブジェクト用")]
    ItemPlatform[] AnswerPlatforms;     // 対応する土台

 

    bool m_isAnswer = false;            // 正解済みかどうか
 

    [SerializeField, Multiline(3)]
    string AnswerExplanation;               // 正解時に説明文を変化させる

 

    // 正解時の反応の種類
    public enum AnswerPattern
    {
        enAnswer_Drop,  // 落下する
        enAnswer_Move,  // 移動する
        // ① 開く演出用の列挙型enAnswer_Openを追加する
        (ここに入力)
    }

    [SerializeField]
    AnswerPattern Answer_Pattern;   // 正解時の反応

 

    void Update()
    {


~後略~

~前略~

    void Answer()
    {
        // 正解時の処理
        switch (Answer_Pattern)
        {
            case AnswerPattern.enAnswer_Drop:
                // 物理演算の有効化
                Rigidbody rb = GetComponent<Rigidbody>();
                rb.isKinematic = false;
                // 前方に飛ばす
                rb.AddForce(transform.parent.forward * 10.0f, ForceMode.Impulse);
                // 回転をかける
                rb.AddTorque(transform.parent.right * 10.0f, ForceMode.Impulse);
                // 説明欄の更新
                Explanation = AnswerExplanation;
                break;
            case AnswerPattern.enAnswer_Move:
                // 移動アニメーション
                GetComponent<Animator>().SetTrigger("Move");
                break;
            // ② Answer_Patternが①で追加したものと同じなら
            //   開くアニメーションを再生し、
            //   再度調べられないようにする(SetIsChesk関数を使用)

            (ここに入力)
        }
    }

}

 今回は正解時の反応が3種類しかないため直接処理を書きましたが、汎用性を考えるとItemGimmickを継承した派生クラスを作る方がより賢い作り方といえます。ゲームを拡張してさらにギミックを追加したい場合は覚えておきましょう。

 chestにItemGimmickとOutlineをアタッチしてください。

 ItemGimmickコンポーネントに必要なパラメータを設定しましょう。Answer Platformsの項目を3つにして、ラッパが置かれていない3つの台をドラッグ&ドロップしてください。

【サンプルの入力例】

Name        :宝箱

Explanation     :開かないようだ

Answer_Pattern :enAnswer_Open

 ここまで設定すると銀の台に正しくアイテムを置くことで宝箱が開くようになります。ゲームを実行して確認してみましょう。

 最後にそれぞれの銀のアイテムの位置を調整しましょう。

 サンプルでは銀のリンゴは場所が固定で戸棚の中に隠されており、銀のゴリラと銀のパイナップルは複数の候補のうちランダムな座標に出現するようになっています。

 まずは戸棚を作成しましょう。調べる処理などはItemObjectを継承するため、再び書く必要はありません。ただ「調べられた時に左へ移動する」処理を書くだけで戸棚は完成します。ここまでの復習も兼ねて挑戦してみましょう。

​ Cubeを4つ作成して、それぞれDoor1、Handle1、Door2、Handle2と名前を入力します。Door1の子オブジェクトにHandle1、Door2の子オブジェクトにHandle2を設定してください。

 戸棚の扉になるようにそれぞれのTransitionを調整してください。
 

【Transformの設定​ Door1

Position  : X=42.3 Y=7.58 Z=128.26

Rotation   : X=0 Y=0 Z=0

Scale    : X=0.2 Y=2.4 Z=4.8

【Transformの設定​ Handle1

Position  : X=0.7 Y=0 Z=0.4

Rotation   : X=0 Y=0 Z=0

Scale    : X=1 Y=0.2 Z=0.04

【Transformの設定​ Door2

Position  : X=42.1 Y=7.58 Z=123.6

Rotation   : X=0 Y=0 Z=0

Scale    : X=0.2 Y=2.4 Z=4.8

【Transformの設定​ Handle2

Position  : X=0.7 Y=0 Z=-0.4

Rotation   : X=0 Y=0 Z=0

Scale    : X=1 Y=0.2 Z=0.04

 全てのCubeに対して「wooden-boards-texture-d」のマテリアルを貼ってください。Ctrlキーでまとめて選択してからマテリアルを変更することで、4つのCubeに一気にマテリアルを適用できます。

 棚や宝箱と同じように、扉が移動するアニメーションを作成しましょう。アニメーションを作成するのは、向かって右側の扉(Door1)だけで構いません。

​ 棚や宝箱のアニメーション作成を参考にしながら、Door1をZ方向に移動させるアニメーションを作成してください。

 Animationウィンドウ左上のアニメーション名をクリックして「Create New Clip」から新しいアニメーションDoorOpenを作成してください。

​ DoorOpenアニメーションにPositionのパラメータを追加して、Z方向に-4移動するアニメーションを作成しましょう。

【Animationの設定】

0フレーム目  : Z=128.26

60フレーム目: Z=124.26

3_3_157

 Animator ControllerをダブルクリックしてAnimatorウィンドウを開いてください。

​ Trigger型のパラメータOpenを作成して、Openが有効になるとステートが切り替わるようにしましょう。

 DoorOpenアニメーションを確認して、LoopTimeにチェックが入っていたら外しておきましょう。

 ItemDoorスクリプトを作成して以下のように入力してください。青い部分は穴埋めになります。継承を使うことで、必要な処理は「自分が調べられた時の処理」だけになります。

​【ヒント】①と②はItemPlatformスクリプトを参考にしましょう!

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

 

public class ItemDoor (ここに入力) // ① ItemObjectを継承
{
    // ② ItemObjectクラスのItemCheck関数をオーバーライドする
    (ここに入力)
    {
        // ③ ドアを開けるアニメーションを再生
        (ここに入力)

 

        // ④ 再度調べられないようにする(SetIsCheck関数)
        (ここに入力)
    }
}

 PlayerItemスクリプトではレイがヒットしたオブジェクトのItemObjectコンポーネントを取得して、ItemCheck関数を呼び出しています。レイがヒットしたオブジェクトがItemDoorコンポーネントをアタッチしていた場合、ItemDoorコンポーネントはItemObjectコンポーネントとして扱われます。

 ItemObjectを継承したItemDoorクラスがItemObjectとして振舞っているイメージです。

 ItemDoorクラスはItemObjectクラスのItemCheck関数の中身を上書きして、オーバーライドしたItemCheck関数の処理に置き換えています。上書きされる側の関数にはvirtualをつけて、仮想関数にそておきます。

 これによってItemObjectをアタッチしたオブジェクトは調べた時に説明文を表示しますが、ItemDoorをアタッチしたオブジェクトは調べた時に説明文は表示せずに、ドアを開くアニメーションを再生することになります。

 継承については完全に理解する必要はありません。ただ「基底クラスの仮想関数の内容は派生クラスから上書きすることができる」という点は知っておきましょう。

​ 後はDoor1にItemDoorとOutlineをアタッチするだけになります。ItemDoorコンポーネントのNameだけは入力しておいてください。

3_3_a

 これで戸棚を開けることができるようになりました。開けた後は再度調べられなくなります。

​ ゲームを実行して確認してみましょう。

 最後は銀のゴリラと銀のパイナップルの出現位置を複数候補からランダムにしましょう。

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

~前略~

    [SerializeField, Tooltip("左上=0 右上=1 左下=2 右下=3")]
    ItemPlatform[] Gimmick2_Platform;
    int Gimmick2No = 0;

 

    // ギミック3
    [SerializeField]
    GameObject GorillaObj, PineappleObj;
    [SerializeField]
    Vector3[] GorillaPos, PineapplePos; // 出現位置(候補からランダム)

 

    void Awake()
    {

​~後略~

~前略~
 

                break;
        }

 

        // ギミック3 ゴリラの位置をランダムにする
        GorillaObj.transform.position = 
            GorillaPos[Random.Range(0, GorillaPos.Length)];
        // ギミック3 パイナップルの位置をランダムにする
        PineappleObj.transform.position = 
            PineapplePos[Random.Range(0, PineapplePos.Length)];

 

    }
 

}

 GameManagerオブジェクト​のインスペクターにパラメータが追加されているので、対象となるアイテムとランダムな出現先である座標を設定してください。​アイテムの出現先の座標はあくまでサンプルの設定例で、自由に項目を増やしたり座標を変更しても構いません。

【GimmickManagerコンポーネントの入力例】

GorillaObj       : シーン内にある銀のゴリラをドラッグ&ドロップ

PineappleObj  : シーン内にある銀のパイナップルをドラッグ&ドロップ

GorillaPos Element0      : X=136 Y=3.32 Z=76

​GorillaPos Element1      : X=-12.5 Y=4.63 Z=22

​GorillaPos Element2      : X=51.5 Y=8 Z=66

PineapplePos Element0 : X=36 Y=8.1 Z=-20

PineapplePos Element1 : X=131.5 Y=4.63 Z=-61.5

PineapplePos Element2 : X=49 Y=5.1 Z=-114

 設定ができたらゲームを実行して、銀のアイテムが候補のうちどれかの座標に出現することを確認してみましょう。

 これで扉のパスワードを隠す3つのギミックが完成しました。

​【評価テスト】

https://forms.gle/LDWV4BiK7X2L2TKG7

3-6 扉のパスワードを生成

3-6 扉のパスワードを生成
評価テスト

 いよいよ最後のギミック、脱出用の扉を作成します。

 その前にまずはランダムな6桁のパスワードを生成して、今まで作成したギミックを解除することで確認できるようにしましょう。

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;    // TextMeshProを扱うために必要

 

public class GimmickManager : MonoBehaviour
{
    // 扉の暗証番号
    [SerializeField]
    TextMeshPro[] NumberText = new TextMeshPro[3];  
// 番号表示用
    [SerializeField]
    int[] DoorAnswer = new int[6];              
   // 6桁の答え
    public int GetAnswer(int num)
    {
        return DoorAnswer[num];
    }

 

    // ギミック2
    [SerializeField, Header("ギミック2")]
    GameObject HintObject;      // ヒント用オブジェクト
    [SerializeField]
    Material[] HintMaterial;    // ヒントのマテリアル
    [SerializeField, Tooltip("左上=0 右上=1 左下=2 右下=3")]
    ItemPlatform[] Gimmick2_Platform;   // 石の土台
    int m_gimmick2No = 0;               // パターン番号

 

    // ギミック3
    [SerializeField, Header("ギミック3")]
    GameObject GorillaObj;
    [SerializeField]
    GameObject PineappleObj;
    [SerializeField]
    Vector3[] GorillaPos, PineapplePos;     // 出現位置(候補からランダム)

 

    // Startより先に実行される関数
    private void Awake()
    {

        // 答えをランダム生成
        for (int i = 0; i < DoorAnswer.Length; i++)
        {
            // int型のRandom.Rangeの挙動に注意!
            DoorAnswer[i] = Random.Range(0, 10);
        }
        // 生成した答えを表示
        int num = 0;
        for (int i = 0; i < NumberText.Length; i++)
        {
            // 2桁ずつ表示する
            NumberText[i].text = "" + DoorAnswer[num] + DoorAnswer[num + 1];
            num += 2;
        }

 

        // ギミック2の答えの乱数をふる
        m_gimmick2No = Random.Range(0, HintMaterial.Length);

​~後略~

【プログラムの解説】

​・3Dアクションゲーム編ではTextMeshProUGUIを使用していましたが、今回使用するのは3DオブジェクトのTextMeshProなのでクラス名はTextMeshProになっています。

・int[] DoorAnswer = new int[6]; のように記述することで、配列の要素数を指定することができます。ここで指定した要素数はあくまで初期値であり、インスペクターから変更できます。

 また、int[] Hoge = { 2, 3, 5, 7, 11 }; のように、初期値を指定することもできます。

・3-4で解説した通り、Random.Range関数は第一引数から第二引数の範囲のうちランダムな値を返す関数です​。int型を引数に指定した場合、戻り値の範囲に第二引数は含まれません。そのため第一引数を0、第二引数を10にした場合0~9の範囲のランダムな値が返されることになります。

 後はパスワード表示用のTextMeshProオブジェクトを追加するだけです。手順が少し複雑なので、注意して進めましょう。


 TextMeshProフォルダ内の「Corporate-Mincho-ver3 SDF Material」を選択してください。
 インスペクター右上の3つの点をクリックして、「Create Material Preset」を選択してください。

 同フォルダ内にマテリアルが生成されるので、名前を「Corporate-Mincho-ver3 SDF - Number」にしてください。名前が間違っていると後で正常に進まないので注意してください(コピペ推奨です)

 作成したマテリアルのシェーダーをTextMeshPro→SRP→TMP_SDF_URP Litに変更してください。

 デフォルトのシェーダーでは3D空間に設置したTextMeshProはライトの影響を受けません。標準のままでは後ほどライトを消して世界を暗くしたときにTextMeshProだけが暗闇に浮かび上がるようになってしまいます。

 この問題を解決するため、教材ではこちらのフォーラムで公式によって配布されていたシェーダーを改変したものを同梱しています。

Unity Tips!

 インスペクターの下部に_MainTexという項目があるので、そこに同梱したフォントのテクスチャをドラッグ&ドロップしてください。

 ヒエラルキーから「3D Object」→「Text - TextMeshPro」を追加してください。

 オブジェクト名をNumber1にして、絵画の後ろに隠れるように座標や回転を調整しましょう。

【サンプルの入力例】

PosX=-17.75   PosY=9.3   PosZ=-10

Width=5   Height=5

Rotation X=0 Y=-90 Z=20

 TextMeshProのパラメータを調整してください。項目が多いので注意しましょう。

​・TextInputに「00」と入力

​→仮のテキストを入力しておきます。

・FontAssetを「Corporate-Mincho-ver3 SDF」にする

→右端の丸いボタンをクリックすると楽に選択できます。

・MaterialPresetを「Corporate-Mincho-ver3 SDF -

Number」にする

→FontAssetを変更してからでないと表示されないので

注意しましょう。

・FontStyleの太字と斜体ボタンを押す

→文字の見た目が変化します。

​・FontSizeを30にする

・Vertex Colorを赤にする

→少し暗めの赤の方がステージに馴染みます。

・Alignmentを中央に設定​

 ここまで設定できたら、絵画の裏に赤い数字が表示されているはずです。

 残りの番号も作成しましょう。色や座標が違うだけで基本設定は同じです。

​ Number1をコピーしてNumber2を作成して、座標や回転を調整してください。

【サンプルの入力例】

PosX=111.8   PosY=9.8   PosZ=148

Width=8   Height=8

Rotation X=0 Y=90 Z=10

 FontSizeを48にして、文字の色を青色に変更してください。

 これで隠し部屋の壁に青い番号が表示されます。

 Number1を複製してNumber3を作成してください。

 Number3は宝箱の蓋の裏に隠すため、Number3をchestの子オブジェクトであるchest_topの子オブジェクトにしてください。蓋の子オブジェクトにすることで、蓋が開くアニメーションをしても問題なく追従するようになります。

 Number3の座標や回転を設定しましょう。親オブジェクトであるchest_topを基準としたローカル座標になっている点に注意しましょう。

【サンプルの入力例】

PosX=0   PosY=0.157   PosZ=0.365

Width=5   Height=5

Rotation X=-70 Y=180 Z=0

Scale X=0.3 Y=0.25 Z=1

 FontSizeを24にして、文字の色を緑色に変更してください。

 これで宝箱の裏に緑の番号を隠すことができました。

 GimmickManagerのNumberTextに作成したNumber1、Number2、Number3を設定してください。Element0から順番に番号を設定していくので、順番を間違えないようにしてください。

 ここまで設定できたら番号の処理は完成です。

​ ゲームを実行するとDoorAnswerにランダムな値が格納されます。

 今まで作成した3つの謎を解いて、扉のパスワードを見つけることができるか確認してみましょう。​

3-7 出口の扉を作成

3-7 出口の扉を作成

 長くなりましたが最後のギミックとして、6桁のパスワードを入力することで開く扉を作成しましょう。

 

​ まずは空オブジェクトを追加して名前をDoorにしておきましょう。

 座標は X=164.6 Y=-1.56 Z=11.56 へ移動させてください。

 ドア本体を作成しましょう。Doorの子オブジェクトにCubeを追加してください。名前はBoardにしておきましょう。

 Boardの座標やマテリアルを調整してください。

​【サンプルの入力例】

Position                      : X=0.23 Y=6.94 Z=-2.96

Scale                          : X=0.5 Y=13.9 Z=6.1

マテリアル(Element0): timber_1_fixed_d

 Boardの子オブジェクトにCubeを作成して、名前はFrameにしておきましょう。

 Frameの座標やマテリアルを調整してください。
 

​【サンプルの入力例】

Position                      : X=-0.4 Y=0.45 Z=0

Scale                          : X=0.8 Y=-0.03 Z=0.8

マテリアル(Element0): wood_4_d

 Frameを複製して、四角の枠になるようにしましょう。

 

​【サンプルの入力例 下側】

Position                      : X=-0.4 Y=0.32 Z=0

Scale                          : X=0.8 Y=0.03 Z=0.8

​【サンプルの入力例 左側】

Position                      : X=-0.4 Y=0.385 Z=0.35

Scale                          : X=0.8 Y=0.1 Z=0.05

【サンプルの入力例 右側】

Position                      : X=-0.4 Y=0.385 Z=-0.35

Scale                          : X=0.8 Y=-0.1 Z=0.05

 ディスプレイの中身は一旦後回しにして、先に入力用のボタンを作成しましょう。

 Boardの子オブジェクトにCubeを追加して、名前をButton_0にしてください。​Transformとマテリアルを設定しましょう。
 

​【サンプルの入力例】

Position                      : X=-0.75 Y=-0.2 Z=0

Scale                          : X=0.5 Y=-0.08 Z=0.2

マテリアル(Element0): wooden-boards-texture_white_d

 Button_0の子オブジェクトに3DオブジェクトのTextMeshProを追加して、名前をNumberにしておきましょう。

​ NumberのPosXを-0.55にしてください。

 TextMeshProの設定を行いましょう。

【TextMeshProの設定】

Text Input       : 0

Font Asset      : Corporate-Mincho-ver3 SDF

Material Preset: Corporate-Mincho-ver3 SDF - Number(Font Assetの設定をしてから選ぶ)

Font Size        : 10

Vertex Color    : 黒に設定

Alignment       : 中央

 Button_0を複製して1~9のボタンを配置してください。それぞれ座標を調整して、TextMeshProのText Inputに値を入力しましょう。

 それでは先に「パスワードを入力して、正解なら扉が開く」処理を実装しましょう。その後にモニターに入力中の値を表示します。

 まずは入力中の値を保持して正解なら開くアニメーションを再生するスクリプトを作成しましょう。

 FinalDoorスクリプトを作成して、以下のように入力してください。少々長いですがfor文や配列への理解があれば問題ありません。

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

 

public class FinalDoor : MonoBehaviour
{
    // 現在の番号
    int m_nowNumber = 0;
    // 正解済みならtrue
    bool m_isNumberOK = false;
    public bool GetIsNumberOK()
    {
        return m_isNumberOK;
    }

 

    // 答え取得用
    [SerializeField]
    GimmickManager Gimmick_Manager;

 

    // 入力番号
    int[] Number = new int[6];

 

    void Awake()
    {
        // 最初にリセット
        NumberReset();
    }

 

    // 引数の番号を入力
    public void SetNumber(int num)
    {
        // 正解済みなら実行しない
        if (m_isNumberOK)
        {
            return;
        }

 

        // 番号入力
        Number[m_nowNumber] = num;
        m_nowNumber++;

 

        // 入力が終わっていないなら終了
        if (m_nowNumber < Number.Length)
        {
            return;
        }

 

        // ここから下は番号を全て入力した時の処理
        bool answer = true;

        // 正解かどうかチェック
        for (int i = 0; i < Number.Length; i++)
        {
            // 1つでも違ったら不正解
            if (Gimmick_Manager.GetAnswer(i) != Number[i])
            {
                answer = false;
            }
        }

 

        // 判定
        if (answer)
        {
            // 正解
            m_isNumberOK = true;
            // 親オブジェクトのアニメーション開始
            transform.parent.GetComponent<Animator>().SetTrigger("Open");
        }
        else
        {
            // 不正解
            NumberReset();
        }

    }
 

    // 入力状態をリセット
    void NumberReset()
    {
        for (int i = 0; i < Number.Length; i++)
        {
            m_nowNumber = 0;
            Number[i] = -1;
        }
    }

 

}

【プログラムの解説】

・transform.parent で自身の親オブジェクトを取得することができます。ここでは自身の親オブジェクトにアタッチされているAnimatorを取得して、アニメーションの再生を開始しています。

 コードが書けたら保存して、Boardにアタッチしておいてください。

​ インスペクターにGimmickManagerを指定する項目があるので、既に作成してあるGimmickManagerを選択しましょう。

 扉が開くアニメーションを作成しましょう。

​ Animationフォルダ内にFinalDoorCloseアニメーションを作成して、Doorオブジェクトにドラッグ&ドロップしてください。

 FinalDoorCloseアニメーションをダブルクリックしてアニメーションウィンドウを開いてください。アニメーションウィンドウを開いた状態でインスペクター内のDoorオブジェクトをクリックし「Add Property」からTransform→Rotationの項目を追加しましょう。

 FinalDoorCloseアニメーションは特に何もしなくて構いません。

​ 左上のアニメーション名が表示されている場所から「Create New Clip」を選択して新しいアニメーションFinalDoorOpenを作成してください。

 同じようにRotationの項目を追加して、ドアが開くアニメーションを作成してください。サンプルでは1秒かけてRotation.yを-120へ変化させています。

​ クオリティを上げたい人は途中にキーを追加して、最初はゆっくり開くようにするとよいでしょう。

 入力側のボタン処理を作成します。入力側は先ほど作成したSetNumber関数を呼ぶだけになります。

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

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

 

public class FinalButton : ItemObject   // 継承する
{
    [SerializeField, Header("扉")]
    FinalDoor finalDoor;

 

    [SerializeField, Header("押すと出力される番号")]
    int Num = 0;

 

    // 正解しているかどうか
    bool m_isEnd = false;

 

    // 自分が調べられた時の関数をオーバーライド(上書き)する
    public override void ItemCheck()
    {
        // ボタンを押すアニメーションを再生
        GetComponent<Animator>().SetTrigger("Push");

 

        // 番号の入力
        finalDoor.SetNumber(Num);
    }

 

    void Update()
    {
        // 正解済みならAnimatorを止める
        if (finalDoor.GetIsNumberOK() && m_isEnd == false)
        {
            GetComponent<Animator>().enabled = false;
            m_isEnd = true;
        }
    }

 

}

 ボタンを押すアニメーションを作成しましょう。流れは今までと変わらないため、細かい説明は省略します。

​ Animationフォルダ内に新しいアニメーションFinalButtonWaitを作成して、Button_0にドラッグ&ドロップしてください。

 Unityのアニメーションには絶対座標と相対座標があります(2Dランゲーム編2-4参照

​ ボタンを押すアニメーションはその場でアニメーションしたいため、相対移動に設定する必要があります。Button_0のAnimatorを選択して、Apply Root Motionにチェックを入れてください。

 FinalButtonWaitには特に操作は必要ありません。新しいアニメーションFinalButtonPushを作成して、Add PropertyからPositionの項目を追加してください。

 相対移動に設定しているため、最初と最後のフレームは全ての項目を0に設定しておきましょう。

 アニメーションの途中にキーを追加して、Xを+0.1するようにしてください。これで全てのボタンに流用できるアニメーションが完成します。

 Animator Controllerを開いて、Trigger型のパラメータを追加しましょう。名前はPushにしておきます。

​ FinalButtonWaitとFinalButtonPushを相互に繋ぐTransitionを作成してください。

 追加した2つのTransitionの設定を変更しましょう。

【FinalButtonWait->FinalButtonPush】

・Has Exit Timeのチェックを外す

・Transition Duration を0にする

​・Conditions右下のプラスボタンを押して、遷移条件をPushに設定

【FinalButtonPush->FinalButtonWait】

・Has Exit Timeにチェックを入れる(アニメーション終了を待つ)

・Exit TimeとTransition Duration を0にする

​・Conditionsには何も設定しない

​ →アニメーションが終わると自動で遷移するようになる

 最後に各ボタンに必要なコンポーネントをアタッチしていきましょう。

 Button_0を選択してFinalButtonとOutlineをアタッチしてください。

 Nameにはカーソルが合っている時に表示される名前を指定します(サンプルでは「0のボタン」)

 FinalDoorには先ほどBoardにアタッチしたFinalDoorコンポーネントを指定します。

​ Numはそのボタンが押されたときに出力する値を指定するので、0にしておいてください。

3_3_151

 他のButton_1~Button_9までをまとめて選択して、Button_0のAnimator Controllerをドラッグ&ドロップしましょう。Apply Root Motionにチェックを入れておきます。

 FinalButtonとOutlineもアタッチして、NameとFinalDoorの設定をしておきましょう。

​ 後は各ボタンに対応したNumを設定していくだけです(Button_1ならNumは1、Button_5ならNumは5)

3_3_153

 これで最後の扉を開く処理が完成しました。ゲームを実行して確認してみましょう。

​ GimmickManager内に表示されている答えを見るか、実際に3つの謎を解いて答えを確認して、最後の扉に答えの番号を入力してみてください。扉が開いたらOKです。

3-8 シェーダーグラフ

3-8 シェーダーグラフ

 後は入力中の番号をモニターへ出力するだけです。

​ 普通に表示してもよいのですが、今回はシェーダーグラフという機能を使って少しだけリッチな表現にしてみましょう。シェーダーグラフはノードを繋げることで視覚的にシェーダーを作ることができる機能です。

​ この機能はURPで実装されたもので、3Dコアでは扱うことができません。シェーダーグラフを使えるのもURPのメリットです。

 HLSLではわかりにくかった計算中の流れを視覚的に確認できるため、シェーダーへの理解も深まります。DirectXでシェーダーが苦手な人もまず挑戦してみましょう。

​ 今回は白い線が移動することでモニターのように見えるシェーダーを作ります。

3_3_158

 Shaderフォルダを作成して「Create」→「Shader Graph」→「URP」→「Lit Shader Graph」を追加してください。

​※ シェーダーに詳しい人はちょっと違和感を覚えるかもしれませんが、後で直します。

 シェーダーグラフが作成されるので、好きな名前をつけたらダブルクリックで開いてください。

 シェーダーグラフの編集画面が開きます。見やすいように位置とサイズを調整したら早速シェーダーを作っていきましょう。

 編集画面の適当なスペースで右クリックして「Create Node」を選択してください。

 検索欄に「Position」と入力して、Positionノードを選択してください。

 編集画面にPositionノードが追加されます。これは描画する場所のワールド座標を返すノードです。

 同じようにSplitノードを検索、追加してください。

​ Splitノードはベクトルの要素を分割するノードです。例えばVector3型のpositionというデータがあったとして、そこからyの要素だけを使いたい場合などに使います。

 PositionノードのOutからSplitノードのInまで、マウスをドラッグして線を繋げてください。

 Timeノードを作成してください。

 Timeノードはゲームを開始してから経過した時間などを返すノードです。Timeは実行中常に増え続ける値なので、段階的にパラメータを変化させたい時などによく使われます。

 Multiplyノードを作成して、TimeノードのTimeとMultiplyノードのAを繋げてください。

​ MultiplyノードはAとBの乗算を行うノードです。

 MultiplyノードのBにあたる値は白いラインがスクロールする速さに該当しますが、このままでは値が2で固定されてしまい調整が面倒です。スクリプトと同じように、後から調整したい値は変数を作って置き換えましょう

 左上のシェーダー名が書いてある場所にある+ボタンをクリックすると、追加する変数の型を選択できます。Float型を選択してください。

 追加したFloat型のパラメータの名前をSpeedにしておいてください。

​ Speedを直接ドラッグ&ドロップすることでノードを追加できます。

 Speedノードを選択すると右側のNode Settingsで様々な設定をすることができます。Default(初期値)を4に変更しましょう。

​ 設定できたら、SpeedノードをMultiplyノードのBに繋いでください。

 Addノードを追加してください。AddノードはAとBの加算を行うノードです。

 ノードを追加できたら、SplitノードのG(ワールド座標のYの要素)とMultiplyノードのOutを繋げましょう。

 Multiplyノードを追加して、AddノードのOutをAに繋げてください。

 追加したMultiplyノードのBの値は線の数(線の細かさ)を示す値なので、こちらもパラメータとして調整できるようにしましょう。

​ Speedの値を追加した時と同じように、Float型のパラメータLineを追加してください。

 Lineノードを追加してMultiplyノードのBへ繋いでください。

 Lineノードを選択して、Node Settingsを変更しましょう。今回はSliderを表示します。

​ ModeをDefaultからSliderへ変更してください。

 これでスライダーを使って値を調整できるようになりました(マテリアルに適応した際に確認できます)

 Defaultを4、Minを0、Maxを10にしてください。

 MultiplyノードのOutとFractionノードのInを繋げてください。

 Fractionノードは値の小数点以下の部分だけを抽出するノードです。これによって液晶画面のノイズにあたる部分が完成します。

 今のままではラインが目立ちすぎるので調整しましょう。

 Colorノードを作成して、色を灰色に設定してください。

 Multiplyノードを作成して、FractionノードのOutとColorノードのOutを繋げてください。

 Color型のパラメータColorを作成してください。これがベースの色になります。

 作成したパラメータColorとMultiplyノードの結果をAddノードを使って加算してください。

​ ColorノードのDefaultを赤色に設定すると、今までのラインと赤色が合成されたマテリアルが出力されます。

 AddノードのOutをFragmentのBase Colorへ繋いでください。これで計算結果が出力されます。

 画像のようにノードを繋いだら、左上の「Save Asset」ボタンをクリックして保存してください。シェーダーグラフを変更した場合は保存しないと反映されないので注意しましょう。

 保存したらシェーダーグラフ編集画面を閉じてください。

​ さっそく作成したシェーダーを使ってみましょう。マテリアルを作成して、名前をRedDisplayにしておきます。

 作成したマテリアルにDisplayShaderをドラッグ&ドロップしてください。これでマテリアルにシェーダーが適用されます。

 マテリアルを選択すると、シェーダーグラフで設定したSpeedやLineなどのパラメータが表示されています。SpeedとLineはお好みで調整して、Colorは赤に設定してください。​Speedを負の数にするとラインの方向が逆になります。

 Boardの子オブジェクトにBoxを作成して、Transformを調整してください。サンプルでは名前をDisplayにしています。

 先ほど作成したRedDisplayマテリアルをBoxにドラッグ&ドロップしましょう。
 

​【サンプルの入力例(Boardの子オブジェクトにしてから調整する)】

 Position X=-0.5 Y=0.385 Z=0.22

 Rotation X=0 Y=0 Z=0

​ Scale X=0.1 Y=0.1 Z=0.22

3_3_185
3_3_186

 これでモニターは完成…としたいところですが、ディレクションライトを消すと微妙な感じになってしまいます。

 実は最初に作成した「Lit Shader Graph」はUnityによってライティングが適用されるシェーダーです。そのため、ライトを消すとそれに応じて暗くなってしまいます。

 シェーダーグラフ編集画面を開いて、Graph Settings のMaterialを「Lit」から「Unlit」へ変更してください。これでライティングが適用されない設定になります。

 基本的にはマテリアルにライティングを適用したいためLitにしておきますが、今回のようにディスプレイなどマテリアルにライティングを反映したくない場合にはUnlitに変更してください。

 設定を変更すると、ディレクションライトが消えても影響を受けないようになります。これでモニターの土台が完成しました。

​ 確認できたらディレクションライトは戻しておいてください。

 入力内容を表示するためのTextMeshProを追加しましょう。

 Displayの子オブジェクトに「3D Object」→「Text - TextMeshPro」を追加しましょう。UIのTextMeshProではないので注意してください。

 座標と回転を調整してください。
 

​【サンプルの入力例(Displayの子オブジェクトにしてから調整する)】

 Position X=-0.52 Y=0 Z=0

 Rotation X=0 Y=90 Z=0

 TextMeshProコンポーネントを設定していきます。
 

【TextMeshProの設定】

 Text Input       : __(アンダーバー2つ)

 Font Asset      : Corporate-Mincho-ver3 SDF

 Font Size        : 8

 Vertex Color    : 白

 Alignment       : 中央

 RedDisplayマテリアルを複製して、BlueDisplayマテリアルとGreenDisplayマテリアルを作成しましょう。設定の違いは色だけです。

 赤いモニターを複製して青と緑のモニターを作成しましょう。

 変更するのは座標、名前、マテリアルのみです。TextMeshProの変更は必要ありません。

​【BlueDisplay(青いディスプレイ)】

 Position X=-0.5 Y=0.385 Z=0

​【GreenDisplay(緑のディスプレイ)】

 Position X=-0.5 Y=0.385 Z=-0.22

 これでオブジェクトの設定は完了です。

​ 

 最後に入力内容を表示するスクリプトを書きましょう。

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

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

 

public class FinalDoor : MonoBehaviour
{
    // 現在の番号
    int m_nowNumber = 0;
    // 正解済みならtrue
    bool m_isNumberOK = false;
    public bool GetIsNumberOK()
    {
        return m_isNumberOK;
    }

 

    // 番号表示用
    [SerializeField]
    TextMeshPro[] NumberObject;

 

    // 答え取得用
    [SerializeField]
    GimmickManager Gimmick_Manager;

 

    // 入力番号
    int[] Number = new int[6];

    void Awake()
    {
        // 最初にリセット
        NumberReset();
        NumberUpdate();
    }

 

    // 引数の番号を入力
    public void SetNumber(int num)
    {
        // 正解済みなら実行しない
        if (m_isNumberOK)
        {
            return;
        }

 

        // 番号入力
        Number[m_nowNumber] = num;
        m_nowNumber++;

 

        NumberUpdate();
 

        // 入力が終わっていないなら終了
        if (m_nowNumber < Number.Length)
        {
            return;
        }

 

        // ここから下は番号を全て入力した時の処理
        bool answer = true;

        // 正解かどうかチェック
        for (int i = 0; i < Number.Length; i++)
        {
            // 1つでも違ったら不正解
            if (Gimmick_Manager.GetAnswer(i) != Number[i])
            {
                answer = false;
            }
        }

 

        // 判定
        if (answer)
        {
            // 正解
            m_isNumberOK = true;
            // 親オブジェクトのアニメーション開始
            transform.parent.GetComponent<Animator>().SetTrigger("Open");
            // 正解なので〇を表示
            SetNumberText("〇〇〇");
        }
        else
        {
            // 不正解
            NumberReset();
            // ×を表示して1秒後に戻す
            SetNumberText("×××");
            Invoke("NumberUpdate", 1.0f);

        }

    }
 

    // 入力状態をリセット
    void NumberReset()
    {
        for (int i = 0; i < Number.Length; i++)
        {
            m_nowNumber = 0;
            Number[i] = -1;
        }
    }

 

    // 表示更新
    void NumberUpdate()
    {
        int num = 0;

        for (int i = 0; i < NumberObject.Length; i++)
        {
       
    // 1番目
            if (Number[num] == -1)
            {
                NumberObject[i].text = "_";
            }
            else
            {
                NumberObject[i].text = "" + Number[num];
            }
            num++;
         
  // 2番目
            if (Number[num] == -1)
            {
                NumberObject[i].text += "_";
            }
            else
            {
                NumberObject[i].text += "" + Number[num];
            }
            num++;
        }
    }

 

    // 文章を表示
    void SetNumberText(string text)
    {
        int len = 0;
        for (int i = 0; i < NumberObject.Length; i++)
        {
           
// 指定文字目から1文字抜き出す
            NumberObject[i].text = text.Substring(len, 1);
            len++;
        }
    }

 

}

【プログラムの解説】

​・StringクラスのSubstring関数は「x文字目からy文字抜き出す」ことができる関数です。

 第一引数に開始地点、第二引数に抜き出す文字数を指定します(第二引数を指定しなかった場合、末尾まで抜き出します)

​ 今回の処理では入力している数値を文字列に変換した後、順番に1文字ずつ抜き出すことでモニターに入力値を表示しています。

​ 入力できたら保存して、FinalDoorコンポーネントのNumber Objectに赤、青、緑の順番でTextMeshProを設定してください。順番を間違えると表示がずれるので注意しましょう。

​ これで最後の扉に入力した番号が表示されるようになりました。

​ 入力に失敗すると×が、成功すると〇が表示されることも確認してください。

3_3_199
3_3_200

​ 今回はシェーダーグラフを使うことで少し高度な演出を実装しました。

 インターネット上に様々なシェーダーグラフのレシピがありますので、まずはレシピの真似をしてノードの使い方を覚えていきましょう。慣れてきたら自分だけのシェーダーを作ってみてください。

 DirectXでゲームを作っている学生も実装するシェーダーの参考にしてみてください。
【おすすめサイト(一部有料ですが無料ページだけでも参考になります】

https://zenn.dev/r_ngtm/books/shadergraph-cookbook/viewer/lookup-of-recipe#%F0%9F%8D%8E-oneminus%E3%83%8E%E3%83%BC%E3%83%89

 

 長くなりましたが、これでゲームの核となるギミックが完成しました。

​ 次のレッスンでは所持しているアイテムやアイテムを調べた時の文章を表示するためのUIを作成していきましょう。

【評価テスト】

​https://forms.gle/Sh1WPVkNjXS76gYAA

評価テスト

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

bottom of page