Unity敌人AI优化求助:实现Don't Starve式减速延迟攻击流程
Unity敌人AI攻击逻辑问题排查与修正
问题描述
开发敌人AI时遇到以下问题:敌人可正常追击玩家,但进入attackRange后无法造成伤害,会保持进入时的方向盲目移动,且不会减速。
需求目标
- 敌人进入attackRange后减速至0.1f;
- 等待0.5秒后发起攻击(玩家仍在范围内则造成伤害,否则仅播放动画);
- 攻击后再等待0.5秒,恢复原移动速度;
- 玩家离开attackRange时,敌人需完成完整流程后再加速,而非立刻提速。
原问题代码
using System.Collections; using System.Collections.Generic; using UnityEngine; public class enemy : MonoBehaviour { public Transform player; public float chaseRange = 5f; public float attackRange = 1f; public float patrolSpeed = 0.5f; public float chaseSpeed = 0.75f; public float minIdleTime = 1f; public float maxIdleTime = 3f; public float minMoveTime = 1f; public float maxMoveTime = 3f; public eData enemyData; public float attackPrepareTime = 1f; public float attackCooldownTime = 2f; public int attackDamage = 9; private bool isChasing = false; private bool isPatrolling = true; private bool isIdle = false; private bool isAttacking = false; private bool isPreparingAttack = false; private bool isCooldown = false; private bool isReturning = false; private float idleTimer = 0f; private float idleTime = 0f; private float moveTimer = 0f; private float moveTime = 0f; private float attackPrepareTimer = 0f; private float attackCooldownTimer = 0f; private Vector2 patrolDirection; private Vector2 moveDirection; private Rigidbody2D rb; private Health enemyHealth; private void Start() { rb = GetComponent<Rigidbody2D>(); enemyHealth = GetComponent<Health>(); // Set enemy health from eData if (enemyHealth != null && enemyData != null) { enemyHealth.SetHealth(enemyData.hp, enemyData.hp); } idleTime = GetRandomIdleTime(); moveTime = GetRandomMoveTime(); patrolDirection = GetRandomDirection(); moveDirection = GetRandomDirection(); } private void Update() { float distanceToPlayer = Vector2.Distance(transform.position, player.position); if (distanceToPlayer <= chaseRange && distanceToPlayer > attackRange) { isChasing = true; isPatrolling = false; ChasePlayer(); } else if (isChasing && distanceToPlayer > chaseRange) { isChasing = false; isPatrolling = true; StopChasing(); } if (isReturning) { ReturnToPlayer(); } else if (isPatrolling) { Patrol(); } if (isPreparingAttack) { attackPrepareTimer += Time.deltaTime; if (attackPrepareTimer >= attackPrepareTime) { isPreparingAttack = false; isAttacking = true; attackPrepareTimer = 0f; rb.velocity = Vector2.zero; Invoke("PerformAttack", 0.5f); } } if (isCooldown) { attackCooldownTimer += Time.deltaTime; if (attackCooldownTimer >= attackCooldownTime) { isCooldown = false; attackCooldownTimer = 0f; if (!isReturning) { rb.velocity = patrolDirection * patrolSpeed; } } } if (isAttacking) { rb.velocity = Vector2.zero; } } private void ChasePlayer() { Vector2 direction = (player.position - transform.position).normalized; rb.velocity = direction * chaseSpeed; if (!isAttacking && !isPreparingAttack) { if (Vector2.Distance(transform.position, player.position) <= attackRange) { rb.velocity = Vector2.zero; isPreparingAttack = true; } } } private void StopChasing() { rb.velocity = Vector2.zero; isAttacking = false; isPreparingAttack = false; isCooldown = false; isReturning = true; } private void ReturnToPlayer() { Vector2 direction = (player.position - transform.position).normalized; rb.velocity = direction * chaseSpeed; if (Vector2.Distance(transform.position, player.position) <= attackRange) { rb.velocity = Vector2.zero; isReturning = false; isPreparingAttack = true; } } private void Patrol() { if (isIdle) { idleTimer += Time.deltaTime; if (idleTimer >= idleTime) { isIdle = false; moveTimer = 0f; moveTime = GetRandomMoveTime(); moveDirection = GetRandomDirection(); } } else { moveTimer += Time.deltaTime; if (moveTimer >= moveTime) { isIdle = true; idleTimer = 0f; idleTime = GetRandomIdleTime(); rb.velocity = Vector2.zero; } else { rb.velocity = moveDirection * patrolSpeed; } } } private float GetRandomIdleTime() { return Random.Range(minIdleTime, maxIdleTime); } private float GetRandomMoveTime() { return Random.Range(minMoveTime, maxMoveTime); } private Vector2 GetRandomDirection() { float randomX = Random.Range(-1f, 1f); float randomY = Random.Range(-1f, 1f); return new Vector2(randomX, randomY).normalized; } private void PerformAttack() { if (isAttacking) { if (Vector2.Distance(transform.position, player.position) <= attackRange) { AttackPlayer(); } isAttacking = false; isCooldown = true; Invoke("FinishAttack", 0.5f); } } private void FinishAttack() { isCooldown = false; if (!isReturning) { rb.velocity = chaseSpeed * (player.position - transform.position).normalized; } } private void AttackPlayer() { Health playerHealth = player.GetComponent<Health>(); if (playerHealth != null) { playerHealth.Damage(attackDamage); } } }
问题排查与修正方案
核心问题分析
- 状态优先级倒置:原代码中攻击相关状态的判断在追击/巡逻逻辑之后,导致追击逻辑会覆盖攻击阶段的速度设置,无法实现减速或停止。
- 减速逻辑缺失:需求要求进入攻击范围后减速至0.1f,原代码直接将速度设为0,不符合需求。
- 流程中断问题:玩家离开攻击范围时,原代码没有保留当前攻击流程,会提前退出攻击状态,不符合“完成完整流程后再加速”的要求。
- Invoke状态同步问题:原代码中
PerformAttack的Invoke调用可能因状态变化导致逻辑失效,且速度恢复逻辑不完整。
修正后代码
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { public Transform player; public float chaseRange = 5f; public float attackRange = 1f; public float patrolSpeed = 0.5f; public float chaseSpeed = 0.75f; public float attackSlowSpeed = 0.1f; // 攻击准备阶段减速速度 public float minIdleTime = 1f; public float maxIdleTime = 3f; public float minMoveTime = 1f; public float maxMoveTime = 3f; public eData enemyData; public float attackPrepareDelay = 0.5f; // 攻击前等待时间(需求中的0.5秒) public float attackAfterDelay = 0.5f; // 攻击后等待时间(需求中的0.5秒) public int attackDamage = 9; private bool isChasing = false; private bool isPatrolling = true; private bool isIdle = false; private bool isAttackProcessing = false; // 标记是否正在执行攻击流程 private float idleTimer = 0f; private float idleTime = 0f; private float moveTimer = 0f; private float moveTime = 0f; private Vector2 moveDirection; private Rigidbody2D rb; private Health enemyHealth; private void Start() { rb = GetComponent<Rigidbody2D>(); enemyHealth = GetComponent<Health>(); if (enemyHealth != null && enemyData != null) { enemyHealth.SetHealth(enemyData.hp, enemyData.hp); } idleTime = GetRandomIdleTime(); moveTime = GetRandomMoveTime(); moveDirection = GetRandomDirection(); } private void Update() { float distanceToPlayer = Vector2.Distance(transform.position, player.position); // 优先处理攻击流程,确保攻击过程不被打断 if (isAttackProcessing) return; // 追击逻辑 if (distanceToPlayer <= chaseRange) { isPatrolling = false; isChasing = true; // 进入攻击范围则启动攻击流程 if (distanceToPlayer <= attackRange) { StartCoroutine(AttackCoroutine()); return; } ChasePlayer(); } else { // 离开追击范围,回到巡逻状态 if (isChasing) { isChasing = false; isPatrolling = true; rb.velocity = Vector2.zero; } if (isPatrolling) { Patrol(); } } } // 攻击流程协程,完整实现需求的攻击步骤 private IEnumerator AttackCoroutine() { isAttackProcessing = true; Vector2 lastChaseDir = (player.position - transform.position).normalized; // 步骤1:减速至attackSlowSpeed rb.velocity = lastChaseDir * attackSlowSpeed; yield return new WaitForSeconds(attackPrepareDelay); // 步骤2:发起攻击(播放动画逻辑可在此处添加) float distance = Vector2.Distance(transform.position, player.position); if (distance <= attackRange) { AttackPlayer(); } // 步骤3:攻击后等待,保持减速状态 rb.velocity = lastChaseDir * attackSlowSpeed; yield return new WaitForSeconds(attackAfterDelay); // 步骤4:恢复原速度,结束攻击流程 isAttackProcessing = false; // 如果玩家仍在追击范围内,继续追击;否则回到巡逻 float finalDistance = Vector2.Distance(transform.position, player.position); if (finalDistance <= chaseRange && finalDistance > attackRange) { ChasePlayer(); } else if (finalDistance > chaseRange) { isChasing = false; isPatrolling = true; } } private void ChasePlayer() { Vector2 direction = (player.position - transform.position).normalized; rb.velocity = direction * chaseSpeed; } private void Patrol() { if (isIdle) { idleTimer += Time.deltaTime; if (idleTimer >= idleTime) { isIdle = false; moveTimer = 0f; moveTime = GetRandomMoveTime(); moveDirection = GetRandomDirection(); } } else { moveTimer += Time.deltaTime; if (moveTimer >= moveTime) { isIdle = true; idleTimer = 0f; idleTime = GetRandomIdleTime(); rb.velocity = Vector2.zero; } else { rb.velocity = moveDirection * patrolSpeed; } } } private float GetRandomIdleTime() => Random.Range(minIdleTime, maxIdleTime); private float GetRandomMoveTime() => Random.Range(minMoveTime, maxMoveTime); private Vector2 GetRandomDirection() { float randomX = Random.Range(-1f, 1f); float randomY = Random.Range(-1f, 1f); return new Vector2(randomX, randomY).normalized; } private void AttackPlayer() { Health playerHealth = player.GetComponent<Health>(); if (playerHealth != null) { playerHealth.Damage(attackDamage); } } }
修正说明
- 状态优先级调整:将攻击流程判断放在Update最前端,确保攻击过程中不会被追击/巡逻逻辑打断,实现“完成完整流程后再加速”的需求。
- 添加减速逻辑:新增
attackSlowSpeed变量,在攻击准备和攻击后阶段将速度设置为该值,符合需求中的减速要求。 - 使用协程管理攻击流程:用
AttackCoroutine替代原有的Invoke和状态标记,更清晰地控制攻击的各个阶段(减速-等待-攻击-等待-恢复速度),避免状态同步问题。 - 简化状态变量:移除冗余的
isPreparingAttack、isAttacking、isCooldown等状态,统一用isAttackProcessing标记是否处于攻击流程,减少逻辑冲突。 - 优化离开范围处理:攻击流程结束后再判断玩家位置,决定继续追击或回到巡逻,确保流程完整执行。
内容的提问来源于stack exchange,提问作者Henzio
相关产品推荐
相关产品推荐

