1. 项目概述与核心目标在上一集我们搭建了基础的战斗框架并实现了第一个敌人“小爬虫”之后很多朋友反馈说感觉游戏世界开始有“生气”了但距离《空洞骑士》那种层层叠叠、充满未知威胁的地下王国还差得很远。确实一个优秀的银河恶魔城游戏其敌人生态的丰富性和层次感是构建沉浸感的关键。这一集我们的核心目标就是让这个寂静的“遗忘十字路”真正“活”起来。我们将不再满足于单一的敌人类型而是要系统地设计并实现一个多样化的敌人阵容包括近战、远程、飞行单位并引入更复杂的AI行为比如巡逻、追击、攻击冷却和简单的状态机。这不仅仅是复制粘贴几个Prefab那么简单它涉及到敌人系统的架构设计、行为逻辑的解耦以及如何高效地管理和迭代这些内容。如果你已经跟着做到了第三集那么这一集将是你的游戏从“玩具”迈向“作品”的关键一步。2. 敌人系统架构设计与思路拆解在开始制作具体敌人之前我们必须先搭建一个健壮、可扩展的敌人系统架构。盲目地为每个敌人都从头编写脚本会导致代码冗余、难以维护后期添加新敌人会变成一场灾难。我们的设计思路是采用面向对象编程中的继承与组件模式。2.1 基类EnemyController的设计我们将创建一个抽象的EnemyController基类它定义了所有敌人的共性生命值、伤害、移动速度、被攻击后的无敌时间、死亡效果等。更重要的是它封装了敌人的核心状态如 idle, patrol, chase, attack和状态切换的逻辑。子类如MeleeEnemy,RangedEnemy,FlyingEnemy将继承这个基类并重写或扩展其特定的行为方法。public abstract class EnemyController : MonoBehaviour, IDamageable { [Header(基本属性)] [SerializeField] protected int maxHealth 3; [SerializeField] protected int contactDamage 1; [SerializeField] protected float moveSpeed 2f; [SerializeField] protected float chaseSpeedMultiplier 1.5f; [SerializeField] protected float attackCooldown 1.5f; [SerializeField] protected float invincibleDuration 0.5f; protected int currentHealth; protected bool isInvincible false; protected float currentAttackCooldown; protected Transform playerTransform; protected Rigidbody2D rb; protected Animator animator; protected SpriteRenderer spriteRenderer; protected enum EnemyState { Idle, Patrol, Chase, Attack, Hurt, Dead } protected EnemyState currentState EnemyState.Patrol; protected virtual void Start() { currentHealth maxHealth; rb GetComponentRigidbody2D(); animator GetComponentAnimator(); spriteRenderer GetComponentSpriteRenderer(); playerTransform GameObject.FindGameObjectWithTag(Player).transform; } protected virtual void Update() { UpdateStateMachine(); HandleCooldowns(); } protected virtual void UpdateStateMachine() { // 状态机逻辑由子类具体实现 switch (currentState) { case EnemyState.Patrol: PatrolBehavior(); break; case EnemyState.Chase: ChaseBehavior(); break; // ... 其他状态 } } public virtual void TakeDamage(int damage, Vector2 hitDirection) { if (isInvincible || currentState EnemyState.Dead) return; currentHealth - damage; // 触发受伤动画、击退、无敌帧 StartCoroutine(InvincibleFlash()); if (currentHealth 0) Die(); else ChangeState(EnemyState.Hurt); } protected virtual void Die() { ChangeState(EnemyState.Dead); // 播放死亡动画、生成魂粒、销毁对象 Destroy(gameObject, 0.8f); } protected abstract void PatrolBehavior(); protected abstract void ChaseBehavior(); // ... 其他抽象行为方法 }这个基类提供了骨架具体的“血肉”如何巡逻、如何追击则由子类去填充。这样做的好处是当我们想新增一个“会跳跃的近战敌人”时只需要创建一个继承自MeleeEnemy的新类并重写其ChaseBehavior加入跳跃逻辑即可基础的生命、伤害、状态管理完全不用操心。2.2 视觉与物理资源的准备在动手写代码前我们需要准备好敌人的“皮囊”和“骨骼”。对于2D游戏通常需要一个Sprite图片和一个Animator Controller动画控制器。你可以使用Aseprite等像素画工具绘制或者在Asset Store寻找风格一致的免费素材。为每个敌人创建独立的动画状态机包含Idle、Walk、Attack、Hurt、Death等状态。物理方面为敌人Prefab添加Rigidbody2D刚体和Collider2D碰撞体。对于地面敌人Rigidbody2D的Body Type通常设为Dynamic并冻结Z轴旋转对于飞行敌人可能需要调整重力缩放甚至设为Kinematic以获得更精确的控制。注意碰撞体的形状和大小需要仔细调试。一个常见的坑是碰撞体比视觉精灵大一圈导致玩家在视觉上“没碰到”却受伤了这非常影响体验。建议将碰撞体调试得略小于视觉精灵。3. 三类典型敌人的实现与核心细节接下来我们将实现《空洞骑士》中三种最具代表性的敌人类型并深入每个的实现细节。3.1 近战敌人盔甲兵Armored Squit这是一种会沿着平台边缘巡逻发现玩家后加速冲撞的敌人。它体现了基础近战AI的典型逻辑。实现步骤创建子类创建MeleeEnemy_ArmoredSquit脚本继承自EnemyController。巡逻逻辑在PatrolBehavior中我们需要让敌人在两个点之间来回移动并在到达边缘或墙壁时转身。这里不能仅仅依赖移动必须结合射线检测。[Header(巡逻设置)] [SerializeField] private Transform leftPoint; [SerializeField] private Transform rightPoint; [SerializeField] private LayerMask groundLayer; [SerializeField] private float groundCheckDistance 0.5f; [SerializeField] private float wallCheckDistance 0.2f; private bool movingRight true; private Transform currentTarget; protected override void PatrolBehavior() { // 确定移动方向 Vector2 moveDirection movingRight ? Vector2.right : Vector2.left; rb.velocity new Vector2(moveDirection.x * moveSpeed, rb.velocity.y); // 前方地面检测 Vector2 groundCheckPos (Vector2)transform.position new Vector2((movingRight ? 0.5f : -0.5f), -0.5f); bool isGroundAhead Physics2D.Raycast(groundCheckPos, Vector2.down, groundCheckDistance, groundLayer); // 前方墙壁检测 Vector2 wallCheckPos (Vector2)transform.position new Vector2((movingRight ? 0.5f : -0.5f), 0); bool isWallAhead Physics2D.Raycast(wallCheckPos, moveDirection, wallCheckDistance, groundLayer); // 如果前方没有地面或者有墙壁则转身 if (!isGroundAhead || isWallAhead) { TurnAround(); } // 玩家检测切换至追击状态 if (PlayerInSight()) ChangeState(EnemyState.Chase); } private void TurnAround() { movingRight !movingRight; spriteRenderer.flipX !spriteRenderer.flipX; }追击与攻击逻辑在ChaseBehavior中一旦发现玩家就朝玩家方向加速移动。当进入攻击范围例如与玩家距离小于1个单位且攻击冷却完毕时切换到攻击状态播放攻击动画并在动画的关键帧触发一个伤害判定框Attack Hitbox。[Header(攻击设置)] [SerializeField] private float attackRange 1f; [SerializeField] private GameObject attackHitbox; // 一个子物体带碰撞体默认禁用 protected override void ChaseBehavior() { Vector2 directionToPlayer (playerTransform.position - transform.position).normalized; rb.velocity new Vector2(directionToPlayer.x * moveSpeed * chaseSpeedMultiplier, rb.velocity.y); // 面向玩家 if (directionToPlayer.x 0 !spriteRenderer.flipX) TurnAround(); else if (directionToPlayer.x 0 spriteRenderer.flipX) TurnAround(); // 进入攻击范围且冷却完毕 float distanceToPlayer Vector2.Distance(transform.position, playerTransform.position); if (distanceToPlayer attackRange currentAttackCooldown 0) { ChangeState(EnemyState.Attack); } // 玩家跑出追击范围返回巡逻 else if (distanceToPlayer 10f) { ChangeState(EnemyState.Patrol); } } // 由动画事件调用 public void ActivateAttackHitbox() { attackHitbox.SetActive(true); // 0.1秒后关闭模拟一次攻击挥击 Invoke(nameof(DeactivateAttackHitbox), 0.1f); }实操心得攻击伤害判定不要写在Update里持续检测。最佳实践是通过动画事件Animation Event在攻击动作的特定帧如武器挥到最高点时激活一个短暂的Collider2D即Hitbox这个碰撞体上带有一个DamageOnTrigger脚本。这样能完美匹配动画节奏手感更真实。3.2 远程敌人投矛菌Spear Fungus这是一种固定在墙上或地面周期性向玩家发射投射物的敌人。它引入了新的概念预制体实例化和抛物线运动。实现步骤创建子类创建RangedEnemy_SpearFungus脚本同样继承自EnemyController。由于其固定不动可以重写Start方法将Rigidbody2D的Body Type设为Kinematic或直接移除移动相关逻辑。攻击逻辑它的PatrolBehavior可能就是静止。核心在AttackBehavior中。我们需要一个计时器每隔一段时间攻击间隔就执行一次射击。[Header(远程攻击设置)] [SerializeField] private GameObject projectilePrefab; [SerializeField] private Transform firePoint; [SerializeField] private float projectileSpeed 8f; [SerializeField] private float attackInterval 2.5f; private float attackTimer; protected override void Update() { base.Update(); // 依然调用基类的Update来处理状态机和冷却 if (currentState EnemyState.Patrol || currentState EnemyState.Chase) { attackTimer - Time.deltaTime; if (attackTimer 0 PlayerInSight()) { ChangeState(EnemyState.Attack); } } } protected override void AttackBehavior() { // 播放蓄力或瞄准动画 animator.SetTrigger(Attack); // 计算射击方向简单的直线瞄准 Vector2 fireDirection (playerTransform.position - firePoint.position).normalized; // 实例化投射物 GameObject projectile Instantiate(projectilePrefab, firePoint.position, Quaternion.identity); ProjectileController projCtrl projectile.GetComponentProjectileController(); if (projCtrl ! null) { projCtrl.Initialize(fireDirection, projectileSpeed, contactDamage); } // 重置攻击计时器并回到空闲/巡逻状态 attackTimer attackInterval; ChangeState(EnemyState.Patrol); }投射物控制器我们需要一个独立的ProjectileController脚本挂在投射物Prefab上负责其移动、碰撞检测和生命周期。public class ProjectileController : MonoBehaviour { public Vector2 direction; public float speed; public int damage; private Rigidbody2D rb; void Start() { rb GetComponentRigidbody2D(); // 让投射物朝向移动方向 float angle Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; transform.rotation Quaternion.AngleAxis(angle, Vector3.forward); // 设置速度 rb.velocity direction * speed; // 3秒后自动销毁防止堆积 Destroy(gameObject, 3f); } void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(Player)) { IDamageable damageable other.GetComponentIDamageable(); damageable?.TakeDamage(damage, direction); } // 碰到墙壁、地面也销毁 if (!other.isTrigger !other.CompareTag(Enemy)) { Destroy(gameObject); } } }注意事项投射物的碰撞体一定要设置为Is Trigger。它的移动通过刚体速度 (rb.velocity) 控制这样性能更好且能正确触发OnTriggerEnter2D事件。务必为投射物设置一个自动销毁时间这是防止内存泄漏和性能问题的关键。3.3 飞行敌人躁动躯壳Vengefly这是一种在空中飞行发现玩家后呈波浪形或直线冲撞的敌人。它的实现挑战在于脱离地面的移动逻辑和更灵活的转向。实现步骤创建子类创建FlyingEnemy_Vengefly脚本。将Rigidbody2D的Gravity Scale设为0使其完全不受重力影响。移动逻辑飞行敌人的移动通常更“柔和”我们可以使用Vector2.MoveTowards或Vector2.SmoothDamp来模拟平滑的飞行轨迹而不是直接设置速度。[Header(飞行移动设置)] [SerializeField] private float smoothTime 0.3f; // 平滑阻尼时间 [SerializeField] private float patrolHeight 3f; // 巡逻高度 [SerializeField] private float sinWaveAmplitude 0.5f; // 正弦波幅度 [SerializeField] private float sinWaveFrequency 2f; // 正弦波频率 private Vector2 currentVelocity; protected override void PatrolBehavior() { // 在固定高度做水平正弦波飞行 float targetX movingRight ? rightPoint.position.x : leftPoint.position.x; Vector2 targetPosition new Vector2(targetX, patrolHeight Mathf.Sin(Time.time * sinWaveFrequency) * sinWaveAmplitude); Vector2 newPosition Vector2.SmoothDamp(transform.position, targetPosition, ref currentVelocity, smoothTime, moveSpeed); rb.MovePosition(newPosition); // 到达水平目标点附近则转身 if (Mathf.Abs(transform.position.x - targetX) 0.1f) { TurnAround(); } } protected override void ChaseBehavior() { // 直接平滑地飞向玩家当前位置 Vector2 targetPosition playerTransform.position; Vector2 newPosition Vector2.SmoothDamp(transform.position, targetPosition, ref currentVelocity, smoothTime, moveSpeed * chaseSpeedMultiplier); rb.MovePosition(newPosition); // 进入攻击范围 if (Vector2.Distance(transform.position, playerTransform.position) attackRange) { ChangeState(EnemyState.Attack); } }使用Rigidbody2D.MovePosition可以在保持物理交互如碰撞的同时进行平滑的位置插值比直接修改Transform.position更优。攻击模式飞行敌人的攻击可以是简单的冲撞类似近战也可以设计为“俯冲-拉起”的循环。可以在攻击状态中用协程Coroutine来控制这个复杂的轨迹。protected override void AttackBehavior() { StartCoroutine(DiveAttackCoroutine()); } private IEnumerator DiveAttackCoroutine() { // 1. 短暂蓄力播放动画 animator.SetTrigger(WindUp); yield return new WaitForSeconds(0.3f); // 2. 高速冲向玩家预测位置 Vector2 diveTarget (Vector2)playerTransform.position playerTransform.GetComponentRigidbody2D().velocity * 0.2f; // 简单预测 float diveSpeed moveSpeed * 3f; while (Vector2.Distance(transform.position, diveTarget) 0.2f) { transform.position Vector2.MoveTowards(transform.position, diveTarget, diveSpeed * Time.deltaTime); yield return null; } // 3. 攻击判定在冲撞过程中持续激活的Hitbox // 4. 拉起并返回巡逻状态 ChangeState(EnemyState.Patrol); }4. 敌人配置、管理与场景放置制作好Prefab后我们需要一种系统化的方式来配置和管理它们。4.1 使用ScriptableObject进行数据配置为每种敌人类型创建一个EnemyDataScriptableObject将生命、伤害、速度等数值属性存储在其中。这样策划或者你自己可以在不修改代码的情况下调整游戏平衡。[CreateAssetMenu(fileName NewEnemyData, menuName Enemy/Enemy Data)] public class EnemyData : ScriptableObject { public string enemyName; public int maxHealth; public int contactDamage; public float moveSpeed; public float chaseSpeedMultiplier; public Sprite defaultSprite; // 可以添加掉落物概率、魂量等更多数据 }然后在EnemyController中引用这个EnemyData在Start时用其数据初始化属性。这实现了数据与逻辑的分离。4.2 场景中的智能放置与测试将制作好的敌人Prefab拖入场景。利用Unity的“预制体模式”进行微调非常高效在场景中选中一个Prefab实例点击“Open Prefab”可以直接编辑这个Prefab所有实例都会同步更新。为每个敌人设置好巡逻点leftPoint,rightPoint这些点可以是场景中的空物体。测试策略不要一次性放置大量敌人。先单独测试每个敌人的所有行为巡逻转向是否正常追击范围是否合理攻击判定框是否准确投射物能否被玩家击打如果设计有的话确认无误后再考虑敌人的组合放置。例如在一个狭窄通道放置一个远程敌人在其前方平台放置一个近战敌人可以形成天然的“交叉火力”增加关卡挑战性。踩坑记录敌人AI的感知范围如PlayerInSight()函数一定要结合射线检测Raycast检查玩家和敌人之间是否有墙壁等障碍物遮挡。否则会出现“隔墙索敌”的诡异情况。同时感知范围不宜过大否则玩家一进入场景就会被所有敌人“围观”体验很差。5. 性能优化与常见问题排查敌人数量增多后性能问题会开始凸显。以下是几个关键的优化点和排查清单。5.1 性能优化要点减少不必要的Update调用如果敌人处于“死亡”或“非激活”状态应该禁用其AI脚本或整个GameObject。可以使用对象池Object Pooling来管理敌人的生成与回收尤其是对于频繁出现的杂兵和投射物这能极大减少实例化和垃圾回收GC的开销。简化物理计算确保敌人的碰撞体尽可能使用简单的形状如BoxCollider2D, CapsuleCollider2D避免使用复杂的PolygonCollider2D除非必要。对于不会移动的远程敌人可以考虑将其Rigidbody2D设为Static或Kinematic。控制AI更新频率不是所有敌人都需要每帧更新其AI决策。对于距离玩家很远的敌人可以降低其状态检测的频率例如每0.5秒检查一次这被称为“AI LOD”Level of Detail。使用Animator的Culling Mode将远处敌人的Animator组件的Culling Mode设置为“Cull Update Transforms”或“Cull Completely”当敌人不在摄像机视野内时Unity会停止更新其动画节省CPU开销。5.2 常见问题与解决方案速查表问题现象可能原因解决方案敌人卡在墙角或穿透地面碰撞体形状/大小不合适移动速度过快导致“隧道效应”。调整碰撞体尺寸在移动代码中使用Raycast或Physics2D.Cast进行预碰撞检测或使用Rigidbody2D.Cast方法。敌人不攻击或攻击频率异常攻击冷却计时器逻辑错误状态切换条件冲突。在Update中正确递减冷却时间currentAttackCooldown - Time.deltaTime;确保状态切换逻辑互斥避免在攻击动画中又被其他条件切走。多个敌人重叠在一起“鬼畜”抖动刚体相互碰撞推挤。为敌人图层Layer设置不同的物理碰撞矩阵使敌人之间不会发生物理碰撞但触发检测依然有效。在Edit - Project Settings - Physics 2D中调整。敌人死亡后还能攻击或移动死亡状态逻辑不完整未禁用相关组件。在Die()方法中除了播放动画还要立即禁用AI脚本 (this.enabled false)、碰撞体 (GetComponentCollider2D().enabled false) 或刚体 (rb.simulated false)。投射物无法击中玩家投射物的碰撞体不是Trigger玩家图层被投射物的碰撞矩阵忽略。确认投射物碰撞体勾选了Is Trigger检查Physics 2D设置中投射物图层和玩家图层是否启用了碰撞。敌人AI导致游戏帧率下降敌人数量过多AI计算过于复杂每帧进行了昂贵的操作如FindGameObjectWithTag。实施AI LOD将玩家引用在Start时缓存而非每帧查找使用对象池使用性能分析器Profiler定位热点。6. 敌人行为扩展与高级技巧基础的三类敌人实现后你可以尝试组合和扩展创造出更有趣的敌人行为。1. 状态机可视化与调试使用Animator的Parameters来控制敌人的状态是一种方法但对于复杂AI使用专门的有限状态机FSM插件如PlayMaker、NodeCanvas或自己编写一个简单的状态机类会让逻辑更清晰也便于调试。你可以在敌人头顶用Gizmos绘制当前状态文本调试时一目了然。2. 行为树Behavior Tree入门对于拥有多种行为模式、需要根据环境动态决策的精英怪或Boss行为树是比状态机更强大的工具。Unity的Package Manager中提供了实验性的Unity.BehaviourTree包或者可以使用Asset Store中的成熟解决方案。它通过树状节点序列、选择、条件、动作等来组织AI逻辑可读性和可维护性极高。3. 与环境交互的敌人让敌人不仅仅是移动和攻击。例如制作一个会投掷爆炸桶的敌人爆炸桶可以炸毁场景中特定的脆弱墙壁需要你为墙壁添加可被特定攻击破坏的标签和逻辑。或者制作一个受到攻击后会召唤小怪的“母体”敌人。这些交互能极大丰富游戏玩法和关卡设计空间。4. 敌人的“个性”与音效不要忘记为敌人添加音效。移动时的脚步声、发现玩家时的警觉音、攻击时的吼叫、受伤的哀鸣、死亡时的消散声。这些音频反馈是塑造敌人性格、提升游戏沉浸感不可或缺的一环。使用AudioSource组件并根据敌人状态在代码中触发相应的PlayOneShot()。走到这一步你的游戏世界已经充满了各种挑战。但一个只有敌人的世界是单调的。在下一集中我们将把目光从“战斗”暂时移开去构建这个世界的另一大支柱交互与环境。我们将制作可破坏的隐藏墙壁、需要特定能力才能开启的机关门、以及记录进度的长椅存档点。这些元素将把一个个战斗房间串联成一个引人探索的、有机的整体。