top of page

3D脱出ゲーム編

Lesson5 クオリティを上げよう

5-1 シーンの作成

5-1 シーンの作成

 今まで作ってきたゲームをゲームとして完成させていきます。

 まずはタイトルとクリアシーンを実装しましょう。

​ Scenesフォルダ内にTitleシーンを作成してください。

 Titleシーンに切り替えたら、オブジェクトを配置していきましょう。

 今回は3Dモデルの家を見下ろして、カメラを回転させることでタイトルシーンの演出にします。

​ メインカメラのTransformを設定して、下向きに調整しましょう。

【サンプルの入力例】

Position  : X=0 Y=45 Z=40

Rotation : X=45 Y=180 Z=0

 次に地面を追加します。

​ 「3D Object」→「Plane」を追加して、以下のようにパラメータを設定してください。

【サンプルの入力例】

Scale : X=80 Y=1 Z=80

Materials Element0 : charcoal_1_d2

​Mesh Collider : (使わないので外してOK)

 Planeを追加した際に自動で当たり判定であるMesh Colliderがついてきますが、タイトルでは物理演算を使わないためコンポーネントを外して構いません。

 次に家を配置しましょう。

 「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Houses」を開いて、「house.003_」をシーン上にドラッグ&ドロップしてください。

 配置した家の座標を調整しましょう。​Xを5、Zを-5に設定してください。

 これで地面と家の配置が完了しました。上にロゴを置くスペースを確保するために、家を画面下寄りに配置しています。

 次にディレクションライトを調整しましょう。

 Directional Lightを選択して、角度や明るさを設定してください。

​【サンプルの入力例】

Rotation : X=110 Y=-30 Z=0

Intensity : 2

 これで画面が明るくなり、昼のような雰囲気になります。

 次にロゴを表示しましょう。

​ 「UI」→「Image」を選択して、画像を追加してください。Imageの名前はLogoにしておきます。

 CanvasのCanvas Scalerは、MainシーンのCanvasと同じ設定にしておいてください。

​ Logoのパラメータを設定します。以下のように設定してください。

(Logoの画像はEXで改造する前提なので若干ホラー風になっています)

【サンプルの入力例】

Anchor : 上

PosX    : 0     PosY : -110
Width   : 480  Height : 240

Sauce Image : Logo 

 Canvasの子オブジェクトに「UI」→「Text - TextMeshPro」を追加してください。名前はStartTextにしておきます。

 StartTextのパラメータを設定します。以下のように設定してください。

【サンプルの入力例】

Anchor : 下

PosX    : 0      PosY   : 80

Width   : 400  Height : 50

Text Input  : Aボタンでスタート

Font Asset : Corporate-Mincho-ver3 SDF

Font Style  : 太字

Font Size   : 36

Alignment  : 中央

 これで必要なオブジェクトの配置が完了しました。​お好みでオブジェクトを追加、調整してください。

 次にクリア用のシーンを作成しましょう。後ほど作成するクリア演出を考慮して、真っ白の背景に文字を表示するだけのシンプルなものにしておきます。

​ 新しいシーンClearを作成してください。

 Clearシーンに切り替えて、Imageを追加してください。

​ Canvasの設定は例のごとく他のCanvas Scalerに合わせてください。

 Imageのインスペクター左上にあるAnchorを選択して、Altキーを押しながら右下のボタンをクリックしてください。画像が画面全体に引き伸ばされます。この設定は画面サイズが変わっても、画像が画面全体に引き伸ばされるようにできます。

 白い画像が画面全体に引き伸ばされたらOKです。

 次にTextMeshProを追加して、名前はClearTextにしておきます。

​ パラメータは以下のように設定してください。

​【サンプルの入力例】

Anchor : 左上

PosX    : 260  PosY   : -80

Width   : 400  Height : 50

Text Input    : CLEAR

Font Asset   : Corporate-Mincho-ver3 SDF

Font Style    : 太字

Font Size     : 80

Vertex Color : 青色(好きな色でOK)

 今までのLessonで説明してきた通り、UIの表示順はヒエラルキーの順番に依存します。ヒエラルキーで下部にあるUIが手前に表示されるため、ClearTextが背景の後ろに表示されないように注意してください。

 これでクリア画面が完成しました。

 単体で見るとシンプルですが、クリア演出と合わせると違和感のないように見えるはずです。

5-2 フェード処理の実装

5-2 フェード処理の実装

 シーンを繋げる前にフェード処理を実装しましょう。シーンを切り替える際に画面をぶつ切りにせずに「暗転」→「暗転の裏でシーン切り替え」→「明転」とすることで、自然にシーンを切り替えます。

​ 2Dランゲーム編5-3と流れは同じです。

 さらに今回はシェーダーグラフを使ってワンランク上の演出も行ってみましょう。

​ まずはClearシーンに新しいCanvasを作成してください。

​ 既存のCanvasと区別がつくように名前をFadeCanvasにしましょう。

 CanvasコンポーネントのSort Orderは、複数のCanvasがあった場合の表示優先度を設定する項目です。

 これからフェード用に真っ黒な画像を表示しますが、それがステージセレクトやゲームのUIより後ろに表示されると困ります。そのため、フェード用の画像は他のUIより前面に表示されるように設定する必要があります。

​ Sort Orderの値が大きいCanvasほど前面に表示されます。0より大きければ何でも構わないのですが、後ほど他のUIを追加する可能性も見越して、10など余裕を持った値にしておきましょう。

​ Canvas Scalerも他のCanvasと同じ設定にしておいてください。

 FadeCanvasの子オブジェクトにImageを追加してください。

​ 先ほどと同じようにImageのアンカーを選択して、Altキーを押しながら右下を選択してください。画像が画面全体に引き伸ばされたらOKです。

 次にシェーダーグラフを使用してフェードの演出を作ってみましょう。

​ 白黒で構成されたルール画像というものを用意して、画像の白い部分から画像を表示(不透明度を変更)していきます。どの程度白い部分を表示するかどうか決めるしきい値を用意して、その値を増加させていくことで画面を暗くしていきます。

 ルール画像を複数​用意することで、​様々なフェード演出を簡単に実装することができます。

​ まずはShaderフォルダ内にImage用のシェーダーグラフを作成しましょう。

 名前はFadeにしておきます。

 シェーダーグラフを作成できたら、ダブルクリックして編集画面を開いてください。さらにタブの名前をダブルクリックすると全画面表示になります。

 シェーダーグラフを作成します。

​ まずはルール画像を指定するためのパラメータを用意しましょう。

​ 左上の+ボタンを押して、Texture2Dを選択してください。パラメータの名前はFadeImageにしておきます。ここにルール画像を設定することになります。

 FadeImageを読み込むためのノードを追加します。

 

 何もない場所を右クリックして「Create Node」を選択しましょう。

 検索欄に「Sample」と入力すると出てくる「Sample Texture 2D」ノードを追加してください(似た名前のノードがいくつかあるので注意)

 Texture2D型のパラメータFadeImageを作成して、ドラッグ&ドロップしてください。

 Fade ImageノードをSample Texture 2DノードのTextureへ繋げてください。

 次にしきい値のパラメータを作成しましょう。

​ Float型のパラメータを作成して、名前をBorderにしておいてください。この値を基準に不透明度を計算していきます。

 Borderを選択して、Graph Inspector内のModeをDefaultからSliderに変更してください。

 Minを0、Maxを1にすることで0~1の範囲のスライダーを表示することができます。

3_4_112
3_4_113

 Borderの計算を行いましょう。

​ 画像の不透明度は以下の計算式で求めることができます。

 ルール画像のr値 + ( しきい値 * 2 - 1 )

 3D脱出ゲーム編3-8も参考にしつつ、シェーダーグラフで上記の計算を行ってみてください。

【ヒント】

・加算を行うノード … Add ノード

・減算を行うノード … Subtract ノード

​・乗算を行うノード … Multiply ノード

 最後に計算結果をSaturateノードに繋げてください。

 Saturateノードは入力値を0~1の間に収めてくれるノードです。a(不透明度)は0~1で扱うので、範囲外の値を渡してしまわないようにしています。

​ SaturateノードのOutをFragmentのAlphaに繋げてください。

 計算の流れは実際に仮の値を入れて計算するとわかりやすくなります。

 rの値は白に近いほど1に近くなります。計算結果の値が不透明度になる点を意識してイメージしてみましょう。

【r=0.5 Border=0 のとき】(rがどんな値でも表示されない)

0.5 + ( 0*2-1 ) = -0.5 → Saturateノードで0に

【r=0.5 Border=0.5 のとき

0.5 + ( 0.5*2-1 ) = 0.5

【r=0.8 Border=0.6 のとき

0.8 + ( 0.6*2-1 ) = 1

【r=0.2 Border=1 のとき】(rがどんな値でも表示される)

0.2 + ( 1*2-1 ) = 1.2 → Saturateノードで1に

​ 最後に色を変えられるようにしましょう。Color型のパラメータを作成して、FragmentのBase Colorに繋げてください。

 シェーダーグラフが書けたら「Save Asset」を押して保存してください。

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

​ 新しいマテリアルFadeMaterialを作成して、Fadeシェーダーをドラッグ&ドロップして適用してください。

 FadeMaterialを選択すると、作成したパラメータを確認できます。

​ FadeCanvasに追加したImageに、FadeMaterialを適用してください。

​ シェーダーの設定を開いて、BorderやColorを色々調整してみてください。ルール画像は設定されていませんが、Imageの不透明度が変化すると思います。

​ FadeImageにルール画像を設定しましょう。様々なルール画像を配布しているサイトがあるため、こちらからダウンロードしてください。

 自作できそうな人は​自作してもOKです。

【ルール画像配布サイト】

https://4you.bz/rule

​ ダウンロードしたフォルダ内に様々なルール画像が入っているので、Spriteフォルダ内にドラッグ&ドロップしてください。

​ (サイトが閉鎖するかもしれないので)一応簡易的なルール画像も置いておきます。

​ 左下から暗くなっていくルール画像です。

​ 追加したルール画像をFadeMaterialのFadeImageに設定してください。

​ Borderを操作するとフェード演出が再生されます。​Scene内で確認してみてください。

​ Scene内ではフェード演出が正常に動いていますが、Game内では不透明度が反映されていません。

 これはUnityの不具合(仕様?)で、シェーダーグラフにはUI表示に必要な設定の一部が含まれていないようです。

【参考1】https://qiita.com/masakatsu_/items/a1d0fa0324fa8f10e8c6

【参考2】https://forum.unity.com/threads/shader-graph-ui-image-shader-does-not-work.1202461/

※ 参考2のフォーラムによると、Unity2023.2.0a18以降では修正されているようです。その場合以下の措置は無視しても問題ないと思います。

​ 参考1によるとCanvasの設定を変更することで対応できるとのことなので、FadeCanvasの設定を変更しましょう。

 FadeCanvasのRenderModeを「Screen Space - Camera」に変更してください。

 Render Cameraの項目が追加されるので、Main Cameraを設定しましょう。

​ Plane Distanceには1を入力してください。

​ シーンのUIを表示しているCanvasを選択して、同じようにRender ModeとRender Cameraの設定をします。Clearシーンの場合、Plane Distanceの設定は不要です

​ これでGameでもフェード演出を確認できます。

 CanvasのRender Modeには以下のような違いがあります。行いたい表現に応じて設定してください。

「Screen Space - Overlay」

 デフォルトの設定で、常に最前面にCanvasが表示されます。UIを表示する場合は基本的にこの設定になります。

 3Dモデルを手前に表示したい時は別のカメラを用意するなど工夫が必要です。

「Screen Space - Camera」

 カメラを指定して、指定したCameraにCanvasを追従させます。カメラが移動、回転してもCanvasはカメラとの相対座標を保ちます(Canvasをカメラの子オブジェクトにするようなイメージです)

 CameraとCanvasの距離を指定でき、その間にあるオブジェクトはCanvasの手前に表示されます。

​「World Space」

 ワールド空間上の座標を指定してCanvasを配置します。カメラからCanvasが外れると映らなくなります。シーン上にシンプルにCanvasを表示したい時に使えます。

Unity Tips!

​ 次はフェード処理のスクリプトを書きましょう。

​ 新しいスクリプトFadeSceneを作成して、以下のように入力してください(基本的な流れは2Dランゲーム編5-3と同じです)

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

 

public class FadeScene : MonoBehaviour
{
    bool m_fadeStart = false;   // trueなら一連の処理を開始

    bool m_fadeMode = false;    // falseなら暗くなる trueなら明るくなる
    float m_alpha = 0.0f;       // 画像の不透明度

    [SerializeField]
    float FadeSpeed = 1.0f;     // フェードの速度(大きいほど速い)

 

    // 遷移先のシーン名を保存
    string m_sceneName;
    // 自身が使用するImageを保存
    Image m_image;

    // falseならマテリアルを使用 trueならImageを使用
    bool m_mode = false;

 

    // フェード開始
    public void FadeStart(string sceneName, Color color, bool mode)
    {
        // フェード開始の準備をする
        m_fadeStart = true;
        m_sceneName = sceneName;
        m_mode = mode;

 

        // 自分の子オブジェクトにアタッチされているImageを取得する
        m_image = transform.GetChild(0).GetComponent<Image>();

        if (m_mode)
        {
            // 通常のフェード
            m_image.material = null;
            m_image.color = color;
        }
        else
        {
            // マテリアルを初期化
            m_image.material.SetFloat("_Border", 0.0f);
            m_image.material.SetColor("_Color", color);

            // 自身のRenderCameraにメインカメラを設定する
            GetComponent<Canvas>().worldCamera = Camera.main;
        }

 

        // 自身はシーンをまたいでも削除されないようにする
        DontDestroyOnLoad(gameObject);
    }

 

    void Update()
    {
        // フェードが開始していないなら中断
        if (m_fadeStart == false)
        {
            return;
        }

 

        // 自身のRenderCameraにメインカメラを設定する
        if (GetComponent<Canvas>().worldCamera == null &&
            m_mode == false)
        {
            GetComponent<Canvas>().worldCamera = Camera.main;
        }

 

        // フェード処理
        if (m_fadeMode == false)
        {
            // 画面を暗くする
            m_alpha += FadeSpeed * Time.deltaTime;

            // 完全に暗くなったのでシーンを変更する
            if (m_alpha >= 1.0f)
            {
                SceneManager.LoadScene(m_sceneName);
                // 明るくするモードに変更
                m_fadeMode = true;
            }
        }
        else
        {
            // 画面を明るくする
            m_alpha -= FadeSpeed * Time.deltaTime;

            // 完全に明るくなったので自身を削除する
            if (m_alpha <= 0.0f)
            {
                Destroy(gameObject);
            }
        }

 

        // 最後に不透明度を設定する
        if (m_mode)
        {
            Color nowColor = m_image.color;
            nowColor.a = m_alpha;
            m_image.color = nowColor;
        }
        else
        {
            m_image.material.SetFloat("_Border", m_alpha);
        }
    }

}

​ コードが長いため一見難しく見えるかもしれませんが、やっていることは前述した「最前面に真っ黒な画像をだんだん表示させる」→「完全に表示されたらシーンを切り替える」→「遷移先のシーンで最前面に表示した画像をだんだん透明にする」という処理になります。

 modeではマテリアルを使ってフェード演出を行うか、Imageを使ってフェード演出を行うか選択できるようにしています。これはMainシーンの演出では通常のフェード演出を行った方が見栄えがよいため、簡単に変更できるようになっています。

【プログラムの解説】

・マテリアルのSetFloat関数を使うことで、第一引数名に指定したパラメータに第二引数の値を代入することができます。第一引数に対応するのはパラメータのReferenceの部分です。

​ これを使うことでスクリプト内でパラメータを動的に変更しています。

​ コードが書けたら、FadeSceneスクリプトをFadeCanvasにアタッチしてください。

​ これでFadeCanvas側の準備は完了です。

​ FadeCanvasをプレハブ化して、シーン上にあるFadeCanvasは削除してください。

​ これでフェード演出を行う準備が整いました。後は適切なタイミングでFadeCanvasを生成して、遷移先のシーンを指定するだけです。

5-3 シーンを繋げる

5-3 シーンを繋げる

 それでは各シーンを繋げていきましょう。クリア演出も同時に作っていきます。

 まずは各シーンをビルド対象にしておきます。Build Settingsを開いて、Title、Main、Clearシーンをビルド対象に設定してください(設定方法を忘れた場合は3Dアクションゲーム編5-5を参照)

​ Titleシーンから始まるように、一番上のシーンはTitleシーンにしておきます。

 TitleシーンからMainシーンへの遷移を実装しましょう。

​ Titleシーンを開いて、UIを表示しているCanvasの設定をしてください。

 RenderModeを「Screen Space - Camera」に変更して、RenderCameraをメインカメラに設定します。Plane Distanceは1に設定してください。

 空オブジェクトを作成して、名前をTitleにしておきます。

 新しいスクリプトSceneChangeを作成して、以下のように入力してください。青い部分は穴埋めになっています。

​【ヒント】FadeCanvasを生成→生成したオブジェクトにアタッチされているFadeSceneコンポーネントを取得→FadeStart関数を呼び出してみよう

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

 

public class SceneChange : MonoBehaviour
{
    [SerializeField]
    string m_sceneName;         // 遷移先のシーン名

    [SerializeField]
    GameObject m_fadeCanvas;    // フェード演出用オブジェクト

 

    bool m_sceneChange = false; // シーン切り替えフラグ
 

    void Update()
    {
        // メインカメラを回転させる
        Camera.main.transform.
            RotateAround(Vector3.zero, Vector3.up, 10.0f * Time.deltaTime);


        // シーン切り替え中は何もしない
        if (m_sceneChange)
        {
            return;
        }

 

        // 決定キーが押されたら
        if((Input.GetKeyDown("joystick button 0") || Input.GetKeyDown(KeyCode.Return)))
        {
            // ① フェード演出用オブジェクトを生成
            (ここに入力)
            // ② 生成したオブジェクトのFadeStart関数を呼び出す
            //    (色はColor.blackを指定、modeはfalse)

            (ここに入力)

 

            m_sceneChange = true;
        }
    }

}

 TitleオブジェクトにSceneChangeをアタッチしてください。

​ SceneNameには遷移先のシーン名を入れるため「Main」と入力しておきます。FadeCanvasにはプレハブ化したFadeCanvasを指定しましょう。

 これでTitleシーンからMainシーンへの遷移ができるようになりました。AボタンかEnterキーを押すとMainシーンに切り替わることを確認してみてください。

 シーンを切り替えるとMainシーンのディレクションライトが反映されておらず暗く描画されているので、ライトマップを事前に作成しておきましょう。

 「Window」→「Rendering」→「Lighting」を選択して、Lightingウィンドウを開いてください。

 Sceneタブの右下にある「Generate Lighting」ボタンをクリックしてライトマップを作成してください(時間がかかる場合はライト以外のオブジェクトを非アクティブにしてから行うと早くなります)

​ この措置をTitleシーンとMainシーンのそれぞれで行ってください。

 これでシーンを切り替えてもライトが反映されるようになります。

 次はクリア演出を作成して、MainシーンからClearシーンへの遷移を実装します。

​ パスワード入力に成功すると扉が開く→光が差し込んで画面がホワイトアウト→Clearシーンへ という流れの演出を作りましょう。

​ まずは扉の先に地面を設置しましょう。

 「3D Object」→「Plane」を選択してください。板を追加できたら座標と大きさを調整して、マテリアルを貼ってください。

【サンプルの入力例】

Position  : X=263.8 Y=-2.2 Z=8

Scale    : X=20 Y=1 Z=20

Element 0   : dark_iron

 地面を置くことで、この後に置くスポットライトが見えやすくなります。

 次にスポットライトを設置しましょう。

 「Light」→「Spotlight」を選択してください。

 インスペクターでスポットライトのパラメータを調整してください。

【サンプルの入力例】

Position  : X=180 Y=5.5 Z=7.3

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

Shape        : Inner=90  Outer Spot Angle=120

Intensity     : 1e+15(適当に大きい値を入れればOK) 

Indirect Multiplier : 0

Range        : 15

 Intensityはライトの輝度の項目です。画面を覆うレベルで明るければOKなので、適当に大きな値を入れておきましょう(雑ですがこれが一番手っ取り早いです)

 Indirect Multiplierは間接光の強度を設定できます。

 例えば以下の画像の中央に置かれているポイントライトや天井は白色ですが、天井や箱は赤みがかった色に見えます。これは赤い壁に当たった光が反射して、白い天井や箱が赤く見えています。

​ 今回のライトは演出用で反射光は特に必要ないので0にしています。

 このライトはクリア演出用のもので、それ以外の時は必要ないので最初は非アクティブにしておいてください。

 クリア演出を作成しましょう。

 まずはクリア演出中に移動したりカメラを回転したりできないように、クリア演出中の状態を管理します。

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

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

 

public class GameManager : MonoBehaviour
{
    // ゲームの状態
    public enum GameState
    {
        enGameState_Play,
        enGameState_Clear,
    }
    static GameState m_gameState = GameState.enGameState_Play;

 

    public void SetGameState(GameState gameState)
    {
        m_gameState = gameState;
    }

    // どこからでも呼び出せる関数
    public static GameState GetGameState()
    {
        return m_gameState;
    }

 

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


​~後略~

~前略~

        // 所持品から番号を削除
        ItemID[SelectItemNo] = -1;
        // UIを更新
        ItemUI.UpdateUI();
    }

 

    void Awake()
    {
        // UIを更新(初期化)
        ItemUI.UpdateUI();
        // ステートを更新(初期化)
        m_gameState = GameState.enGameState_Play;

    }

 

    void Update()
    {
        // プレイ中でないなら中断
        if (m_gameState != GameState.enGameState_Play)
        {
            return;
        }

 

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


​~後略~

 ステート用の変数とステートを取得する関数にstaticがついている点に注意してください。staticがついているGetGameState関数はどこからでも呼び出すことができます。

​ また、m_gameStateにもstaticがついています。この場合変数は自動で初期化されないので、Awake関数内でゲームが開始する度に手動でステートを初期化しています。

 ついでにクリア中はアイテムを捨てられないようにしています。

 m_gameStateがクリア中の場合は移動できないようにしましょう。

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

~前略~

    void FixedUpdate()
    {
        // プレイ中でないなら中断
        if (GameManager.GetGameState() != GameManager.GameState.enGameState_Play)
        {
            return;
        }

 

        // カメラを考慮した移動
        Vector3 PlayerMove = Vector3.zero;
        Vector3 stickL = Vector3.zero;

​~後略~

 同じ処理をカメラの回転処理でも行います。

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

~前略~
 

    void LateUpdate()
    {
        // プレイ中でないなら中断
        if (GameManager.GetGameState() != GameManager.GameState.enGameState_Play)
        {
            return;
        }

 

        // 右スティックでカメラ回転

        // 上下
        float rot = Time.deltaTime * RotSpeed;

​~後略~

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

~前略~
 

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

 

    void Update()
    {
        // プレイ中でないなら中断
        if (GameManager.GetGameState() != GameManager.GameState.enGameState_Play)
        {
            return;
        }

 

        // 球体を発射する
        RaycastHit hit;
        if (Physics.SphereCast(m_cameraObject.transform.position, SPHERE_RADIUS,
            m_cameraObject.transform.forward, out hit, SPHERE_MAX_DISTANCE))

​~後略~

 後はクリア時にライトをアクティブにして、Clearシーンに切り替えるだけです。

​ 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];

 

    // クリア演出
    [SerializeField, Header("クリア演出")]
    GameObject CanvasUI;
    [SerializeField]
    GameObject ClearLight;
    [SerializeField]
    GameObject FadeCanvas;

 

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

​~後略~

~前略~
 

        // 判定
        if (answer)
        {
            // 正解
            m_isNumberOK = true;
            // 親オブジェクトのアニメーション開始
            transform.parent.GetComponent<Animator>().SetTrigger("Open");
            // 正解なので〇を表示
            SetNumberText("〇〇〇");

 

            // ゲームステートをクリアに
            GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>()
                .SetGameState(GameManager.GameState.enGameState_Clear);
            // UIを非表示に
            CanvasUI.SetActive(false);
            // ライトを有効に
            ClearLight.SetActive(true);

 

            // フェード演出
            Invoke("ClearFade", 1.0f);

        }
        else
        {
            // 不正解
            NumberReset();
            // ×を表示して1秒後に戻す
            SetNumberText("×××");
            Invoke("NumberUpdate", 1.0f);
        }

    }
 

    // Clearシーンに切り替える
    void ClearFade()
    {
        // フェード演出用オブジェクトを生成
        GameObject fadeObject = Instantiate(FadeCanvas);
        // 生成したオブジェクトのFadeStart関数を呼び出す
        fadeObject.GetComponent<FadeScene>().FadeStart("Clear", Color.white, true);
    }

 

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


​~後略~

 これでMainシーンからClearシーンへの遷移が完成しました。

​ ゲームを実行して、クリア演出の流れが正常に動作するか確認してみてください。

 最後にClearシーンからTitleシーンへの遷移を実装します。

​ Clearシーンに空オブジェクトを追加して、SceneChangeをアタッチしてください。Titleシーンと同じようにFadeCanvasを指定して、遷移先のシーン名は「Title」にしておきます。

 これで3つのシーンが繋がって、ゲームループが成立するようになりました。

 ゲームを一通り遊んで確認してみましょう。

【評価テスト】​

https://forms.gle/xVsoTWux8TCGHfxN8

評価テスト

5-4 移動時に画面を揺らす

5-4 移動時に画面を揺らす

 次に移動中に画面を上下に揺らす演出を入れてみましょう。

 一見難しそうに見えるかもしれませんが、Lerp関数(線形補間)を使うだけなので簡単です。

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

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

 

public class GameCamera : MonoBehaviour
{
    [Header("カメラの回転速度"), SerializeField]
    float RotSpeed = 200.0f;
    [Header("カメラの回転モード(X)"), SerializeField]
    bool CameraModeX = false;
    [Header("カメラの回転モード(Y)"), SerializeField]
    bool CameraModeY = false;

 

    [Header("X回転最大値"), SerializeField]
    float MaxX = 60.0f;
    [Header("X回転最小値"), SerializeField]
    float MinX = -40.0f;

 

    // 歩行縦揺れ演出
    Rigidbody m_playerRB;
    float m_moveShake = 0.0f;   // シェイク量
    Vector3 m_moveShakeAddPos = Vector3.zero;
    Vector3 m_moveShakeAddPosBackUp = Vector3.zero;
    bool m_moveStop = false;    // 移動停止状態か
    [Header("縦揺れ速度"), SerializeField]
    float ShakeSpeed = 12.0f;
    [Header("縦揺れ量"), SerializeField]
    float ShakePower = 0.15f;

 

    // 初期座標
    Vector3 m_defPos;

 

    void Awake()
    {
        // 初期ローカル座標を保存
        m_defPos = transform.localPosition;
        // 親オブジェクトにアタッチされているRigidbodyを取得
        m_playerRB = transform.root.gameObject.GetComponent<Rigidbody>();

    }


​~後略~

~前略~

        transform.RotateAround(transform.position, Vector3.up, rot);

 

        // Z軸の回転があるとややこしいので制限する
        Vector3 angles = transform.eulerAngles;
        angles.z = 0.0f;
        transform.eulerAngles = angles;

 

        // 座標は固定
        transform.localPosition = m_defPos;

 

        // 画面シェイク処理
        MoveShake();

    }

 

    // 移動時の縦揺れ
    void MoveShake()
    {
        // プレイヤーにかかっている力を取得
        Vector3 playerVelocity = m_playerRB.velocity;
        playerVelocity.y = 0.0f;

 

        // 移動中か判定
        if (playerVelocity.sqrMagnitude > 50.0f)
        {
            // 移動中

            // 停止状態から移動開始時にリセットする
            if (m_moveStop)
            {
                m_moveShakeAddPos = Vector3.zero;
            }

 

            // Sin関数を用いて上下の移動量を計算
            m_moveShakeAddPos = new Vector3(m_moveShakeAddPos.x,
                m_moveShakeAddPos.x + Mathf.Sin(Time.time * ShakeSpeed) * ShakePower,
                m_moveShakeAddPos.z);

 

            // 移動量を記憶しておく
            m_moveShakeAddPosBackUp = m_moveShakeAddPos;
            m_moveShake = 0.0f;
        }
        else
        {
            // 移動中ではない

            // 線形補間用に時間を加算する
            m_moveShake += Time.deltaTime * ShakeSpeed;

            // 移動中だった時の移動量をベースにして、正常な位置に戻す
            m_moveShakeAddPos = (m_defPos + m_moveShakeAddPosBackUp) -
                Vector3.Lerp(m_defPos + m_moveShakeAddPosBackUp, m_defPos, m_moveShake);

 

            m_moveStop = true;
        }

 

        // 移動量を加算する
        transform.localPosition += m_moveShakeAddPos;
    }

}

【プログラムの解説】

​・上下の移動量計算にSin関数を使っています。

 ここでは数学的な説明は省きますが、Sin関数に増加し続ける値(時間など)を引数にすることで、周期的に増減する値を得ることができます。Sin関数の結果は常に-1~1の範囲になります。

 三角関数をいきなり使いこなすのは難しいかもしれませんが「Sin関数にTimeを入れると-1~1の範囲で周期的な値が返ってくる」という特性は覚えておくと何かと便利です。

​【Sin関数のわかりやすいイメージ】https://qiita.com/Nekomasu/items/f526b9392fd16f2bd243

 Cos関数でも増加する値を渡すことで波形を得ることができます。

​ ただし、Sin関数とは位相が異なります。X=0の箇所に注目してみると、上記のSin関数のグラフは原点を通っているのに対して、Cos関数は1を通っています。

(Cos関数はY軸を中心に線対称のグラフになっている点に注目するとわかりやすいかも)

 また、Cos関数とSin関数の位相の違いを利用して、円運動を行うことも可能です。

 ちなみにSin関数とCos関数を入れ替えると反時計回りの移動になります。

Unity Tips!
円形運動

・Lerp関数は3Dアクションゲーム編でも登場した線形補間を行う関数です。

 第一引数に開始地点、第二引数に目的地、第三引数に補間率を指定することで開始地点から目的地までの座標を補間します

 この関数も覚えておくと便利なので、自分で使って覚えてみましょう。

 ここまでできたらゲームを実行して、移動中にカメラが揺れることを確認してみてください。

(人によっては3D酔いを起こすかもしれないので、酔いそうな人はMoveShake関数をコメントアウトしましょう。コンフィグ画面を作ってプレイヤーが選択できるようにするとGOOD!)

5-5 クリア時に扉の方を向く

5-5 クリア時に扉の方を向く

 さらにカメラを改造しましょう。

 パスワード入力に正解した時、カメラの方を向く処理を実装します。これによって、正解したことがよりプレイヤーに伝わりやすくなります。こちらもLeap関数を使えば簡単に実装できます。

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

~前略~
 

    // 歩行縦揺れ演出
    Rigidbody m_playerRB;
    float m_moveShake = 0.0f;   // シェイク量
    Vector3 m_moveShakeAddPos = Vector3.zero;
    Vector3 m_moveShakeAddPosBackUp = Vector3.zero;
    bool m_moveStop = false;    // 移動停止状態か
    [Header("縦揺れ速度"), SerializeField]
    float ShakeSpeed = 12.0f;
    [Header("縦揺れ量"), SerializeField]
    float ShakePower = 0.15f;

 

    // フォーカス処理
    Vector3 m_targetPos, m_startPos;
    float m_focusTimer = 0.0f;
    float m_speed;
    bool m_isFocus = false;
    public void FocusStart(Vector3 targetPos, float speed, float range = 10.0f)
    {
        // フォーカス準備
        m_startPos = transform.position + (transform.forward * range);
        m_targetPos = targetPos;
        m_speed = speed;
        m_focusTimer = 0.0f;
        m_isFocus = true;
    }


    // 初期座標
    Vector3 m_defPos;

    void Awake()
    {
        // 初期ローカル座標を保存
        m_defPos = transform.localPosition;
        // 親オブジェクトにアタッチされているRigidbodyを取得
        m_playerRB = transform.root.gameObject.GetComponent<Rigidbody>();
    }

 

    void Update()
    {
        // フォーカス処理
        if (m_isFocus == false)
        {
            return;
        }

 

        m_focusTimer += Time.deltaTime * m_speed;
        Vector3 nowTarget = Vector3.Lerp(m_startPos, m_targetPos, m_focusTimer);

        // 対象を見る
        transform.LookAt(nowTarget);
        if (m_focusTimer > 1.0f)
        {
            // フォーカス終了
            m_isFocus = false;
        }

    }

 

    void LateUpdate()
    {
        // プレイ中でないなら中断
        if (GameManager.GetGameState() != GameManager.GameState.enGameState_Play)
        {
            return;
        }

~後略~

【プログラムの解説】

・LookAt関数はTransformクラスの関数で、引数に指定した座標の方向を向くことができます。

​ いきなり目標地点の座標を見てしまうと変化が急すぎるため、開始地点から目標地点までの座標をLeap関数で補間し、その座標に注目するようにしています。これによって、緩やかに目標地点に注目することができます。

 FinalDoor関数を開いて、赤い部分のコードを追加してください。

~前略~
 

            // ゲームステートをクリアに
            GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>()
                .SetGameState(GameManager.GameState.enGameState_Clear);

            // UIを非表示に
            CanvasUI.SetActive(false);
            // ライトを有効に
            ClearLight.SetActive(true);

 

            // カメラ演出
            Vector3 targetPos = transform.position + new Vector3(0.0f, 4.0f, 0.0f);
            Camera.main.GetComponent<GameCamera>().FocusStart(targetPos, 1.0f);

 

            // フェード演出
            Invoke("ClearFade", 1.0f);
        }
        else
        {
            // 不正解
            NumberReset();
            // ×を表示して1秒後に戻す
            SetNumberText("×××");
            Invoke("NumberUpdate", 1.0f);
        }

    }


~後略~

 これでクリア時に扉の方に注目するようになります。ゲームを実行して確認してみましょう。

​(今回使用しなかった第三引数はLessonEXで使用します)

5-6 ヒントを追加

5-6 ヒントを追加

 謎解きの難易度を下げるためにヒント用のメモを設置しましょう。

 スクリプトはもう完成しているので、オブジェクトを追加するだけです。

​ まずはマテリアルを作成します。必要なマテリアルを一気に作ってしまいましょう。

 Materialフォルダ内のHintを複製して、名前はMemo_YellowBookに変更してください。初期設定はHintを流用します。

 Memo_YellowBookマテリアルのBase MapをSpriteフォルダ内のMemo1に変更してください。変更するのはここだけで、​法線やMetallicの設定は変更しなくて構いません。

 同じ流れでマテリアルをあと2つ作成してください。​作成したマテリアルのBaseMapだけを変更しましょう。

【BaseMapに設定するスプライト(テクスチャ)】

​Memo_YellowBook → Memo1

​Memo_Apple    → Memo2

​Memo_Silver    → Memo3

 まずは黄色い本についてのヒントを配置しましょう。

​ 「3D Object」→「Plane」を追加してください。インスペクターからパラメータを設定しましょう。​

​ OutlineコンポーネントとItemObjectコンポーネントをアタッチしてください。

​【サンプルの入力例 Memo1

 Position  : X=130 Y=3.34 Z=37

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

​ Scale      : X=0.2 Y=1 Z=0.26

 Element0 : Memo_YellowBook

 Name : メモ

​ Explanation : メモが置いてある…
        「黄色い台に合う本を…」

 これで「黄色い台に黄色い本を置く」というヒントを配置できました。調べることで説明文のヒントを確認できます。

 このメモをコピーして、残り2つのヒントも配置しましょう。座標やマテリアル、説明文をそれぞれ変更してください。

​【サンプルの入力例 Memo2】

 Position  : X=91 Y=6.25 Z=-116.5

 Rotation  : X=0 Y=90 Z=0

​ Scale      : X=0.2 Y=1 Z=0.26

 Element0 : Memo_Apple

 Name : メモ

​ Explanation : メモが置いてある…
        「リンゴは戸棚の中に」

【サンプルの入力例 Memo3】

 Position  : X=51 Y=3.5 Z=52

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

​ Scale      : X=0.2 Y=1 Z=0.1

 Element0 : Memo_Silver

 Name : メモ

​ Explanation : メモが置いてある…
        「銀 しりとり 宝箱」

3_5_34
3_5_33

 これでヒント用のメモを配置することができました。​お好みで座標を変更したり、ヒントを追加したりしても構いません。

5-7 ロウソクを作る

5-7 ロウソクを作る

 初期状態では様々な場所にロウソクが置いてありますが、火がついていません。

​ 簡易的な火のエフェクトとポイントライトを追加しましょう。

 

​ 火のエフェクトはParticle Systemを活用します。3Dアクションゲーム編Lesson6-3も参考にしながら進めていきましょう。​

 まずは「Model」→「Mega Fantasy Props Pack」→「Prefabs」→「Miscellaneous」→「Candels」を選択して、candle.001をダブルクリックしてロウソクのプレハブを開きましょう。

 まずは炎のエフェクトを作成します。「Effects」→「Particle System」を選択してください。

 作成したParticle Systemの名前をFireBaseに変更して、Transformを以下のように調整してください。

​【サンプルの入力例】

 Position  : X=0 Y=0.4 Z=0

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

 Particle Systemの設定をしましょう。各項目の解説は3Dアクションゲーム編Lesson6-3で既に行っているため省略します。

・Durationを1にする

・Start Lifetimeを1にする

・Start Speedを1~1.5の範囲にする

(設定方法は後述)

・Start Sizeを0.5~2の範囲にする

​・Start Colorをオレンジにする

​(右の図を参照)

​[Emission]

​・Rate over Timeを20にする

 項目の右端にある▼をクリックして

Random Between Two Constants を選択することで、

下限と上限の間からランダムな値を使用するように

設定することができます。

 

 エフェクトが単調にならず、変化をつけられるので

​エフェクトを作る際には覚えておきましょう。

3_5_41

 閉じている項目がある場合はクリックして

開いてください。

 

[Shape]

・ShapeをBoxにする

・ScaleをXYZ全て0.1にする

[Velocity over Lifetime]

 チェックを入れて有効にする

・Linear XYZ全て-0.2~0.2の範囲にする

→パーティクルに力を加えることができる

項目です。これによって炎が揺れながら

上昇するように​なります。

【Size over Lifetime】

 チェックを入れて有効にする

​・Sizeのグラフを下がっていくグラフに設定

​(右から2番目)

 これでパーティクルの動きは炎らしくなりますが、見た目は微妙な状態です。標準のパーティクルはアルファブレンドになっていますが、これを加算ブレンドに変更しましょう。

 Unityに標準で実装されているパーティクル用マテリアルの設定を直接変えることはできないため、マテリアルをコピーして、そのマテリアルを加算ブレンドに設定します。

​ プロジェクトウィンドウ右上の検索欄に「Particle」と入力してください。左側のSearchを「All」に設定しましょう。

​ ParticlesUnlitというマテリアルがヒットするため、それを選択してからCtrl+Cキーを押すことでコピーしてください(同名のシェーダーがあるので注意)

 コピーできたらMaterialフォルダを開いて、Ctrl+Vキーを押してペーストしてください。

 コピーしたParticlesUnlitマテリアルを選択して、Blending ModeをAdditiveに変更しましょう。

​ これで描画の際に加算合成されるようになります。

 エフェクトのMaterialに先ほど作成したマテリアルをドラッグ&ドロップしてください。

 これで加算ブレンドが反映され、炎のエフェクトが完成しました。

​ お好みでパラメータを調整してください。

 もう少しエフェクトのクオリティを上げましょう。

 

 作成したエフェクトをコピーして名前をFireCenterに変更します。

​ ScaleをXYZ全て0.4に設定し、StartColorを白&半透明に設定してください。

 白いエフェクトを重ねることで炎のクオリティが上がります。

 ポイントライトを作成して、パラメータを設定してください。

​【サンプルの入力例】

 Position  : X=0 Y=0.8 Z=0

[General]

​ Mode : Mixed

[Emission]

​ Color : オレンジに設定(画像参照)

 Intensity : 200

 Range : 20

[Shadows]

 Shadow Type : Soft Shadows

​ Resolution : Mesium

 最後にcandle.001を選択して、Cast ShadowsをOffにしてください。

 これでロウソクの設定は完了です。プレハブ編集画面を閉じて、ゲームを実行してください。

 プレハブを編集したため、シーン内にある全てのロウソクに変更が反映されています。確認してみましょう。

​ 今はシーンが明るいため目立ちませんが、LessonEXで世界を暗くすると目立つようになります。

5-8 効果音を鳴らす

5-8 効果音を鳴らす

 最後に効果音を鳴らしましょう。システムさえ完成すれば残りは単純作業になります。

 基本的な流れは2Dランゲーム編Lesson6-3と同じですが、3Dサウンドへの対応を行っています。

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

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

 

public class OneShotAudioClip : MonoBehaviour
{
    AudioSource m_audioSource;
    bool m_isPlay = false;

 

    void Awake()
    {
        // シーンが切り替わってもオブジェクトが削除されないようにする
        DontDestroyOnLoad(gameObject);
    }

 

    // 効果音を再生
    public void PlaySE(AudioClip audioClip, 
        float volume = 1.0f,
        float spatialBlend = 0.0f,
        float minDistance = float.MinValue, 
        float maxDistance = float.MaxValue)
    {
        // 自分にアタッチされているAudioSourceを取得
        m_audioSource = GetComponent<AudioSource>();

 

        // オーディオクリップを設定
        m_audioSource.clip = audioClip;
        m_audioSource.volume = volume;
        m_audioSource.spatialBlend = spatialBlend;

 

        // 距離を設定
        if (minDistance != float.MinValue)
        {
            m_audioSource.minDistance = minDistance;
        }
        if (maxDistance != float.MaxValue)
        {
            m_audioSource.maxDistance = maxDistance;
        }

 

        // 再生
        m_audioSource.Play();

        // 再生フラグを立てる
        m_isPlay = true;
    }

 

    void Update()
    {
        // 再生フラグが立っていて、オーディオソースの再生が終わったら…
        if (m_isPlay && m_audioSource.isPlaying == false)
        {
            // 自身を削除する
            Destroy(gameObject);
        }
    }

 

}

 基本的には2Dサウンドですが、必要に応じて3Dサウンドへ切り替えられるようにしています。

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

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

 

public class GameManager : MonoBehaviour
{

    // 効果音再生関数 どこからでも呼べる
    static public OneShotAudioClip PlaySE(AudioClip clip,
        GameObject sauceObject = null,
        float volume = 1.0f,
        float spatialBlend = 0.0f,
        float minDistance = 0.0f,
        float maxDistance = 0.0f)
    {
        // 効果音オブジェクトを生成
        GameObject oneShotObj = Instantiate((GameObject)Resources.Load("OneShotSE"));

        // 座標を設定
        if (sauceObject != null)
        {
            oneShotObj.transform.position = sauceObject.transform.position;
        }

 

        // オーディオクリップを設定
        OneShotAudioClip oneShotAudio = oneShotObj.GetComponent<OneShotAudioClip>();
        oneShotAudio.PlaySE(clip, volume,
            spatialBlend, minDistance, maxDistance);

 

        return oneShotAudio;
    }

 

    // ゲームの状態
    public enum GameState
    {
        enGameState_Play,
        enGameState_Clear,
    }
    static GameState m_gameState = GameState.enGameState_Play;

​~後略~

【プログラムの解説】

・static publicをつけた関数はどこからでも呼び出すことができます。

​ これによって、どこからでも効果音の再生処理を行うことが可能です。

・Resources.Load 関数はResourcesフォルダの中にある指定した名前のファイルをロードします。

2Dランゲーム編Lesson6-3参照)

​ 適当なシーンに空オブジェクトを作成して、名前を「OneShotSE」にしてください(間違えると動作しないので注意)

 OneShotSEにAudioSourceとOneShotAudioClipをアタッチしましょう。AudioSourceのPlay On Awakeのチェックを外しておいてください。

 Resourcesフォルダを作成してください。こちらも名前を間違えるとうまく動作しないので注意しましょう。

 OneShotSEオブジェクトをResourcesフォルダ内にドラッグ&ドロップしてプレハブ化してください。プレハブ化できたらシーン内にあるOneShotSEは削除しましょう。

 次にSoundフォルダを作成して、効果音素材をダウンロードしてください。

 今回は効果音素材にAudioStockを使用しており、再配布ができないため素材リンクだけを貼っておきます。AudioStockが使えない場合は、各自で似たような効果音素材を探してください。

【素材リンク】

・ゲーム開始   :https://audiostock.jp/audio/884671
・決定      :https://audiostock.jp/audio/131714
・足音      :https://audiostock.jp/audio/422259
・アイテム取得  :https://audiostock.jp/audio/66773
・アイテム切替  :https://audiostock.jp/audio/857207
・アイテム衝突音 :https://audiostock.jp/audio/838593
・ボタン入力   :https://audiostock.jp/audio/1273053

・本棚移動    :https://audiostock.jp/audio/115439
・引き戸     :https://audiostock.jp/audio/1384514
・宝箱が開く   :https://audiostock.jp/audio/18562
・正解      :https://audiostock.jp/audio/995200
・不正解     :https://audiostock.jp/audio/862353
・扉が開く    :https://audiostock.jp/audio/1239340

 後は再生したいタイミングでGameManagerのPlaySE関数でオーディオクリップを渡すだけになります。

 試しに戸棚を開く音の効果音を追加してみましょう。

​ ItemDoorスクリプトを開いて、赤い部分のコードを埋めてください。青い部分は穴埋めになっています。

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

 

public class ItemDoor : ItemObject // ① ItemObjectを継承
{
    [SerializeField]
    AudioClip OpenSE;  
// 開く音

 

    // ② ItemObjectクラスのItemCheck関数をオーバーライドする
    public override void ItemCheck()
    {
        // ③ ドアを開けるアニメーションを再生
        GetComponent<Animator>().SetTrigger("Open");

 

        // ④ 再度調べられないようにする(SetIsCheck関数)
        SetIsCheck(true);

 

        // ⑤ 効果音(OpenSE)を再生する
        (ここに入力)

    }
}

 戸棚のインスペクターにオーディオクリップを設定するパラメータが表示されるため、再生したい効果音を設定してください。

 基本的には上記の方法で効果音を鳴らすことができるため、解説は省略します。宝箱が開く音やボタンを押す音などを実装してみてください。

 ただし、アイテム操作の効果音のみ少し特殊なためここで解説します。

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

~前略~

    // 操作説明のUI
    [SerializeField]
    UI_Operation OperationUI;
    public UI_Operation GetOperationUI()
    {
        return OperationUI;
    }

 

    // 効果音
    [SerializeField]
    AudioClip ItemGetSE, ItemHitSE, SelectSE, EnterSE;
    public AudioClip GetHitSE()
    {
        return ItemHitSE;
    }
    public AudioClip GetEnterSE()
    {
        return EnterSE;
    }

 

    // アイテムを取得する
    // アイテム欄に空きがあったら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;

 

                // 効果音再生
                PlaySE(ItemGetSE);

 

                // UIを更新
                ItemUI.UpdateUI();

 

                return true;
            }

 

            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;

        // UIを更新
        ItemUI.UpdateUI();
    }

 

    // アイテムを置く
    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;

 

        // 効果音再生
        PlaySE(ItemHitSE);

 

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

        // UIを更新
        ItemUI.UpdateUI();
    }

 

    void Awake()
    {
        // UIを更新(初期化)
        ItemUI.UpdateUI();
        // ステートを更新(初期化)
        m_gameState = GameState.enGameState_Play;
    }

 

    void Update()
    {
        // プレイ中でないなら中断
        if (m_gameState != GameState.enGameState_Play)
        {
            return;
        }

 

        // 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;
            }
            // UIを更新

            ItemUI.UpdateUI();

            // 効果音再生
            PlaySE(SelectSE);

        }
        if ((Input.GetKeyDown("joystick button 5") || Input.GetKeyDown(KeyCode.Alpha2)))
        {
            SelectItemNo--;
            if (SelectItemNo < 0)
            {
                SelectItemNo = ItemID.Length - 1;
            }
            // UIを更新
            ItemUI.UpdateUI();

            // 効果音再生
            PlaySE(SelectSE);

        }

    }
 

}

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

~前略~

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

 

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

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

            // 獲得できないアイテムなので説明を表示
            m_gameManager.GetSearchUI().SearchUI_On(Explanation, true);
            m_gameManager.GetSearchUI().AutoOff();

 

            // 効果音を再生
            GameManager.PlaySE(m_gameManager.GetEnterSE());

        }

        else
        {
            // ② アイテムを取得
            // 【ヒント】ゲームマネージャーのGetItem関数を使おう

            bool isGet = m_gameManager.GetItem(ItemID);

 

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

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

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

 

                // アイテム名を削除する
                m_gameManager.GetSearchUI().SearchUI_Off();

                // Aボタン表示を暗くする
                m_gameManager.GetOperationUI().SetOperation(UI_Operation.Button.enButton_A,
                    "", false);

 

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

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

                // アイテムがいっぱいなので拾えない
                m_gameManager.GetSearchUI().SearchUI_On("これ以上アイテムを持てません!", true);
                m_gameManager.GetSearchUI().AutoOff();

                // 効果音を再生
                GameManager.PlaySE(m_gameManager.GetEnterSE());

            }

        }
    }

 

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



​~後略~

~前略~
 

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

 

    // 自分に何かが衝突した瞬間
    private void OnCollisionEnter(Collision collision)
    {
        // 自分がアイテムでないなら中断
        if (gameObject.CompareTag("Item") == false)
        {
            return;
        }

 

        // 重力適応中のみ衝突音を再生(3D)
        if (GetComponent<Rigidbody>().isKinematic == false)
        {
            GameManager.PlaySE(m_gameManager.GetHitSE(),
                gameObject,
                1.0f, 1.0f);
        }
    }


}

 衝突音のみ3Dサウンドにしています。

 GameManagerのインスペクターで必要な効果音を指定してください。

 一通りの効果音を設定したら完成です!お疲れさまでした!

 

​ 3D脱出ゲーム編ではURPを使って実践的なゲーム開発を行いました。改造は難しい部分もあると思いますが、ぜひ自力でコードを書いて自分だけのゲームに改造してみてください。まずは現在ゴールになっている扉の続きを作ったりして、コードの理解を深めましょう。

 Lesson1-1で解説した通り、URPは3Dコアと描画方式が違うため、モデルを追加したときに正常に表示されない場合があります。

​ ここではAsset StoreからダウンロードしたモデルをURPに対応させる流れを解説します。

​ まずはAsset Storeから使いたいモデルをダウンロード&インポートしてください。

 元からURPに対応している素材は普通に使えます。説明文をチェックして、あえてURPに対応していない素材を選んでください。
【サンプルで使用した素材】

https://assetstore.unity.com/packages/3d/characters/robots/robot-sphere-136226

 インポートしたモデルを表示しようとすると、ピンク一色で表示されてしまいます。

 これは古いシェーダーを使っているマテリアルを適用しているためです。URPでは古いシェーダーに対応しておらず、変換が必要になります。

 「Window」→「Rendering」→「Render Pipeline Converter」を選択してください。

 プルダウンメニューから「Built-in to URP」を選択してください。

 スクロールして「Material Upgrade」にチェックを入れたら、左下の「Initialize Converters」ボタンをクリックしてください。

 変換対象のマテリアルが表示されるので、変換したくないマテリアルがある場合はチェックを個別で外してください。

​ 確認したら右下の「Convert Assets」ボタンをクリックすると変換が完了します。

 マテリアルの変換が完了すると、ピンク一色の状態から正常なマテリアルが適用された状態になります。

 Asset StoreではアセットがURPに対応しているか確認することができます。

​ 対応していなくても上記の変換を行えば対応できますが、特殊なシェーダーを使っている場合などは変換に失敗することがあります。その場合は個別でURP用のシェーダーへ対応する必要があるので注意してください。

Unity Tips!
URP

 これでゲームは一通り完成しました。

 LessonEXでは世界を暗くしたり、敵を実装してホラーゲームに改造していきましょう。

【評価テスト】

https://forms.gle/Fm6JS5kcstiLfKsb7

評価テスト

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

bottom of page