You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 05:54:57