Skip to main content

ゾンビを実装

主人公を追いかける「ゾンビ」を実現しましょう!

シーンの設定

ゲームオブジェクトと3Dモデル

Timmyと同様に、ゾンビのゲームオブジェクトを作成しましょう。空のゲームオブジェクトを作成し、その下に子オブジェクトとしてZombieのモデルを追加する:

image.png

ただし、ゾンビは物理的に動かないので、コライダーとリジッドボディが要らない。ゾンビの動かした方については、下記で説明する。

アニメーション

今回は、アニメーションは1つしかなくて、ずっと歩くようにしましょう。ただし、Timmy と同様に「アバター」の設定が必要。

image.png

image.png

そして、ゾンビのアニメーターコントローラーを作成し、「ZombieWalk」のアニメションをデフォルトにする

image.png

これで実行してみてください(ループを忘れずに~)

自動的に道を探す「ナビゲーション」

人間を制御するキャラクターが、当たり判定を確認するだけで、動きの実装ができる。人間が勝手に障害物などを避けて動く。一方、CPUキャラクターがとても馬鹿であり、障害物を避けたらすることができない。

この問題を解決するには、Unityが「Navigation」(ナビゲーション)を提供する。ナビゲーションとは、自動車のナビと同じように、目的地を設定し、最適なルートを探す処理である。

ナビゲーションは以下の主なコンポーネントを組み合わせて、作成する

  • ナビゲーションメッシュ(NavMesh Surface):ナビの「地図」歩けるところ、歩けないところを指定する
  • エージェント(NavMesh Agent):地図の上に動くもの。一般的に、CPUキャラクターである

その他にも、ナビゲーションの影響にあるコンポーネント(NavMesh Link、NavMesh Obstacle、など)もあるが、最低限でこの2つの組み合わせで十分。

ナビゲーションメッシュ(NavMesh Surface)

ナビの地図である。地図を作るのは、シーンを固定してから!シーンが変わったら(壁を増やした、障害物の位置を変えた)、地図も更新しないといけないので、ご注意ください。

まず、地図に使うオブジェクトをグループ化にしないといけないので、シーンを整理し、ゾンビと主人公の間にいくつかの壁を作ってみましょう。空ゲームオブジェクト「Stage」を作成し、中には床面、ランプそして新しい壁を追加してください。

image.png

ここで「ナビゲーションメッシュ」を作りましょう。「Stage」を選択し、「NavMesh Surface」を追加してください

image.png

ここで地図を作る時に必要なパラメータを設定できるが、最も重要なのは、何を基づいて地図を作るのか。これは「Object Collection」から設定ができる。今回は「Stageとその子オブジェクトを使用」にしたいので「Current Object Hierarchy」(現在オブジェクトのヒエラルキー)を選択しましょう。

そして、ナビゲーションメッシュを作成するには「Bake」(焼く)ボタンを押すだけ。このボタンを押すと、Stageとその子オブジェクトのすべてを処理し、歩ける「地図」を水色で表示される:

image.png

壁にあまり近づかないよう、周囲に「歩けない」領域が現れる。この地図が焼いているので、ステージが変わっても地図が変わらない! 1つの壁を削除しても、「歩けない」領域が残る:

image.png

ナビゲーションメッシュを焼いた後にシーンが変わったら、もう一度「Bake」してください。

エージェント(NavMesh Agent)

エージェントは、水色の「地図」(ナビゲーションメッシュ)上で歩くものである。目的地を設定すれば、現在地から最適な道を探し、自動的に動く。

今回のゲームで、ゾンビが動いてほしいので、エージェントにしましょう。ゾンビ(親オブジェクト)に「NavMesh Agent」を追加してください。

image.png

ここでエージェントのパラメータを調整ができる。主なパラメータは:

Base Offset(基礎オフセット) 足元からの「ずれ」
Speed(速度) 移動速度
Angular Speed(回転速度) 向きを変える速度
Acceleration(加速度) 加速と減速の速さ
Stopping Distance(停止距離) 目的地までこの距離になったら止まる
Radius(半径) エージェントの「太さ」
Height(高さ) エージェントの「身長」

とりあえず、ゾンビの速度を「1」にし、残りのパラメータをそのままにしましょう。

スクリプト

プレーヤーを追いかける

スクリプトは、プレーヤーの位置を求め、エージェントの目的地を設定するだけ。「PlayerChase」のスクリプトは以下の通りである:

using UnityEngine;
using UnityEngine.AI;

// プレーヤーを追いかける
public class PlayerChase : MonoBehaviour
{
    // 追いかけるプレーヤー
    private GameObject player;
    
    // エージェントの目的地を設定するため
    private NavMeshAgent agent;
    
    private void Start()
    {
        // 後ほどプレハブにするので、Unityで紐づけせず、
        // 実行中にプレーヤーを探す
        player = GameObject.Find("Player");

        // NavMeshAgentのコンポネント取得
        agent = GetComponent<NavMeshAgent>();
    }

    private void Update()
    {
        // 追いかける
        // SetDestination はエージェントの目的地を設定するメソッド。
        // 引数はその位置である(プレーヤーの位置)
        agent.SetDestination(player.transform.position);
    }
}

このスクリプトを Zombie にアッタッチし、実行してみてください。

Timmyの体力

これで、追いかけるようになったが、ゾンビが主人公を捕まっても、なにもならない。Timmyの体力を実装しましょう。「HitPoints」のスクリプトは以下の通りである:

// 体力を管理するクラス
public class HitPoints : MonoBehaviour
{
    // 体力
    private int hp = 3;
}

当たり判定とダメージ

ここでトリガーを活用ができる。もし、HitPoints がダメージの起源(ゾンビ、罠、炎など)と重なったら、ダメージを受ける。どれぐらい受けるのかをその「ダメージの起源」(DamageSource)で指定ができる

image.png

まず、ダメージの起源を作成しましょう:

// ダメージの起源
public class DamageSource : MonoBehaviour
{
    // ダメージの量
    [SerializeField] 
    private int damage;
    
    // ダメージの量(読み込む専用)
    public int GetDamage()
    {
        return damage;
    }
}

そして、HitPoints スクリプトは:

// 体力を管理するクラス
public class HitPoints : MonoBehaviour
{
    // 体力
    private int hp = 3;

    // トリガーに入ったら
    private void OnTriggerEnter(Collider other)
    {
        // ダメージの起源かどうかを確認
        DamageSource src = other.GetComponent<DamageSource>();
        if (src == null)
            return;
      
        // 体力を減らし、0 以下にならないように制限する
        hp = Mathf.Max(0, hp - src.GetDamage());
    }
}

確認

確認するには、ゾンビにダメージの起源を追加しなければならない。Zombieに新しい子オブジェクトを追加し、トリガーと DamageSource を追加してください

image.png

そして、Player に HitPoints を追加し、Debug.Log で体力を表示してください。

UIの更新(体力)

Debug.Logでカッコ悪いので、UIも作成しましょう。画面の左に、3つのハート画像をグループとして配置しましょう

image.png

そして、UIManager を更新し、ハートの表示・非表示しましょう。

// UIを管理する
public class UIManager : MonoBehaviour
{
    // コインのテキスト
    [SerializeField] 
    private TextMeshProUGUI coinText;
    
    // 鍵のテキスト
    [SerializeField] 
    private TextMeshProUGUI keyText;

    // ハートの配列
    [SerializeField] 
    private GameObject [] hearts;

    // 表示したいハートの数
    public void SetHearts(int count)
    {
        for(int i = 0; i < hearts.Length; i++)
            hearts[i].SetActive(i < count);
    }

    // (省略)
}

SetHeartsブロック崩しゲームの「SetLives」と同じ処理であるが、書き方を短くしただけである。後は、HitPoints の体力が変わったら、UI を更新するにで、 HitPoints を修正しましょう:

using UnityEngine;

// 体力を管理するクラス
public class HitPoints : MonoBehaviour
{
    // 体力を表示するため
    [SerializeField] 
    private UIManager uiManager;
    
    // 体力
    private int hp = 3;

    private void Start()
    {
        uiManager.SetHearts(hp);
    }

    // トリガーに入ったら
    private void OnTriggerEnter(Collider other)
    {
        // ダメージの起源かどうかを確認
        DamageSource src = other.GetComponent<DamageSource>();
        if (src == null)
            return;
      
        // 体力を減らし、0 以下にならないように制限する
        hp = Mathf.Max(0, hp - src.GetDamage());
        uiManager.SetHearts(hp);
    }
}