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

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--;
            // 保留原有射击逻辑
        }
    }
}

二、实现武器切换逻辑

在射击流程中加入弹药检查,无弹药时自动切换:

  1. 在ProcessShooting方法中,先判断当前武器是否有弹药,无弹药则触发切换
  2. 遍历武器列表,找到第一个有弹药的武器设为当前武器
  3. 若所有武器都无弹药,则启动寻枪逻辑

修改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;
        }
    }
}

三、实现寻路拾取武器逻辑

当所有武器无弹药时,自动寻找并拾取场景武器:

  1. 给场景中可拾取武器添加PickupWeapon标签,方便批量查找
  2. 遍历所有带标签的物体,计算距离找到最近的武器
  3. 启动寻路协程,到达后将武器加入敌人武器列表并设置为当前武器

示例代码:

// 新增:寻找并拾取武器的协程
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 21:55:44