U
タブ 02キャラクターと戦闘9 レッスン — タップで開く
08 / 09 · 13 分
キャラクターと戦闘

追ってきて反撃する敵

ここまでのプロジェクト

Assets/

Scenes/

Arena.unity

KnightDummy ×3→Enemy ×3 (chases and hits back)

Scripts/

SkillCaster.csHealthBar.csEnemyBrain.cs

Prefabs/

Enemy.prefab

プロジェクトの設定

Enemy layerObstacle layerNavMesh bake

すでにあるものこのレッスンで追加このレッスンで置き換え

前レッスンの Dummy はダメージを受けて死ぬだけで、できることはそれだけです。動かず、反撃せず、負かしてもくれません。今は負けようがない——つまりゲームにもなっていません。

このレッスンでそれを立たせます。同じカプセル、同じ CharacterStats に、新しい行動を 3 つ:あなたに気づく、あなたへ歩いてくる、あなたを叩く。

アニメーション図

距離がすべてを決めます:遠ければ巡回、リングの内側なら追跡、手の届く範囲なら一撃です。

ステップ 1 —— 3 つの状態、1 つの数値

これだけの広さのアリーナに、パスファインディングのシステムは不要です。距離を 1 回測れば状態が決まり、状態が行動を決めます。脳みそはそれで全部です。

EnemyBrain.cs
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class EnemyBrain : MonoBehaviour
{
    public Transform target;          // the Knight
    public float aggroRange = 9f;
    public float attackRange = 1.8f;  // stop and swing
    public float speed = 2.6f;
    public float damage = 12f;
    public float attackEvery = 1.2f;

    CharacterController cc;
    float nextAttack;

    void Awake() => cc = GetComponent<CharacterController>();

    void Update()
    {
        if (target == null) return;

        // Flatten Y: a target standing on a block is still "close" horizontally.
        Vector3 flat = target.position - transform.position;
        flat.y = 0f;
        float distance = flat.magnitude;

        if (distance > aggroRange) return;              // Patrol: do nothing yet

        transform.rotation = Quaternion.LookRotation(flat);

        if (distance > attackRange)                     // Chase
        {
            cc.Move(flat.normalized * speed * Time.deltaTime);
        }
        else if (Time.time >= nextAttack)               // Attack
        {
            nextAttack = Time.time + attackEvery;
            target.GetComponent<CharacterStats>().TakeDamage(damage);
        }

        cc.Move(Vector3.down * 9.8f * Time.deltaTime);  // stay on the floor
    }
}
  • flat.y = 0f こそ重要な一行です。これがないと、ブロックの上に立つ Knight は「遠い」と判定され、あなたがジャンプした瞬間に敵の興味が切れます。

  • nextAttack はプレイヤーのスキルと同じクールダウンの型です——敵もあなたのルールで動きます。

  • CharacterStats はすでにあなたが書いたスクリプトです。敵はあなたに TakeDamage を呼び、スキルは敵にそれを呼びます。スクリプト 1 つで双方向。

ステップ 2 —— シーンでつなぐ

  1. 1

    Dummy を選択します。前レッスンの CharacterStats と Enemy レイヤーがそのままあるので、両方残してください。

  2. 2

    Character Controller を追加し、次に EnemyBrain を追加します。Knight を Target フィールドへドラッグしてください。

  3. 3

    Enemy にリネームして、Assets/Prefabs へドラッグします。ここからは Dummy ではなく敵を置くことになります。

  4. 4

    ブロックの回りに 3 体を違う距離に配置し、Play を押して 1 体へ歩いて向かいます。

ステップ 3 —— 敵を倒せるようにする

スキルは Enemy レイヤーの何にでもダメージを出せるので、この半分はほぼ無料です。新しいのは HP が 0 になった瞬間のことだけで、CharacterStats はすでにそれを知らせてくれます——ヘルスバーのときに OnDied を追加しましたよね。敵はその知らせを聞いて自分を消すだけです。

EnemyBrain.cs — replace Awake with this
CharacterStats stats;

void Awake()
{
    cc = GetComponent<CharacterController>();
    stats = GetComponent<CharacterStats>();

    // The Knight has CharacterStats too, so that script must never destroy
    // anything itself. The enemy listens for its own death instead.
    stats.OnDied += () => Destroy(gameObject);
}

ステップ 4 —— ブロックは攻撃を止めるはず

今のアリーナには飛び乗れるブロックがありますが、戦闘側の誰もその存在を知りません。flat.y = 0f は高さを捨てているので、床の敵は 1 m 上に立つ Knight へ、ブロックを貫いて一撃を振ります。

2 つの質問で直りますが、意図的に分けています:そこまで攻撃が届くか、間に何もないか。壁は、両者とも床に立っているときでさえ 2 つ目の質問で通しません。

  1. 1

    Obstacle レイヤーはすでにあります——スキルがブロックを貫通して飛ぶのを止めたときに作って、P_Cube も既に入っています。敵も同じレイヤーを読みます。

  2. 2

    Enemy の prefab で名前を右クリック → Create Empty、Eye と名付け、Position を 0, 1.4, 0 に設定します。敵がそこから見ている位置です。

  3. 3

    下のコードを EnemyBrain に追加し、Inspector で Eye を Eye フィールドへドラッグ、Obstacles マスクでは Obstacle だけにチェックを入れます。

add to EnemyBrain.cs — what stops an attack
[Header("Cover & height")]
public LayerMask obstacles;          // the Obstacle layer: blocks, walls
public float reachHeight = 1.2f;     // how far above itself it can still swing
public Transform eye;                // an empty at head height

// A straight line from its eye to your chest. Anything on the Obstacle
// layer in the way means it has no shot.
bool CanSee()
{
    Vector3 chest = target.position + Vector3.up;
    return !Physics.Linecast(eye.position, chest, obstacles);
}

// You standing on a block are out of swinging range, even if the
// flattened distance says you are right next to it.
bool CanReach()
{
    return target.position.y - transform.position.y <= reachHeight;
}

そして攻撃の行が、振る前に 2 つの質問を両方確かめます。Update のそれ以外は変えません。

the attack line, now with two conditions
else if (Time.time >= nextAttack && CanReach() && CanSee())
{
    nextAttack = Time.time + attackEvery;
    target.GetComponent<CharacterStats>().TakeDamage(damage);
}

ステップ 5 —— 経路を組み立てる脳

追跡はまだ直進するだけです。敵と Knight の間にブロックを置くと、側面に歩いて行って張り付き、絶対に抜けられない壁にひたすら前へ押し付けたままになります。回り道があることを誰も教えていないので、遠回りもしません。

Unity に経路計算を任せられます。歩ける床を 1 回 Bake すれば、NavMeshAgent がその地図に沿って、ブロックを回り、隙間を抜けて、あなたまでやってきます。

  1. 1

    まず Window▸AI▸Navigation を開きます。タブが見当たらなければ、Window▸Package Manager から AI Navigation パッケージをインストールしてください。

  2. 2

    Floor と親の Blocks を選び、Inspector の右上にある Navigation Static にチェックを入れます。「ここは地図の一部ですよ」と伝えるのがこの項目です。

  3. 3

    Navigation ウィンドウで Bake タブ → Bake。床に青い膜が張られ、ブロックの手前で止まります——この青い場所が、敵の歩ける場所です。

  4. 4

    Enemy の prefab では Character Controller を外して、Nav Mesh Agent を追加します。移動は agent が担当になるので、cc.Move の行もそのままお役御免です。

  5. 5

    agent の Radius を 0.4 くらい、Height を 2 に設定します。二つのブロックの間に通れる幅になり、隙間を拒まなくなります。

EnemyBrain.cs — chasing with a NavMeshAgent
using UnityEngine;
using UnityEngine.AI;                       // NavMeshAgent lives here

[RequireComponent(typeof(NavMeshAgent))]
public class EnemyBrain : MonoBehaviour
{
    public Transform target;
    public float aggroRange = 9f;
    public float attackRange = 1.8f;
    public float speed = 2.6f;

    NavMeshAgent agent;

    void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.speed = speed;
        agent.stoppingDistance = attackRange * 0.9f;   // stop before it hugs you
    }

    void Update()
    {
        if (target == null) return;

        Vector3 flat = target.position - transform.position;
        flat.y = 0f;
        float distance = flat.magnitude;

        if (distance > aggroRange)
        {
            agent.ResetPath();                         // lost interest
            return;
        }

        if (distance > attackRange)
            agent.SetDestination(target.position);     // it works out the route
        else
            agent.ResetPath();                         // close enough: stand and swing
    }
}
  • SetDestination が要求の全部です:点を渡せば、経路はあちらが見つけてくれます。パスファインディングを自分で書くことは二度とありません。

  • stoppingDistance は、振る前にあなたへ重なるまで歩いていくのを防ぎ、これまで追跡を止めていた距離チェックの役割もこちらが引き継ぎます。

  • スクリプトから重力が消えたのは、agent が Bake 済みの面上に自分で留まるからです。agent はマップの外へ落ちないため、あなたがブロックへ飛び乗ってもついて来られません。

  • CanSee と CanReach は一切変えずにそのまま残します。パスファインディングが決めるのはあなたにどう辿り着くかだけで、当てられるかどうかは相変わらずこの 2 つが決めます。

✖ Chasing with transform.position +=

敵が、まさにあなたが飛び乗るあのブロックにめり込み、そのままその中に立ち尽くします。

✓ Chasing with CharacterController.Move

ブロックに止められて通り抜けはしなくなり——NavMeshAgent になってからは回り道をするので、ブロックへ飛び乗る価値が生まれます。