Unity第三人称射击游戏敌人攻击模块化方案优化咨询
我正在开发大学课程的Unity 3D第三人称射击游戏项目,计划对所有敌人单位(后续扩展到玩家)的攻击系统进行模块化设计。由于游戏中所有敌人均为预生成,我计划在每个敌人预制体中初始化其攻击逻辑。目前在完成逻辑编写与调试前,希望确认当前实现方案是否合理,或是否存在更高效的优化方式。
我创建了一个可序列化的父类Attack,包含virtual类型的DoAttack()方法,通过继承该类创建了三个子类,分别重写DoAttack()以实现不同攻击行为:
[System.Serializable] public class Attack { public string Name; public float AttackScaling; public int StartupTime; public int ActiveTime; public int RecoveryTime; public float MovementForce; public float InterruptValue; public virtual void DoAttack() { } } public class HitboxAttack : Attack { public Collider attackCollider; public override void DoAttack() { //wait in StartupTime seconds //activate attackCollider for ActiveTime seconds //wait in RecoveryTime seconds } } public class ProjectileAttack : Attack { public GameObject projectile; public float verticalForce; public float horizontalForce; public override void DoAttack() { //wait in StartupTime seconds //instantiate projectile using verticalForce and horizontalForce //destroy projectile after hit or after ActiveTime seconds //wait in RecoveryTime seconds } } public class CollisionAttack : Attack { public Collider unitCollider; public float CollisionSpeed; public override void DoAttack() { //wait in StartupTime seconds //charge at player at CollisionSpeed speed using collider for ActiveTime seconds //wait in RecoverTime seconds } }
此外,我还创建了AttackSequence类作为攻击容器,用于容纳多段攻击,它包含一个Attack对象的List,并通过方法遍历列表执行每个Attack的DoAttack():
[System.Serializable] public class AttackSequence { public float ActivationRange; public List<Attack> Sequence = new(); public void DoSequence() { //iterate through each attack in Sequence and call DoAttack() } }
最后,每个敌人单位挂载EnemyUnit脚本,其中包含AttackSequence对象的List,以及选择攻击序列的逻辑(仅展示相关代码):
using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class EnemyUnit : MonoBehaviour { [SerializeField] private Transform Player; public NavMeshAgent Agent; public List<AttackSequence> Attacks; [Header("Settings")] public LayerMask PlayerLayer; public float SightRange; private float AttackRange; public float SafeRange; private AttackSequence ActiveAttackSequence; private AttackSequence QueuedAttack; [SerializeField] private bool IsPlayerInSight; [SerializeField] private bool IsPlayerInAttackRange; [SerializeField] private bool IsSafeToAttack; [SerializeField] private bool IsAttacking; private void Update() { if (IsAttacking) return; IsPlayerInSight = Physics.CheckSphere(transform.position, SightRange, PlayerLayer); IsSafeToAttack = !Physics.CheckSphere(transform.position, SafeRange, PlayerLayer); if (IsPlayerInSight) { transform.LookAt(Player); if (IsSafeToAttack) { QueuedAttack = ChooseAttack(); Debug.Log(QueuedAttack); if (QueuedAttack != null) { AttackRange = QueuedAttack.ActivationRange; //this is for gizmo debugging DoAttackSequence(QueuedAttack); } else Agent.SetDestination(Player.position); } else { Agent.SetDestination(Player.position); Agent.speed = -Agent.speed; } } else Agent.SetDestination(transform.position); } private AttackSequence ChooseAttack() { List<AttackSequence> DoableAttacks = Attacks.FindAll(attack => Physics.CheckSphere(transform.position, attack.ActivationRange, PlayerLayer)); DoableAttacks.Sort((a,b) => a.ActivationRange.CompareTo(b.ActivationRange)); return DoableAttacks.Count > 0 ? DoableAttacks[0] : null; } private void DoAttackSequence(AttackSequence attack) { IsAttacking = true; ActiveAttackSequence = attack; //call DoSequence() in attack IsAttacking = false; } }
现咨询当前的攻击模块化实现方案是否合理,是否存在更高效的优化方式?
一、现有方案的合理性
你的基础架构设计是合理且符合模块化思想的,核心优势包括:
- 采用继承+多态实现不同攻击行为的封装,每个Attack子类专注单一攻击逻辑,符合单一职责原则;
- 序列化设计让攻击参数(如攻击前摇、伤害倍率)可直接在Unity Inspector中配置,方便调试与不同敌人的差异化设置;
- AttackSequence作为攻击容器,统一管理多段攻击的执行顺序,简化了复杂攻击组合的逻辑;
- EnemyUnit中的攻击选择逻辑清晰,通过距离筛选可执行攻击,符合敌人AI的行为逻辑。
二、关键优化点
1. 用协程处理攻击时序逻辑
当前DoAttack()中的等待逻辑无法直接实现,普通方法会阻塞主线程。必须改用Unity协程来处理攻击的前摇、生效、后摇阶段:
- 修改Attack类的
DoAttack()返回IEnumerator,子类重写时使用yield return new WaitForSeconds()实现等待; - AttackSequence的
DoSequence()也改为返回IEnumerator,遍历序列时依次协程执行每个Attack; - EnemyUnit的
DoAttackSequence()启动协程,等待序列执行完毕后再将IsAttacking设为false,避免攻击未完成就触发新行为。
示例修改(Attack类):
[System.Serializable] public abstract class Attack // 改为抽象类 { // 原有属性... public abstract IEnumerator DoAttack(); // 改为抽象方法,返回IEnumerator } public class HitboxAttack : Attack { public Collider attackCollider; public override IEnumerator DoAttack() { // 前摇等待 yield return new WaitForSeconds(StartupTime); // 激活碰撞体 attackCollider.enabled = true; // 攻击生效时长 yield return new WaitForSeconds(ActiveTime); // 关闭碰撞体 attackCollider.enabled = false; // 后摇等待 yield return new WaitForSeconds(RecoveryTime); } }
2. 将Attack改为抽象类
由于Attack父类的DoAttack()没有实际逻辑,且不需要实例化,建议改为abstract class,强制子类实现DoAttack(),避免空实现的冗余,同时提升代码规范性。
3. 优化物理检测性能
ChooseAttack()中对每个AttackSequence都调用Physics.CheckSphere(),会产生冗余的物理计算。可以先计算敌人与玩家的距离,再通过距离对比筛选可执行攻击:
private AttackSequence ChooseAttack() { if (Player == null) return null; float distanceToPlayer = Vector3.Distance(transform.position, Player.position); List<AttackSequence> DoableAttacks = Attacks.FindAll(attack => distanceToPlayer <= attack.ActivationRange); DoableAttacks.Sort((a,b) => a.ActivationRange.CompareTo(b.ActivationRange)); return DoableAttacks.Count > 0 ? DoableAttacks[0] : null; }
这样只需要一次距离计算,避免多次物理检测,提升性能。
4. 确保攻击状态的正确性
当前DoAttackSequence()中调用DoSequence()后立刻将IsAttacking设为false,会导致攻击还在执行时就允许新的攻击触发。必须通过协程等待序列完成:
private IEnumerator DoAttackSequence(AttackSequence attack) { IsAttacking = true; ActiveAttackSequence = attack; yield return StartCoroutine(attack.DoSequence()); IsAttacking = false; }
5. 提升编辑器兼容性
给所有Attack子类添加[System.Serializable]标签,确保子类的属性能在Unity Inspector中正常显示和编辑:
[System.Serializable] public class HitboxAttack : Attack { // 子类属性... }
6. 投射物复用优化
ProjectileAttack中每次实例化投射物会产生GC开销,建议使用对象池复用投射物:提前创建一批投射物对象,需要时从池中取出,使用完后放回池内,避免频繁创建销毁。
7. 实现攻击中断逻辑
Attack类中定义了InterruptValue但未实现,可在EnemyUnit中添加逻辑:当敌人受到玩家攻击时,根据InterruptValue判断是否打断当前攻击序列,提升战斗交互性。
8. 兼容玩家攻击扩展
当前架构已具备扩展到玩家的基础,只需编写PlayerAttack脚本,将攻击触发逻辑从敌人的距离检测改为玩家的按键输入,复用Attack和AttackSequence的逻辑即可。
内容的提问来源于stack exchange,提问作者COLE SAMUEL BALAJADIA

