Unity2017.3.1f1敌人投射物脚本无法瞄准最近目标仅返回单对象求助
现有代码核心问题排查
- 子弹脚本逻辑错误:
RandomAIProjectile的Start方法中遍历所有玩家后反复覆盖子弹飞行速度,最终子弹只会朝向数组最后一个玩家飞行,且碰撞到任意目标后立刻销毁子弹,天然只能命中单个对象 - 敌方数据只初始化一次:
RandomAIEnemy的player数组仅在Start执行时查询一次,后续新加入的玩家/AI单位永远不会被识别 - 单玩家逻辑限制:敌方脚本的
playerX仅在Start时绑定单个玩家组件,多玩家场景下碰撞后只会扣除固定单个玩家的血量 - 目标查询逻辑限制:
TryGetClosestPlayer方法仅返回距离最近的单个玩家,没有多目标筛选能力
List功能改造实现(兼容Unity 2017.3.1f1)
所有用到List的脚本头部都需要先引入泛型命名空间:
using System.Collections.Generic;
1. 敌方控制脚本改造
将固定数组改为动态List,支持多目标查询,可选定时刷新玩家列表适配玩家进出场景的需求:
public class RandomAIEnemy : MonoBehaviour // 原脚本漏了继承MonoBehaviour,必须补上才能挂载到GameObject上 { // 原有属性变量全部保留,仅修改玩家存储结构 private List<GameObject> players = new List<GameObject>(); public float playerRefreshInterval = 0.5f; // 每0.5秒刷新一次玩家列表,可根据需求调整 private float refreshTimer; void Start () { RefreshPlayerList(); waitTime = startWaitTime; moveSpots.position = new Vector2 (Random.Range (minX, maxX), Random.Range (minY, maxY)); } void FixedUpdate () { // 定时刷新玩家列表 refreshTimer += Time.fixedDeltaTime; if (refreshTimer >= playerRefreshInterval) { RefreshPlayerList(); refreshTimer = 0; } EnemyS (); RandomMove(); // 原逻辑未调用该方法,补上才能实现随机移动 } // 刷新玩家列表方法,自动过滤已销毁/失活的单位 void RefreshPlayerList() { players.Clear(); foreach (var p in GameObject.FindGameObjectsWithTag("Player")) { if (p != null && p.activeSelf) { players.Add(p); } } } // 新增:获取射击范围内所有玩家的方法,返回List结构的多目标集合 private List<GameObject> GetAllPlayersInShootingRange() { List<GameObject> inRangePlayers = new List<GameObject>(); foreach (var p in players) { if (p == null) continue; // 用sqrMagnitude代替Distance减少平方根计算,降低性能消耗 float sqrDist = ((Vector2)p.transform.position - (Vector2)transform.position).sqrMagnitude; if (sqrDist <= shootingRange * shootingRange) { inRangePlayers.Add(p); } } return inRangePlayers; } // 改造射击逻辑,支持对范围内所有玩家发射子弹 void EnemyS () { List<GameObject> inRangePlayers = GetAllPlayersInShootingRange(); if (inRangePlayers.Count > 0 && nextFireTime < Time.time) { // 示例:给每个范围内的玩家都发射一颗子弹,可按需修改为随机选择N个目标的逻辑 foreach (var target in inRangePlayers) { GameObject newBullet = Instantiate (bullet, bulletParent.transform.position, Quaternion.identity); // 直接给子弹传递目标,避免子弹自主查询Tag浪费性能 newBullet.GetComponent<RandomAIProjectile>().SetTarget(target); } nextFireTime = Time.time + fireRate; } } private void OnTriggerEnter2D(Collider2D collision) { // 改造扣血逻辑,碰到哪个玩家就扣哪个的血量,不再绑定固定单玩家 if (collision.gameObject.tag.Equals ("Player")) { Instantiate(blood,transform.position, Quaternion.identity); var playerX = collision.GetComponent<BlackthronpodPlayerX>(); if (playerX != null) { playerX.health --; Debug.Log(playerX.health); } Destroy(gameObject); } // 原有子弹碰撞逻辑保留 if (collision.tag == "Bullet") { Game.AddToScore(1); Destroy(gameObject); } } // 其余原有方法(RandomMove、OnDrawGizmosSelected等)无需修改,直接保留即可 }
2. 子弹脚本改造
移除子弹自主查询目标的逻辑,改为接收外部传入的目标,可配置穿透次数支持单子弹多命中:
public class RandomAIProjectile : MonoBehaviour // 同样补全MonoBehaviour继承 { public float speed; Rigidbody2D bulletRB; public GameObject explosionEffect; public int penetrateCount = 1; // 子弹可穿透的目标数,1为仅命中1个,调大可实现穿甲效果 private int hitCount = 0; void Awake() { bulletRB = GetComponent<Rigidbody2D> (); Destroy (this.gameObject, 2); // 超时销毁逻辑提前到Awake执行,避免目标为空时子弹不销毁 } // 外部调用的目标设置方法 public void SetTarget(GameObject target) { if (target == null || bulletRB == null) return; Vector2 moveDir = (target.transform.position - transform.position).normalized * speed; bulletRB.velocity = moveDir; } private void OnTriggerEnter2D(Collider2D other) { if (other.gameObject.GetComponent<BlackthronpodDiePlayer>() != null) { Destroy (other.gameObject); hitCount++; } if (other.gameObject.tag == "Player") { Instantiate (explosionEffect, transform.position, Quaternion.identity); hitCount++; } if (other.CompareTag ("Bullet")) { Destroy (other.gameObject); hitCount++; } // 达到穿透上限再销毁子弹 if (hitCount >= penetrateCount) { Destroy(gameObject); } } }
性能优化补充建议
- 玩家数量低于100的情况下,0.5秒刷新一次列表的性能消耗极低,完全可以满足Slither.io类游戏的需求
- 如果玩家数量过大,可改为玩家生成/销毁时主动向全局管理器注册/注销的逻辑,完全避免全局查找Tag的消耗
- 子弹可引入对象池复用,减少频繁
Instantiate和Destroy带来的GC开销,适合大量生成子弹的对战场景
内容的提问来源于stack exchange,提问作者Nathan Kumwenda
相关产品推荐
相关产品推荐

