Unity中生成超过3个敌人后FPS掉帧问题排查与优化咨询
Unity 敌人数量超3个时卡顿掉帧问题修复
核心问题原因
卡顿是多个代码问题叠加导致的,按影响程度排序如下:
- 会直接触发性能爆炸的逻辑错误
- 零向量归一化产生非法值,导致物理引擎计算异常
追击逻辑中,当敌人和玩家坐标完全重合时,direction变量为零向量Vector2.zero,直接调用Normalize()会生成NaN(非数字)值,后续给刚体的angularVelocity、velocity赋值NaN后,2D物理引擎的迭代求解器无法处理非法值,会出现计算无法收敛的问题,直接导致掉帧卡顿。 - 协程递归写法存在溢出风险
当前通过递归启动新协程实现循环生成的写法非常脆弱,一旦TimeStep参数被意外设为0或负数,WaitForSeconds会直接跳过等待,瞬间生成海量敌人,直接把帧率压到个位数。
- 零向量归一化产生非法值,导致物理引擎计算异常
- 带来不必要开销的冗余逻辑
- 每个敌人生成时重复执行全局查找
每个敌人在Start()里都调用FindObjectOfType<Player>()和FindGameObjectWithTag("Player")两个遍历全场景的查找API,敌人数量上来后会产生累积开销。 - 生命周期计时逻辑不规范
销毁计时放在FixedUpdate()中执行,还使用Time.deltaTime累加,额外占用物理帧的执行时间,逻辑上也容易出现计时不准的问题。 - 未做碰撞层过滤
没有配置碰撞矩阵的情况下,敌人和敌人之间、敌人和场景无关碰撞体之间会持续产生无意义的物理碰撞计算,白白消耗性能。 - 频繁创建销毁对象触发GC
反复调用Instantiate/Destroy会产生垃圾回收开销,内存碎片积累到一定程度会触发帧时间突增。
- 每个敌人生成时重复执行全局查找
修复方案
1. 修复核心逻辑错误
首先替换敌人生成脚本,把递归协程改成内部循环,同时缓存等待对象减少GC,生成敌人时直接传入玩家引用,避免敌人自己做全局查找:
using System.Collections; using UnityEngine; public class SpawnEnemy : MonoBehaviour { public GameObject[] enemy; public Transform[] spawnPoint; public float spawnInterval = 3f; private WaitForSeconds _spawnWait; private void Start() { _spawnWait = new WaitForSeconds(spawnInterval); StartCoroutine(SpawnLoop()); } IEnumerator SpawnLoop() { while (true) { yield return _spawnWait; int randEnemyId = Random.Range(0, enemy.Length); int randSpawnId = Random.Range(0, spawnPoint.Length); GameObject newEnemy = Instantiate(enemy[randEnemyId], spawnPoint[randSpawnId].position, Quaternion.identity); // 直接传玩家引用,省去敌人内部查找开销 newEnemy.GetComponent<EnemyTypeOne>().Init(Player.Instance.transform); } } }
再替换敌人行为脚本,修复零向量归一化问题,把计时逻辑移到Update中,去掉冗余查找:
using UnityEngine; using UnityEngine.SceneManagement; [RequireComponent(typeof(Rigidbody2D))] public class EnemyTypeOne : MonoBehaviour { public float speed = 5f; public float rotateSpeed = 200f; public float lifeTime = 10f; private Rigidbody2D rb; private Transform _target; private Player _player; private float _lifeTimer; // 初始化时直接接收玩家引用 public void Init(Transform playerTarget) { _target = playerTarget; _player = _target.GetComponent<Player>(); rb = GetComponent<Rigidbody2D>(); // 强制关闭重力,避免无意义的物理计算 rb.gravityScale = 0f; _lifeTimer = 0f; } private void Update() { // 生命周期计时放在Update中,不占用物理帧时间 _lifeTimer += Time.deltaTime; if (_lifeTimer >= lifeTime) { Destroy(gameObject); } } private void FixedUpdate() { Vector2 direction = (Vector2)_target.position - rb.position; // 距离过近时跳过旋转计算,避免零向量归一化产生NaN if (direction.sqrMagnitude < 0.01f) { rb.velocity = Vector2.zero; rb.angularVelocity = 0f; return; } direction.Normalize(); float rotateAmount = Vector3.Cross(direction, transform.right).z; rb.angularVelocity = -rotateAmount * rotateSpeed; rb.velocity = transform.right * speed; } private void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("Player")) { Destroy(gameObject); if (_player.myHealth > 5) { _player.myHealth -= 5f; } else { SceneManager.LoadScene(0); } } } }
最后给Player脚本加全局单例,方便直接获取引用,把下面代码加到Player类里即可,原有逻辑不用改:
public class Player : MonoBehaviour { public static Player Instance; public float myHealth; private void Awake() { Instance = this; } // 原有Player逻辑保持不变 }
2. 编辑器配置优化
- 打开
Edit > Project Settings > Physics2D,在Layer Collision Matrix中取消Enemy层和Enemy层的碰撞勾选,让敌人之间不产生碰撞检测,记得把所有敌人预制体的Layer设为Enemy。 - 把敌人的Rigidbody2D的Collision Detection模式设为
Discrete,除非遇到高速穿透问题否则不要用Continuous模式,前者性能开销低很多。 - 再次确认所有敌人预制体的Rigidbody2D重力缩放参数为0,避免无意义的重力计算。
3. 进阶优化
如果后续需要提升同屏敌人数量上限,将当前的Instantiate/Destroy逻辑替换为对象池实现:提前生成一批敌人对象缓存在池中,需要生成时从池里取可用对象,销毁时把对象放回池中复用,避免反复创建销毁对象带来的GC和内存碎片问题。
补充说明:Unity编辑器本身运行就带有调试刷新、Gizmos绘制等额外开销,修复上述问题后如果编辑器下帧率稳定,编译出包后的运行帧率会更高。
内容的提问来源于stack exchange,提问作者George Zyn
相关产品推荐
相关产品推荐

