Unity中如何实现敌人弹药耗尽后拾取最近武器的功能?
问题描述
我想给敌人实现以下智能行为:
- 当前武器弹药耗尽时自动切换到其他可用武器
- 若所有武器都无弹药,则寻找场景中最近的武器并拾取切换
目前已经完成了敌人的巡逻、范围内发现玩家、追逐及攻击功能,相关代码如下:
public class EnemyControler : MonoBehaviour { [Header("Attack")] [SerializeField] float shootingDistance =10.0f; [SerializeField] float shootDelay = 3.5f; [Range(0,1.0f)][SerializeField] float shootingAccuracy =0.5f; [SerializeField] int shootDamage =5; [SerializeField] int ammo = 1; [Header("User Interface")] public Transform canvasTr; public Slider Healthbar; [Header("Health/Damage/Death")] public float MaxHealth; public float Damage; public float AttackRange; public int deathCounter; public Transform ammoObject; private NavMeshAgent navAgent; private Collider enemycollider; private Transform PlayerTr; private Animator EnemyAnim; float Health; bool showingHealthBar, alive; bool isPatrolling =false; bool isInShootingRange =false; bool canResumeIdleState =true; bool isPreparingToShoot=false; bool isDead =false; bool isAlerted =false; float shootTimer = Mathf.Infinity; AIPatrolBehavior aIPatrolBehavior = null; void Start() { Health = MaxHealth; canvasTr.gameObject.SetActive(false); navAgent = GetComponent<NavMeshAgent>(); EnemyAnim = GetComponent<Animator>(); enemycollider = GetComponent<Collider>(); PlayerTr = GameObject.FindGameObjectWithTag("Player").transform; aIPatrolBehavior = GetComponent<AIPatrolBehavior>(); alive = true; ammo = 100; StartCoroutine(Idle()); // are you sure you want to randomly change MaxHealth after settting health = maxHealth? //the slider gets weirdly bugged // MaxHealth = Random.Range(50, 200); Healthbar.maxValue = MaxHealth; // set the max value to MaxHelth } private void Update() { if(isDead)return; if(Vector3.Distance(PlayerTr.transform.position, transform.position) > 20 && !isAlerted || PlayerTr.GetComponent<PlayerController>().HealthBar.value <= 0.0f){ StopAllCoroutines(); GetComponent<AIPatrolBehavior>().enabled =true; return; } if(Vector3.Distance(PlayerTr.transform.position, transform.position) < 20 && isAlerted) isAlerted=false; isInShootingRange = DistanceToPlayer() < shootingDistance && DistanceToPlayer() > AttackRange - 0.5f && PlayerTr.GetComponent<PlayerController>().HealthBar.value > 0; if(isInShootingRange) { if(canResumeIdleState){ StopAllCoroutines(); canResumeIdleState=false; } ProcessShooting(); } else{ if(!canResumeIdleState){ isPreparingToShoot =false; StartCoroutine(Idle()); canResumeIdleState =true; navAgent.enabled =true; } } } private void ProcessShooting() { if(!isPreparingToShoot) shootTimer +=Time.deltaTime; navAgent.enabled =false; transform.LookAt(PlayerTr); float randomProbability =Random.Range(0,1.0f); if(shootTimer > shootDelay){ ShootAtPlayer(); } EnemyAnim.SetFloat("MovmentSpeed", 0, 0.3f, Time.deltaTime); } private void ShootAtPlayer(){ if(isPreparingToShoot)return; EnemyAnim.SetBool("PrepareAttack", false); shootTimer =0.0f; isPreparingToShoot = true; EnemyAnim.SetTrigger("shoot"); } public void ShootPlayerAnimationEvent(){ Debug.Log("Player got shot"); float randomAccuracy = Random.Range(0, 1.0f); bool willHitTarget = randomAccuracy > 1.0f - shootingAccuracy; if(willHitTarget && DistanceToPlayer() < shootingDistance){ PlayerTr.GetComponent<PlayerController>().DoDamage(shootDamage,true); } GetComponentInChildren<AIWeapon>().UseWeapon(); isPreparingToShoot=false; } IEnumerator Idle() { EnemyAnim.SetBool("PrepareAttack", false); yield return new WaitUntil(() => Vector3.Distance(PlayerTr.transform.position, transform.position) < 20 || isAlerted); StartCoroutine(RunToTarget()); } IEnumerator RunToTarget() { aIPatrolBehavior.enabled =false; if(navAgent.isOnNavMesh) { // save from error apperng navAgent.isStopped = false; } EnemyAnim.SetTrigger("Attack"); while (Vector3.Distance(PlayerTr.transform.position, transform.position) > AttackRange - 0.5f) { if(navAgent.isOnNavMesh) { // save from error apperng navAgent.SetDestination(PlayerTr.position); } // navAgent.SetDestination(PlayerTr.position); EnemyAnim.SetFloat("MovmentSpeed", 1, 0.3f, Time.deltaTime); yield return null; } StartCoroutine(Attack()); } IEnumerator Attack() { EnemyAnim.SetBool("PrepareAttack", true); navAgent.isStopped = true; while (Vector3.Distance(PlayerTr.position, transform.position) < AttackRange) { EnemyAnim.SetTrigger("Attack"); float t = 0.5f; while (t > 0) { Vector3 rotation = Vector3.RotateTowards(transform.forward, PlayerTr.position - transform.position, 5f * Time.deltaTime, 1f); transform.forward = rotation; t -= Time.deltaTime; yield return null; } yield return null; } EnemyAnim.SetBool("PrepareAttack", false); StartCoroutine(RunToTarget()); } float DistanceToPlayer(){ return Vector3.Distance(transform.position,PlayerTr.position); } public void DoDamage(float damage) { Alert(); if (!showingHealthBar) { showingHealthBar = true; StartCoroutine(ShowHealthBar()); } Health -= damage; Debug.Log("Health: " + Health + " of: " + MaxHealth); Healthbar.value = Health; if (Health <= 0) { StopAllCoroutines(); if(navAgent.isOnNavMesh) { // save from error apperng navAgent.isStopped = true; } if (alive) { alive = true; // does this make sense? StartCoroutine(Death()); } } } }
尝试过谷歌查找相关教程但没找到,求可行的实现思路。
实现思路
一、重构武器数据结构
首先要给敌人添加多武器管理能力,替换现有单一武器的依赖:
- 在
EnemyController中新增List<AIWeapon>字段存储所有携带武器,再用AIWeapon currentWeapon标记当前使用武器 - 给
AIWeapon类扩展弹药属性,添加弹药判断方法
示例代码:
// EnemyController新增字段 [Header("Weapons")] [SerializeField] private List<AIWeapon> allWeapons; private AIWeapon currentWeapon; // AIWeapon类扩展 public class AIWeapon : MonoBehaviour { [SerializeField] private int maxAmmo = 30; [SerializeField] private int currentAmmo; private void Start() { currentAmmo = maxAmmo; } public bool HasAmmo() { return currentAmmo > 0; } public void UseWeapon() { if (HasAmmo()) { currentAmmo--; // 保留原有射击逻辑 } } }
二、实现武器切换逻辑
在射击流程中加入弹药检查,无弹药时自动切换:
- 在
ProcessShooting方法中,先判断当前武器是否有弹药,无弹药则触发切换 - 遍历武器列表,找到第一个有弹药的武器设为当前武器
- 若所有武器都无弹药,则启动寻枪逻辑
修改ProcessShooting及新增切换方法:
private void ProcessShooting() { if(!isPreparingToShoot) shootTimer +=Time.deltaTime; navAgent.enabled =false; transform.LookAt(PlayerTr); // 新增:检查当前武器弹药,无弹药则尝试切换 if (!currentWeapon.HasAmmo()) { TrySwitchWeapon(); // 切换后仍无可用武器,则停止射击去寻枪 if (!currentWeapon.HasAmmo()) { StartCoroutine(SearchForWeapon()); return; } } float randomProbability =Random.Range(0,1.0f); if(shootTimer > shootDelay){ ShootAtPlayer(); } EnemyAnim.SetFloat("MovmentSpeed", 0, 0.3f, Time.deltaTime); } // 新增:切换到有弹药的武器 private void TrySwitchWeapon() { foreach(var weapon in allWeapons) { if (weapon.HasAmmo()) { currentWeapon = weapon; EnemyAnim.SetTrigger("switchWeapon"); // 可选:播放切换动画 break; } } }
三、实现寻路拾取武器逻辑
当所有武器无弹药时,自动寻找并拾取场景武器:
- 给场景中可拾取武器添加
PickupWeapon标签,方便批量查找 - 遍历所有带标签的物体,计算距离找到最近的武器
- 启动寻路协程,到达后将武器加入敌人武器列表并设置为当前武器
示例代码:
// 新增:寻找并拾取武器的协程 private IEnumerator SearchForWeapon() { isPreparingToShoot = false; navAgent.enabled = true; while (true) { GameObject[] pickupWeapons = GameObject.FindGameObjectsWithTag("PickupWeapon"); if (pickupWeapons.Length == 0) { // 无武器可捡时, fallback 到近战攻击 StartCoroutine(Attack()); yield break; } // 找到最近的武器 GameObject nearestWeapon = null; float minDistance = Mathf.Infinity; foreach(var weaponObj in pickupWeapons) { float distance = Vector3.Distance(transform.position, weaponObj.transform.position); if (distance < minDistance) { minDistance = distance; nearestWeapon = weaponObj; } } // 移动到目标武器 navAgent.SetDestination(nearestWeapon.transform.position); EnemyAnim.SetFloat("MovmentSpeed", 1, 0.3f, Time.deltaTime); // 到达后拾取 yield return new WaitUntil(() => Vector3.Distance(transform.position, nearestWeapon.transform.position) < 1f); AIWeapon pickedWeapon = nearestWeapon.GetComponent<AIWeapon>(); allWeapons.Add(pickedWeapon); currentWeapon = pickedWeapon; // 将武器挂载到敌人身上(需提前创建WeaponHolder空物体) pickedWeapon.transform.SetParent(transform.Find("WeaponHolder")); pickedWeapon.transform.localPosition = Vector3.zero; pickedWeapon.transform.localRotation = Quaternion.identity; // 移除拾取物的交互组件,避免重复拾取 Destroy(nearestWeapon.GetComponent<Collider>()); Destroy(nearestWeapon.GetComponent<Rigidbody>()); // 返回射击状态 StopCoroutine(this); ProcessShooting(); yield break; } }
四、整合现有逻辑
- 在
Start方法中初始化当前武器:
void Start() { // 原有初始化代码... currentWeapon = allWeapons[0]; }
- 修改
ShootPlayerAnimationEvent方法,使用当前武器:
public void ShootPlayerAnimationEvent(){ Debug.Log("Player got shot"); float randomAccuracy = Random.Range(0, 1.0f); bool willHitTarget = randomAccuracy > 1.0f - shootingAccuracy; if(willHitTarget && DistanceToPlayer() < shootingDistance){ PlayerTr.GetComponent<PlayerController>().DoDamage(shootDamage,true); } currentWeapon.UseWeapon(); isPreparingToShoot=false; }
五、可选优化
- 添加换弹逻辑:给
AIWeapon增加备用弹药字段,优先换弹再切换武器 - 优化拾取优先级:优先选择伤害高、射程远的武器,而非只看距离
- 状态机管理:将巡逻、追逐、射击、寻枪等状态用状态机封装,提升逻辑可读性
内容的提问来源于stack exchange,提问作者Laurens van Oorschot
相关产品推荐
相关产品推荐

