top of page

3D脱出ゲーム編

LessonEX ホラーゲーム化しよう

EX-1 世界を暗くする

EX-1 世界を暗くする

 Lesson5で完成させたゲームをベースにホラーゲーム化しましょう。

 一人称視点のホラーゲームは簡単に臨場感を出せるので、慣れてきたころの制作にオススメです。

​※ ホラーが苦手な方は世界を明るくしたり、敵を弱くしたりすることで遊びやすいように改造してください。

 まずはシーンを暗くしましょう

​ Directional Lightを削除または非アクティブにしてください。

 しかし、ディレクションライトを削除しても世界が完全に真っ暗にはなりません。

​ これは環境光によって世界が照らされているからになります。

 環境光の影響を消して、世界を完全に暗くしましょう。

​ 「Window」→「Rendering」→「Lighting」を選択してください。

 ライティングウィンドウを開いたら、Environmentタブに切り替えてください。

 Environment LightingのIntensity Multiplierと、Environment ReflectionsのIntensity Multiplierを0にしてください。ここで環境光の強さを設定しており、0にすることで世界が完全に真っ暗になります。

​ 最後に下のGenerate Lightingボタンを押して変更を反映させてください(時間がかかる場合はステージなどの3Dモデルを一時的に非アクティブにしてから実行すると改善されることがあります)

 これで世界が完全に暗くなりました。

​ ロウソクのポイントライトの強度はお好みで調整してください。

EX-2 懐中電灯を追加する

EX-2 懐中電灯を追加する

 このままではロウソクの光が当たっていない場所が全く見えないため、プレイヤーに懐中電灯を持たせましょう。といっても、親子関係を活用するだけなので難しくありません。

​ Playerの子オブジェクトであるMain Cameraの子オブジェクトにSpot Lightを追加してください。

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

【サンプルの入力例】

Inner/Outer Spot Angle : 30/170

Intensity         : 50

Indirect Multiplier    : 0

Range          : 50

 これでスポットライトで前方を照らせるようになりました。パラメータはお好みで調整してみてください。

EX-3 UIを修正する

EX-3 UIを修正する

 ディレクションライトを消したことで、UIのアイテム表示が真っ暗になってしまう問題が発生しています。これを修正しておきましょう。

 「Light」→「Directional Light」を追加してください。

 Culling Mask ではライトの影響を受けるレイヤーを指定できます。デフォルトは「Everything」(全て)になっていますが、「UI_Item」だけに変更してください。これでUI_Itemレイヤーのみライトの影響を受けるようになります。

【サンプルの入力例】

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

Intensity : 3

3_5_b

 これでUIにのみディレクションライトが適用され、しっかり表示されるようになりました。

EX-4 敵を配置する

EX-4 敵を配置する

 いよいよ敵を実装していきます。

 必要な要素は多いですが、これが終われば3D脱出ゲーム編はほぼ終了なので頑張りましょう!

 まずは制作しやすいように事前準備を行います。

​ Ceiling(天井)を表示している場合は一時的に非アクティブにしてください。

 シーンが暗いままでは制作しにくいので、シーン内ではライトの影響を消しておきましょう。

 敵のモデルを配置しましょう。

 「Model」→「Monster_4」→「Mesh」から「sk_monster_4」をシーン上にドラッグ&ドロップしてください。

 敵のモデルが気に入らない場合は自由に差し替えてもらっても構いません。

​ モデルのAnimation TypeがHumanoidになっているモデルであれば、アニメーションも問題なく使用できるはずです(ユニティちゃんのモデルや3Dアクションゲーム編EXで変換したMMDモデルなど)

 アニメーションがうまく動作しない場合は、AnimatorコンポーネントのAvatarがモデルに対応したものになっているか確認してください。

Unity Tips!

 敵のモデルのTransformを調整してください。いきなりプレイヤーと出くわさないように、プレイヤーから遠い座標を開始位置にしています。

【サンプルの入力例】

Position  : X=146 Y=1 Z=8

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

Scale      : X=6.8 Y=6.8 Z=6.8

 後ほどレイを使ってプレイヤーを探すため、自身にレイがヒットしないようにレイヤーを「Ignore Raycast」に変更してください。

 子オブジェクトのレイヤーも変更するか確認されますが「Yes, change children」を選択してください​。

 暗闇でも敵が見えやすいように、敵の目の部分に赤いポイントライトを追加しましょう。

​ 敵のモデルの子オブジェクトから「root」→「pelvis」→「spine_01」→「spine_02」→「spine_03」→「neck_01」→「head」と開いていって、headの子オブジェクトにポイントライトを追加してください。

 ポイントライトの座標や明るさを設定しましょう。

【サンプルの入力例】

Position  : X=-0.08 Y=-0.11 Z=0.04

Color                   : 赤(R=255 G=0 B=0)

Intensity              : 10

Indirect Multiplier : 0

Range                 : 1

 先ほどのポイントライトを複製して、Z座標だけ反転させてください。

【サンプルの入力例】

Position  : X=-0.08 Y=-0.11 Z=-0.04

 これで暗闇でも敵が見えやすいようになりました。

 実際にゲームを実行してみると、敵の顔の部分が赤く見えています。このポイントライトは敵のボーンの子オブジェクトにしているため、敵がアニメーションしても問題なく目の位置を保ちます。

 次に敵のアニメーションを設定しましょう。

​ 「Animation」→「Enemy」フォルダ内にAnimator Controllerを作成してください。名前はわかりやすいようにEnemyにしておきます。

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

​ Enemyフォルダ内にあるIdle、Shout、WalkのアニメーションをAnimatorウィンドウにドラッグ&ドロップしましょう。

※ もし初期アニメーションステートがIdle以外になってしまった場合は、Idleのアニメーションステートを右クリックして「Set as Layer Default State」をクリックしてください。

 次はパラメータを作成しましょう。

 Bool型のパラメータMoveと、Trigger型のパラメータAttackを追加してください。

 最後にそれぞれのアニメーションの遷移を設定していきます。

 まずはIdle→Walk、Walk→Idle、Any State→Shoutへトランジションを繋げてください。

(トランジションの繋げ方を忘れた人は3Dアクションゲーム編1-7を参照してください)

 AnyStateはAny(どれでも)の名の通り、このステートに繋げたステートはどのステートからも遷移できるようになります。今のアニメーションがIdleアニメーションでもWalkアニメーションでも、条件さえ満たせば遷移できるということになります。

 最後にそれぞれのトランジションの設定をしていきましょう。設定項目は少なめです。

​【Idle→Walk】

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

​・Conditions(遷移条件)はMoveがtrueの時

【Walk→Idle】

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

​・Conditions(遷移条件)はMoveがfalseの時

【AnyState→Shout】

・Conditions(遷移条件)はAttackが有効な時

 これでAnimator Controllerの設定は完了です。Animatorウィンドウを閉じてください。

​ 最後に敵のAnimatorコンポーネントに、作成したAnimator Controllerを設定してください。

 これで敵の準備は一通り完了です。

​ 次のレッスンからはいよいよ敵のAIを作っていきましょう。

EX-5 敵のAI 巡回編

EX-5 敵のAI 巡回編

 ここからは敵のAIを作っていきます。

 AIを作る前に、敵のAIに求める要件をまとめておきましょう。

① 普段は指定した複数地点を巡回する(確率で巡回する順番を逆にする)

② プレイヤーを発見したらプレイヤーを追尾する

③ 追尾しているプレイヤーが視界から外れた場合は「プレイヤーを最後に見た瞬間から0.5秒後のプレイヤーの座標」を目指す

④ もし ③ の座標を目指して移動してもプレイヤーが見つからなかったら、数秒待機して ① の巡回モードに戻る

​⑤ プレイヤーが一定距離の近さになったらプレイヤーを攻撃してゲームオーバーにする

 このAIを作るためには「パス移動」と「ナビメッシュ」について理解しておく必要があります。

 パス移動はシーン上に複数の座標データを用意しておいて、それを順番に目的地にすることで移動する手法です。

 ナビメッシュはステージの中で歩行可能な場所を表す地形データです。

 Unityには床と障害物の頂点データから画像のようなナビメッシュを生成する機能があります。

 このナビメッシュを使って現在位置から目的地への経路探索を行います。

 UnityではA*(エースター)アルゴリズムという計算方法を使って「目的地への最短経路​」を求めています。

 A*アルゴリズムの仕組みを簡単に説明しておきます(あくまで概要的なものです)

 まずはナビメッシュを用意します。今回はわかりやすいように格子状にしていますが、実際は三角形を用いて細かく分けられます。

​ 水色の部分がナビメッシュで、黒い部分は障害物のためナビメッシュは生成されていません。

3_5_84

 開始地点から目的地に向かう複数のルートがありますが、どうやって最短ルートを求めるのでしょうか。

​ まずは開始地点に隣接する3つのマスと目的地の直線距離を計算します。下方向は障害物があり、ナビメッシュが生成されないため計算しません。

3_4_85

 それぞれのマスの計算結果を保存しておきます。今回は目的地との距離が10mだったため、10を保存します(距離の値はイメージです)

 ただし開始地点より上のマスは他のマスより明らかに目的地への距離が遠いため、今後の計算からは除外しておきます。

※ 正確な経路探索を行う際は除外しないのですが、今回は解説の簡略化のため除外していきます

 次は右のマスにさらに隣接するマスと目的地の距離を計算します。

 同じように計算結果を保存しておきます。

 ただし保存する計算結果は、

 (直前のマスに覚えさせておいた距離の値+現在のマスと目的地の距離)

 になります。

 直前のマスと目的地の距離は10だったため、現在のマスと目的地の距離である12に加算して22を覚えることになります。

 これを繰り返して目的地を目指します。

​ 直前のマスに覚えさせた値に加算していくため、マスに覚えさせる値もどんどん大きくなっていきます。

 計算が終わって生成されたルートのうち、最後の一番値が小さいルートが最短ルートになります。

​ これがA*アルゴリズムの基本的な考え方です(説明の簡略化のために省いた要素はあります)

 ルートが決まったら、そのルートを構成するマスをパス移動で移動すれば、障害物を迂回しつつ最短ルートで目的地に到着できる、というわけです。

​ ナビメッシュとA*アルゴリズムを用いた経路探索はk2Engineにも実装されています。興味のある人はプログラムを見たり、自力で実装したりしてみてください。

Wikipedia - A* より引用
Subh83 - 投稿者自身による著作物,

CC 表示 3.0, https://commons.wikimedia.org/w/index.php?curid=14916867 による

Unity Tips!

 今回のゲームではパス移動を行いつつ、目的地への移動はナビメッシュを使用します。単純なパス移動だけでは障害物を迂回することはできませんが、目的地への移動にナビメッシュを使うことで障害物を避けて巡回することができます。

 この2つを組み合わせることで巡回のAIを作ってみましょう。

 実装のためにはまずナビメッシュを作る必要があります。

 まずはナビメッシュ生成の対象にする地形を指定しましょう​。

 Mapオブジェクトを選択してNavigation Staticにチェックを入れてください。

 子オブジェクトも変更するかどうか確認されるので「Yes, change children」を選択してください。

 Navigation Staticに指定したオブジェクトがナビメッシュ生成の対象になります。

​ 「Window」→「AI」→「Navigation」を選択してください。

 ナビゲーションウィンドウを開いたらBakeタブに切り替えて、設定を調整してください。 

 設定を調整したら右下のBakeボタンを押してください。

【サンプルの入力例】

Agent Radius : 2.3

Agent Height : 2.6

Max Slope     : 45

Step Height   : 1

Voxel Size     : 0.08

 Bakeボタンを押すとナビメッシュが生成されます。水色になっている部分が敵が通れる場所になります。

 これでナビメッシュの準備は完了です。今後地形を変更した場合は、再度Bakeボタンを押すようにしてください。

 UnityのバージョンによってはNavigation機能がデフォルトで搭載されていない場合があるようです。​その場合はPackage Managerの検索欄に「Navigation」と入力して、AI Navigationをインストールしてください。

Unity Tips!

 次に敵を選択して必要なコンポーネントをアタッチしましょう。

​ Capsule ColliderとNav Mesh Agentをアタッチしてください。Nav Mesh Agentはナビメッシュを使って移動させる時に必要なコンポーネントです。

 それぞれのパラメータを設定してください。

【サンプルの入力例】

[Capsule Collider]

Center  : X=0 Y=1 Z=0

Radius  : 0.4

Height  : 2

[Nav Mesh Agent]

Speed(移動の最高速度)    : 6

Angular Speed(回転速度) : 800

Acceleration(加速度)   : 18

Radius                              : 0.8

Height                              : 2

 カプセルが衝突用の当たり判定で、円柱がNav Mesh Agentの計算で使用する当たり判定になります。敵の歩くアニメーションは腕を大きく振るため、腕が壁に埋まらないようにNav Mesh Agentの半径を大きめにしています。

 半径や高さの設定は使用しているモデルに合わせて調整してください。

 それでは敵のスクリプトを書いていきましょう。ここでは巡回処理だけを実装します。

​ Enemyスクリプトを作成して、以下のように入力してください。少し長いですが頑張りましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;       // NavMeshAgentを扱うときに必要

 

public class Enemy : MonoBehaviour
{
    [SerializeField]
    GameObject m_targetPlayer;

    NavMeshAgent m_agent;
    Animator m_animator;
    [SerializeField]
    AudioClip StepSE;

 

    // エネミーの状態
    enum EnemyState
    {
        enEnemyState_Search,    // 巡回
        enEnemyState_Chase,     // 追跡
        enEnemyState_Lost,      // 見失った
        enEnemyState_Attack     // 攻撃
    }
    [SerializeField]
    EnemyState m_enemyState = EnemyState.enEnemyState_Search;

 

    // 巡回
    [SerializeField]
    Vector3[] m_targetPos;
    int m_targetNum = 0;
    bool m_targetMode = false;    // 移動先の決め方(trueにすると逆になる)
    void TargetAdd(int add)
    {
        // 目的地をずらす
        if (m_targetMode)
        {
            m_targetNum -= add;
        }
        else
        {
            m_targetNum += add;
        }
        // 値をループさせる
        m_targetNum = (int)Mathf.Repeat(m_targetNum, m_targetPos.Length);
    }

 

    void Awake()
    {
        // 必要なコンポーネントを取得
        m_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();

 

        // 最初の巡回方向はランダム
        if (Random.Range(0, 2) == 0)
        {
            m_targetMode = !m_targetMode;
        }
        // 目的地を適当にずらす
        TargetAdd(Random.Range(1, 3));
        // 初期アニメーションは移動
        m_animator.SetBool("Move", true);
    }

 

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

 

        // ステートによって分岐
        switch (m_enemyState)
        {
            case EnemyState.enEnemyState_Search:
                // 巡回
                // NavMeshAgentに目的地を教える

                m_agent.SetDestination(m_targetPos[m_targetNum]);

 

                // 目的地についたので次の場所を決める
                if ((transform.position - m_targetPos[m_targetNum]).sqrMagnitude
                    <= 1.0f)
                {
                    // 1/5で巡回方向変更
                    if (Random.Range(0, 6) == 0)
                    {
                        m_targetMode = !m_targetMode;
                    }

                    int targetAdd = 1;
                    // 1/3で巡回場所を1箇所飛ばす
                    if (Random.Range(0, 4) == 0)
                    {
                        targetAdd = 2;
                    }
                    // 目的地をずらす
                    TargetAdd(targetAdd);
                }

 

                break;
            case EnemyState.enEnemyState_Chase:
                // 追跡

 

                break;
            case EnemyState.enEnemyState_Lost:
                // 見失った

 

                break;
            case EnemyState.enEnemyState_Attack:
                // 攻撃

 

                break;
        }
    }

 

    // 足音を再生
    public void PlayStepSE()
    {
        // 3Dサウンドで再生
        GameManager.PlaySE(StepSE, gameObject, 1.0f,
            1.0f, 2.0f, 6.0f);
    }
}

【プログラムの解説】

・スクリプト内でNavMeshAgentを扱う際は最初に using UnityEngine.AI; と記述する必要があります。

・enum(列挙型)で敵の状態を巡回、追跡、見失う、攻撃に分けています。ステートによる状態の管理はゲームプログラミングの教材も参考にしてみてください。

​・Mathf.Repeat関数は指定した変数を0~最大値の範囲で循環させる関数です。この関数を使って目的地の配列を順番に対象にしています。

・NavMeshAgentクラスの SetDestination関数を使うことで、NavMeshAgentに目的地を教えています。引数には目的地の座標を指定します。

 NavMeshAgentに目的地を教えたら後は自動で移動してくれるため便利です。

​・敵のWalkアニメーションにはアニメーションイベントが設定されています。これはアニメーションの指定したフレームで関数を呼び出すことができる機能です。

​ これによって足音の効果音を適切なタイミングで再生することができます。

​(アニメーションイベントについて詳しくは剣を振る/ボールを投げるで解説しています)

 ここまでできたら保存して、敵にEnemyスクリプトをアタッチしてください。

​ インスペクターにパラメータが表示されるため、それぞれ設定していきましょう。

 Target Playerにはプレイヤーを指定しておきましょう。今は使っていませんが、次のLessonでプレイヤーを追尾するときに使います。

 StepSEには足音の効果音を指定します。プレイヤーに使用している効果音でも問題ありませんが、音が軽くて違和感がある場合はお好みで差し替えてください。

※ 効果音を指定しないとエラーが出るため、効果音を鳴らしたくない場合はPlayStepSE関数の中身だけを削除しておいてください。中身はなくても関数は残してください。

 Target Posにはパス移動に必要な座標を指定してください。

 サンプルでは各部屋と廊下の角の座標を指定しています。

​【サンプルの入力例】

Element0  : X=146 Y=1 Z=8

Element1  : X=124 Y=1 Z=-40

Element2  : X=146 Y=1 Z=-94

Element3  : X=83   Y=1 Z=-128

Element4  : X=45   Y=1 Z=-108

Element5  : X=14   Y=1 Z=-94

Element6  : X=36   Y=1 Z=-38

Element7  : X=0     Y=1 Z=-7

Element8  : X=33   Y=1 Z=64

Element9  : X=14   Y=1 Z=112

Element10: X=58   Y=1 Z=127

Element11: X=115 Y=1 Z=72

 これで敵の巡回処理が完成しました。ゲームを実行して確認してみましょう。

 ゲームビューでは敵の動きを確認しにくいので、シーンビューに切り替えて上から確認してみてください。

 上から確認して、指定した地点を敵が巡回していればOKです。確率で巡回地点を1つ飛ばしたり、巡回する順番を逆にしたりすることで不規則な巡回を行っています。

 今回ナビメッシュを作成するために地形を「Navigation Static」に設定しました。

 勘のいい方はお気付きかと思いますが、これはStatic(静的な)の名の通り、地形が動かないことを前提として事前にナビメッシュを作っています

 Unityにはゲームの実行中でも動的にナビメッシュを変更するNav Mesh Obstacleというコンポーネントがあります。今回は使用しませんが「地形の形状が頻繁に変わるステージで追いかけっこをしたい!」という場合は使ってみてください。

Unity Tips!

EX-6 敵のAI 追跡編

EX-6 敵のAI 追跡編
評価テスト

 次はEX4で定義した要件のうち、

② プレイヤーを発見したらプレイヤーを追尾する

 を実装していきましょう。

 とはいえナビメッシュは既に作成しているため、必要なのはプレイヤーを発見する判定だけになります。

​ プレイヤーの発見処理は視野角とレイを用いて行います。

(なす角やレイは2Dランゲーム編1-6でも使用しています)

 敵の正面ベクトルとプレイヤーに伸びるベクトルの内積を計算し、arcos(逆余弦関数)を通すことで2つのベクトルのなす角を求めることができます。便利なことにUnityには2つのベクトルのなす角を計算してくれるVector3.Angle関数がありますので、今回はこちらを使用しましょう。

​ そして、そのなす角が一定以下なら、プレイヤーは視野角内にいると判断します​。

 Ray(レイ)は「光線」のような意味で、文字通り光線を発射して、衝突したオブジェクトの情報を取得することができる機能です。

 敵からプレイヤーに向かってレイを発射して、レイにプレイヤー以外のオブジェクトが衝突しないならプレイヤーを発見した判定になります。

​(レイについてはLessonEX「Raycast」も参考にしてください)

 敵がプレイヤーを発見する条件をまとめると、

・プレイヤーが視野角内にいる

・プレイヤーに向かってレイを飛ばして、プレイヤー以外のオブジェクトがヒットしない

 になります。

 つまり、プレイヤーが真後ろにいたり、物陰に隠れていたりする場合は発見されません。

 では、巡回中にプレイヤーが上記の条件を満たした場合、敵のステートを追跡に切り替えて、NavMeshAgentの目的地をプレイヤーの座標にしましょう。この時点では見失う処理がないため、一度見つかると永遠に追いかけてきます。

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

~前略~

    // 巡回
    [SerializeField]
    Vector3[] m_targetPos;
    int m_targetNum = 0;
    bool m_targetMode = false;    // 移動先の決め方(trueにすると逆になる)
    void TargetAdd(int add)
    {
        // 目的地をずらす
        if (m_targetMode)
        {
            m_targetNum -= add;
        }
        else
        {
            m_targetNum += add;
        }
        // 値をループさせる
        m_targetNum = (int)Mathf.Repeat(m_targetNum, m_targetPos.Length);
    }

    // 追跡
    [SerializeField]
    float m_searchAngle, m_searchRayRange, m_chaseRayRange;

 

    void Awake()
    {
        // 必要なコンポーネントを取得
        m_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();

 

        // 最初の巡回方向はランダム
        if (Random.Range(0, 2) == 0)
        {
            m_targetMode = !m_targetMode;
        }
        // 目的地を適当にずらす
        TargetAdd(Random.Range(1, 3));
        // 初期アニメーションは移動
        m_animator.SetBool("Move", true);
    }

 

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

 

        // ステートによって分岐
        switch (m_enemyState)
        {
            case EnemyState.enEnemyState_Search:
                // 巡回
                // NavMeshAgentに目的地を教える

                m_agent.SetDestination(m_targetPos[m_targetNum]);

 

                // 目的地についたので次の場所を決める
                if ((transform.position - m_targetPos[m_targetNum]).sqrMagnitude
                    <= 1.0f)
                {
                    // 1/5で巡回方向変更
                    if (Random.Range(0, 6) == 0)
                    {
                        m_targetMode = !m_targetMode;
                    }

                    int targetAdd = 1;
                    // 1/3で巡回場所を1箇所飛ばす
                    if (Random.Range(0, 4) == 0)
                    {
                        targetAdd = 2;
                    }
                    // 目的地をずらす
                    TargetAdd(targetAdd);
                }

 

                // プレイヤーを見つけた場合追跡状態に移行
                if (PlayerSearch(m_searchRayRange))
                {
                    StateChange(EnemyState.enEnemyState_Chase);
                }

                break;
            case EnemyState.enEnemyState_Chase:
                // 追跡

                // プレイヤーの座標へ向かう
                m_agent.SetDestination(m_targetPlayer.transform.position);

                break;
            case EnemyState.enEnemyState_Lost:
                // 見失った

 

                break;
            case EnemyState.enEnemyState_Attack:
                // 攻撃

 

                break;
        }
    }

 

    // プレイヤーを探す 見つけたらtrueを返す
    bool PlayerSearch(float rayRange)
    {

        // レイの始点を計算
        Vector3 startPos = transform.position;
        startPos.y += 10.0f;

        // プレイヤーへ伸びるベクトルを
計算
        Vector3 diff = m_targetPlayer.transform.position - startPos;

 

        // レイを描画
        Debug.DrawRay(startPos, diff.normalized * rayRange, Color.red, 0.1f);

 

        // レイを発射
        RaycastHit hit;
        if (Physics.Raycast(startPos, diff.normalized, out hit, rayRange))
        {

            // プレイヤーが視野角内かつレイが最初にヒットしたのがプレイヤーだったら…
            if (Vector3.Angle(transform.forward, diff) <= m_searchAngle
                && hit.collider.CompareTag("Player"))
            {

                // プレイヤー発見
                return true;
            }
        }
        return false;
    }

 

    // ステート切替
    void StateChange(EnemyState state)
    {
        m_enemyState = state;

        // 切り替えた瞬間に行う処理
        switch (state)
        {
            case EnemyState.enEnemyState_Search:
                Debug.Log("巡回開始");
                m_animator.SetBool("Move", true);

 

                break;

            case EnemyState.enEnemyState_Chase:
                Debug.Log("プレイヤー発見");
                m_animator.SetBool("Move", true);

 

                break;

            case EnemyState.enEnemyState_Lost:
                Debug.Log("プレイヤーを見失った");

                break;

            case EnemyState.enEnemyState_Attack:
                Debug.Log("捕まった");

                break;
        }
    }

 

    // 足音を再生
    public void PlayStepSE()
    {
        // 3Dサウンドで再生
        GameManager.PlaySE(StepSE, gameObject, 1.0f,
            1.0f, 2.0f, 6.0f);
    }
}

【プログラムの解説】

​・Debug.DrawRay 関数はシーン上にレイを描画する関数です。本来レイは透明なので見えませんが、デバッグ用にレイを描画したいときに使います。

 第一引数にレイの始点、第二引数にレイのベクトル、第三引数に色、第四引数に表示時間を指定します。
・Vector3.Angle 関数は第一引数と第二引数のベクトルのなす角を返す関数です。

 これでプレイヤーの発見、追跡処理が完成しました。

​ Enemyコンポーネントにパラメータが追加されているので設定しましょう。

 Search Angleは視野角です。サンプルでは少しだけ後ろも見えるように100にしています。値を小さくすると視野が狭くなるため、難易度が下がります。

 Search Ray Rangeは巡回中のレイの長さを指定しています。サンプルでは60にしています。

 Chase Ray Rangeは追跡中のレイの長さを指定しています。サンプルでは120にしています。追跡中はプレイヤーを追いかけるため、巡回中よりレイの長さを伸ばして執念深さを発揮するように(?)しています。

 ゲームを実行して、プレイヤーが敵の前方に立つと「プレイヤー発見」​のログが出力されると同時に追跡が始まることを確認してみてください。現在は追跡を終了する処理がないためどこまでも追いかけてきますが、次のLessonで修正します。

 シーンビューでは Debug.DrawRay関数で描画したレイが表示されています。敵からプレイヤーに向かって伸びていることを確認してみてください。

EX-7 敵のAI 見失う編

EX-7 敵のAI 見失う編

 次はEX4で定義した要件のうち、

③ 追尾しているプレイヤーが視界から外れた場合は「プレイヤーを最後に見た瞬間から0.5秒後のプレイヤーの座標」を目指す

④ もし ③ の座標を目指して移動してもプレイヤーが見つからなかったら、数秒待機して ① の巡回モードに戻る

 の2つを実装していきましょう。

 まずは③を実装していきます。

 「プレイヤーを最後に見た瞬間から0.5秒後のプレイヤーの座標」を目指すことで、プレイヤーを見失ってもプレイヤーのいる場所を推測して追いかけている雰囲気を出しています。

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

~前略~

        // 値をループさせる
        m_targetNum = (int)Mathf.Repeat(m_targetNum, m_targetPos.Length);
    }

    // 追跡
    [SerializeField]
    float m_searchAngle, m_searchRayRange, m_chaseRayRange;
    // 見失う

    // 追跡状態で見失ってからこの秒数後のプレイヤーの座標を目指す
    const float PLAYER_FINAN_POSITION_LIMIT = 0.5f;
    float m_playerFinalPosTimer = PLAYER_FINAN_POSITION_LIMIT;  
// 見失ってからのタイマー
    Vector3 m_playerFinalPosition;     // プレイヤーを見失ってから目指す座標
    bool m_isPlayerFinalPosSet = false; // 最後に目指す座標をセット済みかどうか

 

    void Awake()
    {
        // 必要なコンポーネントを取得
        m_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();

​~後略~

~前略~
 

                // プレイヤーを見つけた場合追跡状態に移行
                if (PlayerSearch(m_searchRayRange))
                {
                    StateChange(EnemyState.enEnemyState_Chase);
                }

                break;

            case EnemyState.enEnemyState_Chase:
                // 追跡
                if (PlayerSearch(m_chaseRayRange))
                {
                    // プレイヤーが見えるならプレイヤーへ向かう
                    m_agent.SetDestination(m_targetPlayer.transform.position);
                    // 見失った時の準備
                    m_playerFinalPosTimer = PLAYER_FINAN_POSITION_LIMIT;
                    m_isPlayerFinalPosSet = false;
                }
                else
                {
                    // プレイヤーが見えない
                    if (m_isPlayerFinalPosSet)
                    {
                        // プレイヤーを最後に見た瞬間から一定秒数経過したプレイヤーの場所を目指す
                        m_agent.SetDestination(m_playerFinalPosition);
                        // 目的地についた
                        if ((transform.position - m_playerFinalPosition).sqrMagnitude <= 1.0f)
                        {
                            StateChange(EnemyState.enEnemyState_Lost);
                        }
                    }
                    else
                    {
                        // この時点では引き続きプレイヤーを目指す
                        m_agent.SetDestination(m_targetPlayer.transform.position);
                        // タイマー減少
                        m_playerFinalPosTimer -= Time.deltaTime;
                        if (m_playerFinalPosTimer <= 0.0f)
                        {
                            // プレイヤーの最終座標を記憶
                            m_isPlayerFinalPosSet = true;
                            m_playerFinalPosition = m_targetPlayer.transform.position;
                            // Y座標だけ同じにしておく
                            m_playerFinalPosition.y = transform.position.y;
                        }
                    }
                }

                break;

            case EnemyState.enEnemyState_Lost:
                // 見失った


​~後略~

 このコードには特に新しい要素はありません。

​ 敵は追跡中にプレイヤーを見つけられなくなったら、PLAYER_FINAN_POSITION_LIMITで指定した秒数後のプレイヤーの座標を目指すようになります。それでもプレイヤーを見つけられなかった場合は、敵のステートをenEnemyState_Lost(見失った)に切り替えています。

 現在は見失った先の処理がありませんが、これで見失う処理は完成です。ゲームを実行して確認してみてください。

​ 敵に見つかった後、敵の廊下の角を使って敵の視界から外れて、近くの部屋に逃げてみてください。「プレイヤーを見失った」というログが出力されたらOKです。

 次に④を実装していきます。

 「もし ③ の座標を目指して移動してもプレイヤーが見つからなかったら、数秒待機して ① の巡回モードに戻る」の前半部分は既に実装したため「数秒待機して巡回モードに戻る」処理を実装しましょう。

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

~前略~
 

        // 値をループさせる
        m_targetNum = (int)Mathf.Repeat(m_targetNum, m_targetPos.Length);
    }

    // 追跡
    [SerializeField]
    float m_searchAngle, m_searchRayRange, m_chaseRayRange;
    // 見失う
    // 追跡状態で見失ってからこの秒数後のプレイヤーの座標を目指す

    const float PLAYER_FINAN_POSITION_LIMIT = 0.5f;
    float m_playerFinalPosTimer = PLAYER_FINAN_POSITION_LIMIT;   // 見失ってからのタイマー
    Vector3 m_playerFinalPosition;     // プレイヤーを見失ってから目指す座標
    bool m_isPlayerFinalPosSet = false; // 最後に目指す座標をセット済みかどうか
    // 巡回へ復帰
    const float PLAYER_RETURN_LIMIT = 3.0f;
    float m_playerReturnTimer = PLAYER_RETURN_LIMIT;

 

    void Awake()
    {
        // 必要なコンポーネントを取得
        m_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();

​~後略~

~前略~
 

                        // タイマー減少
                        m_playerFinalPosTimer -= Time.deltaTime;
                        if (m_playerFinalPosTimer <= 0.0f)
                        {
                            // プレイヤーの最終座標を記憶
                            m_isPlayerFinalPosSet = true;
                            m_playerFinalPosition = m_targetPlayer.transform.position;
                            // Y座標だけ同じにしておく
                            m_playerFinalPosition.y = transform.position.y;
                        }
                    }
                }

                break;

            case EnemyState.enEnemyState_Lost:
                // 見失った
                m_playerReturnTimer -= Time.deltaTime;

 

                if (PlayerSearch(m_searchRayRange))
                {
                    // 再発見
                    StateChange(EnemyState.enEnemyState_Chase);
                }
                if (m_playerReturnTimer <= 0.0f)
                {
                    // 巡回モードへ戻る
                    StateChange(EnemyState.enEnemyState_Search);
                }

                break;

            case EnemyState.enEnemyState_Attack:
                // 攻撃

 

                break;
        }
    }


​~後略~

~前略~
 

    // ステート切替
    void StateChange(EnemyState state)
    {
        m_enemyState = state;

 

        // 切り替えた瞬間に行う処理
        switch (state)
        {
            case EnemyState.enEnemyState_Search:
                Debug.Log("巡回開始");
                m_animator.SetBool("Move", true);

                break;
 

            case EnemyState.enEnemyState_Chase:
                Debug.Log("プレイヤー発見");
                m_animator.SetBool("Move", true);

                break;
 

            case EnemyState.enEnemyState_Lost:
                Debug.Log("プレイヤーを見失った");

                m_animator.SetBool("Move", false);
                m_agent.ResetPath();
                m_playerReturnTimer = PLAYER_RETURN_LIMIT;
                
                // 次の巡回先を決める
                // 一番近い巡回先の番号を取得

                m_targetNum = NextPoint();
                // 乱数を加算して次の巡回先にランダム性を持たせる
                TargetAdd(Random.Range(-3, 3));

                break;
 

            case EnemyState.enEnemyState_Attack:
                Debug.Log("捕まった");

                break;
        }
    }

 

    // 一番近い巡回地点の番号を返す
    int NextPoint()
    {
        int num = -1;
        float numRange = float.MaxValue;
        float range;

        for (int i = 0; i < m_targetPos.Length; i++)
        {
            // 保存中の距離より近いかチェック
            range = (m_targetPos[i] - transform.position).sqrMagnitude;
            if (range < numRange)
            {
                num = i;
                numRange = range;
            }
        }

        return num;
    }

 

    // 足音を再生
    public void PlayStepSE()
    {
        // 3Dサウンドで再生
        GameManager.PlaySE(StepSE, gameObject, 1.0f,
            1.0f, 2.0f, 6.0f);
    }
}

 プレイヤーを完全に見失った場合、一番近い巡回地点を基準にランダムで適当な場所を巡回対象にするようにしています。

​ これで攻撃(ゲームオーバー)以外の処理は一通り完成です。ゲームを実行して敵から逃げ切った際に「巡回開始」のログが表示されることを確認してみてください。

EX-8 敵のAI 攻撃編

EX-8 敵のAI 攻撃編

 いよいよ最後です!最後はEX4で定義した要件のうち、

⑤ プレイヤーが一定距離の近さになったらプレイヤーを攻撃してゲームオーバーにする

 を実装していきましょう。

 最後の要件はシンプルですが、突然ゲームオーバーになってしまってはプレイヤーが混乱するため、しっかり捕まったことがわかるように演出を入れていきます。

​ まずはゲームオーバー用のシーンを作りましょう。基本の流れはクリアシーンと同じです。

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

 GameOverシーンを開いたらゲームオーバー画面を作ってみましょう。思いつかない人はClearシーンからCanvas、EventSystem、Clearを複製して編集してもOKです。

 ホラーゲームのゲームオーバーシーンはシンプルにしておくとプレイヤーの想像力を掻き立てるようになるため、サンプルではシンプルなデザインにしています。

 「CanvasとClearオブジェクトが必要なのはわかるけどEventSystemって何?」と思う人もいるかもしれません。

 EventSystem ​クリックなどのユーザーからの入力を受け取って、ButtonといったUIにイベントを通知するという、中継地点のような役割を果たすオブジェクトです。つまりこのオブジェクトがないと、UIがクリックされた瞬間を検知できないということになります。EventSystemは基本的にUI(Canvas)を作成した際に自動で作成されます。

 このシーンのUIにButtonはありませんが、後でButtonを追加した際に動作しないといった問題が発生しないように前もって用意しました。

​(EventSystemの公式マニュアル

3_5_97
Unity Tips!

 Clearシーンと同じようにこのシーンで決定キーを押すとTitleシーンに切り替わるようにしておきましょう。

​ それでは敵の攻撃処理を実装します。

​ まずはGameManagerスクリプトを開いて、GameStateにゲームオーバーを追加しておきましょう。赤い部分のコードを追加してください。

~前略~
 

        return oneShotAudio;
    }

 

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

 

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


​~後略~

 プレイヤーとの距離が近くなったら敵が攻撃して、ゲームオーバーになる演出を追加します。

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

~前略~
 

    // 巡回へ復帰
    const float PLAYER_RETURN_LIMIT = 3.0f;
    float m_playerReturnTimer = PLAYER_RETURN_LIMIT;
    // 攻撃
    const float GAMEOVER_RANGE = 20.0f;     // プレイヤーを捕まえる距離
    [SerializeField]
    GameObject m_fadeCanvas;

 

    void Awake()
    {
        // 必要なコンポーネントを取得
        m_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();

​~後略~

~前略~
 

                    else
                    {
                        // この時点では引き続きプレイヤーを目指す
                        m_agent.SetDestination(m_targetPlayer.transform.position);

                        // タイマー減少
                        m_playerFinalPosTimer -= Time.deltaTime;
                        if (m_playerFinalPosTimer <= 0.0f)
                        {
                            // プレイヤーの最終座標を記憶
                            m_isPlayerFinalPosSet = true;
                            m_playerFinalPosition = m_targetPlayer.transform.position;
                            // Y座標だけ同じにしておく
                            m_playerFinalPosition.y = transform.position.y;
                        }
                    }
                }

 

                // プレイヤーとの距離を調べる(Y座標は無視)
                Vector3 playerPos = m_targetPlayer.transform.position;
                playerPos.y = transform.position.y;
                // 距離が近くなったら攻撃(ゲームオーバー)
                if ((transform.position - playerPos).sqrMagnitude <= GAMEOVER_RANGE)
                {
                    StateChange(EnemyState.enEnemyState_Attack);
                }

                break;

            case EnemyState.enEnemyState_Lost:
                // 見失った
                m_playerReturnTimer -= Time.deltaTime;

                if (PlayerSearch(m_searchRayRange))
                {
                    // 再発見
                    StateChange(EnemyState.enEnemyState_Chase);
                }

​~後略~

~前略~
 

                // 次の巡回先を決める
                // 一番近い巡回先の番号を取得

                m_targetNum = NextPoint();
                // 乱数を加算して次の巡回先にランダム性を持たせる
                TargetAdd(Random.Range(-3, 3));

 

                break;
 

            case EnemyState.enEnemyState_Attack:
                Debug.Log("捕まった");

                m_animator.SetTrigger("Attack");
                m_agent.ResetPath();

 

                // ゲームマネージャーを取得
                GameManager gameManager =

                GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
                gameManager.SetGameState(GameManager.GameState.enGameState_GameOver);

                // 自分はプレイヤーを見る
                Vector3 playerPos = m_targetPlayer.transform.position;
                playerPos.y = transform.position.y;
                transform.LookAt(playerPos);
                // カメラはエネミーを見る
                Vector3 targetPos = transform.position;
                targetPos.y = 9.0f;
                Camera.main.GetComponent<GameCamera>().FocusStart(targetPos, 3.0f, 2.0f);


                // 自分の当たり判定は演出に邪魔なので消しておく
                GetComponent<CapsuleCollider>().enabled = false;
                // プレイヤーに力がかかっていたら止める
                m_targetPlayer.GetComponent<Rigidbody>().velocity = Vector3.zero;

                // シーン切替
                // フェード演出用オブジェクトを生成

                GameObject fadeObject = Instantiate(m_fadeCanvas);
                // 生成したオブジェクトのFadeStart関数を呼び出す
                fadeObject.GetComponent<FadeScene>().FadeStart("GameOver", Color.black, true);

                break;
        }
    }

    // 一番近い巡回地点の番号を返す
    int NextPoint()
    {
        int num = -1;
        float numRange = float.MaxValue;
        float range;

​~後略~

​ カメラが敵の方を向く処理は、Lesson5-5で実装した扉の方を向く処理を流用しています。

 このままゲームを実行すると、捕まった瞬間にエラーが出てしまいます。エラーを見て適切な修正を行い、敵の処理を完成させましょう!

 ようやく敵の処理が一通り完成しました。

​ 敵に追いつかれるとゲームオーバーになることを確認してみてください。

EX-9 演出を追加する

 敵に追いかけられている状態かどうかプレイヤーに伝わりにくいため、追いかけられている間は緊迫感のあるBGMを再生して、画面が赤くなるようにしてみましょう。

 画面を赤くするためにはポストエフェクトの操作が必要です。

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

 

public class GameEffect : MonoBehaviour
{
    [SerializeField]
    Volume GlobalVolume;
    ColorAdjustments m_colorAdjustments;

 

    // 画面
    Color m_changeColor, m_startColor;
    float m_changeTimer = 0.0f;
    bool m_isChangeColor = false;

 

    // BGM
    AudioSource m_AudioBGM;
    float m_changeVolume, m_startVolume;
    float m_changeVolumeTimer = 0.0f;
    bool m_isChangeVolume = false;

 

    void Awake()
    {
        // ColorAdjustmentsを取得
        GlobalVolume.profile.TryGet(out m_colorAdjustments);

 

        // 自身にアタッチされたオーディオソースを取得
        m_AudioBGM = GetComponent<AudioSource>();
        m_AudioBGM.volume = 0.0f;  // 最初は無音
    }

 

    void Update()
    {
        // 画面の色を変更
        if (m_isChangeColor)
        {
            // 色を設定
            m_colorAdjustments.colorFilter
                .Override(Color.Lerp(m_startColor, m_changeColor, m_changeTimer));

 

            m_changeTimer += Time.deltaTime;
            if (m_changeTimer >= 1.0f)
            {
                m_isChangeColor = false;
            }
        }

 

        // BGM音量を変更
        if (m_isChangeVolume)
        {
            // 音量を設定
            m_AudioBGM.volume = Mathf.Lerp(m_startVolume, m_changeVolume, m_changeVolumeTimer);

 

            m_changeVolumeTimer += Time.deltaTime;
            if (m_changeVolumeTimer >= 1.0f)
            {
                m_isChangeVolume = false;
            }
        }

    }
 

    // 引数に指定した色に画面の色を緩やかに変化させる
    public void StartColorChange(Color changeColor)
    {
        // 変更前の色と変更後の色を保存
        m_startColor = ((Color)m_colorAdjustments.colorFilter);
        m_changeColor = changeColor;

        m_changeTimer = 0.0f;
        m_isChangeColor = true;
    }

 

    // 引数に指定した音量に緩やかに変化させる
    public void StartBGMVolume(float volume)
    {
        m_changeVolume = volume;
        m_startVolume = m_AudioBGM.volume;

 

        // BGMを最初から再生する
        if (volume != 0.0f)
        {
            m_AudioBGM.Play();
        }

 

        m_changeVolumeTimer = 0.0f;
        m_isChangeVolume = true;
    }

 

    // BGMを止める
    public void BGMStop()
    {
        m_AudioBGM.Stop();
    }

}

EX-9 演出を追加する

【プログラムの解説】

​・ポストプロセスをスクリプトで扱う場合は少し特殊で、
 using UnityEngine.Rendering;
 using UnityEngine.Rendering.Universal;

をまず最初に記述する必要があります。

​ さらに、エフェクトを格納するためのメンバ変数を用意する必要があります(BloomやDepthOfFieldなど。今回はColorAdjustmentsです)

 最後に対象のVolume​からTryGet関数を呼びだして、必要なエフェクトを取得します。

 他のコンポーネントより手順が複雑なので注意しましょう。

​・ColorクラスのLeap関数は今まで使ってきたMathf.Leap関数と使い方は同じです。第一引数に補間前の値、第二引数に補間後の値、第三引数に補間率を指定する点も変わりません。

​ スクリプトが書けたら保存して、GameManagerオブジェクトにAudioSourceとGameEffectコンポーネントをアタッチしてください。

 GameEffectコンポーネントにはGlobal Volumeを指定して、AudioSourceには追跡中のBGMを設定してください(下記のURLからダウンロードするか、お好きな素材を用意してください)

​ BGM素材はストリーミング再生に変更しておきます。

 最初からBGMが再生されないようにPlay On Awakeのチェックを外して、Loopにはチェックを入れておきましょう。

【追跡中のBGM素材】https://audiostock.jp/audio/474965

 後はGameEffectスクリプトで指定した関数を適切なタイミングで呼び出すだけです。

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;       // NavMeshAgentを扱うときに必要

 

public class Enemy : MonoBehaviour
{
    [SerializeField]
    GameObject m_targetPlayer;
    NavMeshAgent m_agent;
    Animator m_animator;
    [SerializeField]
    AudioClip StepSE;

 

    // ゲームエフェクト
    GameEffect m_gameEffect;
 

    // エネミーの状態
    enum EnemyState
    {
        enEnemyState_Search,    // 巡回
        enEnemyState_Chase,     // 追跡
        enEnemyState_Lost,      // 見失った
        enEnemyState_Attack     // 攻撃
    }
    [SerializeField]
    EnemyState m_enemyState = EnemyState.enEnemyState_Search;


​~後略~

~前略~
 

    // 攻撃
    const float GAMEOVER_RANGE = 20.0f;     // プレイヤーを捕まえる距離
    [SerializeField]
    GameObject m_fadeCanvas;

 

    void Awake()
    {
        // 必要なコンポーネントを取得
        m_agent = GetComponent<NavMeshAgent>();
        m_animator = GetComponent<Animator>();
        m_gameEffect = GameObject.FindGameObjectWithTag("GameController").
            GetComponent<GameEffect>();

 

        // 最初の巡回方向はランダム
        if (Random.Range(0, 2) == 0)
        {
            m_targetMode = !m_targetMode;
        }

​~後略~

~前略~
 

    // ステート切替
    void StateChange(EnemyState state)
    {
        m_enemyState = state;

 

        // 切り替えた瞬間に行う処理
        switch (state)
        {
            case EnemyState.enEnemyState_Search:
                Debug.Log("巡回開始");
                m_animator.SetBool("Move", true);

                break;
 

            case EnemyState.enEnemyState_Chase:
                Debug.Log("プレイヤー発見");
                m_animator.SetBool("Move", true);
                m_gameEffect.StartColorChange(Color.red);     // 画面の色を変更
                m_gameEffect.StartBGMVolume(1.0f);

                break;
 

            case EnemyState.enEnemyState_Lost:
                Debug.Log("プレイヤーを見失った");

                m_animator.SetBool("Move", false);
                m_agent.ResetPath();
                m_playerReturnTimer = PLAYER_RETURN_LIMIT;
                m_gameEffect.StartColorChange(Color.white);   // 画面の色を変更
                m_gameEffect.StartBGMVolume(0.0f);

 

                // 次の巡回先を決める
                // 一番近い巡回先の番号を取得

                m_targetNum = NextPoint();
                // 乱数を加算して次の巡回先にランダム性を持たせる
                TargetAdd(Random.Range(-3, 3));

                break;
 

            case EnemyState.enEnemyState_Attack:
                Debug.Log("捕まった");

                m_animator.SetTrigger("Attack");
                m_agent.ResetPath();

 

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

 

                // 自分はプレイヤーを見る
                Vector3 playerPos = m_targetPlayer.transform.position;
                playerPos.y = transform.position.y;
                transform.LookAt(playerPos);

                // カメラはエネミーを見る
                Vector3 targetPos = transform.position;
                targetPos.y = 9.0f;
                Camera.main.GetComponent<GameCamera>().FocusStart(targetPos, 3.0f, 2.0f);

                // 自分の当たり判定は演出に邪魔なので消しておく
                GetComponent<CapsuleCollider>().enabled = false;
                // プレイヤーに力がかかっていたら止める
                m_targetPlayer.GetComponent<Rigidbody>().velocity = Vector3.zero;

 

                // シーン切替
                // フェード演出用オブジェクトを生成

                GameObject fadeObject = Instantiate(m_fadeCanvas);
                // 生成したオブジェクトのFadeStart関数を呼び出す
                fadeObject.GetComponent<FadeScene>().FadeStart("GameOver", Color.black, true);

 

                // BGMを止める
                m_gameEffect.BGMStop();

                break;
        }
    }

 

    // 一番近い巡回地点の番号を返す
    int NextPoint()
    {

​~後略~

 これで追跡中の演出が追加されました。

​ 敵に追いかけられている間は画面が赤くなり、BGMが流れるようになっていることを確認してください。

 些細なことですがTitleシーンが昼のように明るいため、ディレクションライトのColorを調整して雰囲気を調整しておいてください。

 色は単純な黒より、青みががった色の方が夜の雰囲気が出ます。

Unity Tips!

 これで3D脱出ゲーム編のゲームは完成です!!お疲れさまでした!!

EX-10 最後に

EX-10 最後に

 3D脱出ゲーム編では継承やナビメッシュなど今まで解説していなかった要素と、3Dアクションゲーム編の要素を組み合わせたゲームを作成しました。少々複雑なので改造は難しいかもしれませんが、別シーンを作って新しいステージやギミックを作ってみるなど、簡単な拡張にも挑戦してみてください。

 継承やナビメッシュは脱出ゲームに限らず様々なジャンルに使えます。今回の内容を参考にして、様々なゲームを作ってみてください!

【評価テスト】

https://forms.gle/gj5ugcoLZaZaQZmQ7

評価テスト

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

bottom of page