会追击并反击的敌人
Assets/
Scenes/
Arena.unity
Scripts/
Prefabs/
项目设置
你已经有的这一课新增这一课替换
上一课的 Dummy 只会挨打和死去,仅此而已。它不移动、不还手、也永远不会让你输。现在你根本输不了 —— 所以还算不上游戏。
这一课让它站起来。还是那个 capsule、那套 CharacterStats,新增三种行为:发现你、走向你、打你。
距离决定一切:离得远就巡逻,进了圈就追击,到了跟前就抡你。
第 1 步 —— 三个状态,一个数字
竞技场就这么大,还轮不上寻路系统出场。一次距离检测选出状态,状态再选出行为。它的脑子就这么多内容。
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;你的技能对它调用同一个。一个脚本,两个方向。
第 2 步 —— 在场景里接好
- 1
选中 Dummy。它从前一课起就有 CharacterStats 和 Enemy 层 —— 两个都保留。
- 2
添加 Character Controller,再添加 EnemyBrain。把 Knight 拖进 Target 字段。
- 3
把它重命名为 Enemy 并拖进 Assets/Prefabs。从此你放的是敌人,不是假人。
- 4
在方块阵周围放下三个,距离各不相同。按 Play,朝其中一个走过去。
第 3 步 —— 让敌人死得掉
你的技能本来就能对 Enemy 层上的任何东西造成伤害,所以这一半几乎白送。唯一的新内容是血量归零那一刻 —— 而 CharacterStats 早就在播报了:你做血条的时候加过 OnDied。敌人只要听着,然后自我删除。
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 把高度丢了,所以地板上的敌人会隔着实心的方块,朝头顶一米上的 Knight 抡拳。
两个问题就能修好,而且是故意分开的:它打得到那么高吗?中间有没有东西挡着?哪怕你俩都站在地板上,一堵墙卡的也是第二个问题。
- 1
Obstacle 层早就存在:你当初阻止技能隔着方块打人时建的,P_Cube 也已经在里面。敌人读的还是同一个层。
- 2
在 Enemy prefab 上右键它的名字 → Create Empty,命名为 Eye,Position 设成 0, 1.4, 0。这就是敌人看东西的起点。
- 3
把下面的代码加进 EnemyBrain,然后在 Inspector 里把 Eye 拖进 Eye 字段,Obstacles 遮罩里只勾 Obstacle。
[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;
}然后攻击那行在出手前把两个问题都问一遍。Update 里其余的一律不动。
else if (Time.time >= nextAttack && CanReach() && CanSee())
{
nextAttack = Time.time + attackEvery;
target.GetComponent<CharacterStats>().TakeDamage(damage);
}第 5 步 —— 会自己找路的脑子
追击仍然是一条直线。在敌人和 Knight 之间放个方块,它就会顶在方块侧面不动了,对着永远穿不过的墙一个劲往前挤。它不绕路,因为没人告诉过它有路可绕。
Unity 能替它把路算好。把可行走的地板 Bake 一次,NavMeshAgent 就照着那张地图走:绕过方块、穿过缺口、来到你面前。
- 1
打开 。找不到这个 tab 的话,就到 里安装 AI Navigation 包。
- 2
选中 Floor 和父物体 Blocks,在 Inspector 右上角勾上 Navigation Static。它就是在声明“这块属于地图”。
- 3
在 Navigation 窗口里,Bake 标签页 → Bake。地板上覆起一层蓝色、到方块前停住 —— 蓝色之处就是敌人能走的地方。
- 4
在 Enemy prefab 上移除 Character Controller,添加 Nav Mesh Agent。移动从此归 agent 管,旧的那几行 cc.Move 也就一起退役了。
- 5
把 agent 的 Radius 设成 0.4 左右、Height 设成 2,这样它挤得进两个方块之间,而不是嫌缝太窄不肯走。
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 不会一直贴到你身上才出手,它也替代了原先用来停下追击的距离检测。
重力从脚本里消失了,因为 agent 自己会待在 Bake 好的表面上。agent 掉不出地图之外 —— 这也是它不会跟你跳上方块的原因。
CanSee 和 CanReach 原样保留。寻路只决定它怎么走到你跟前;能不能打到你,仍旧由这两个说了算。
✖ Chasing with transform.position +=
✓ Chasing with CharacterController.Move