常见坑
目录
10. 常见坑
Unity C# 开发中的常见问题和解决方案 - 30+个常见坑和解决方案
📌 本章导航
性能相关问题
1. 频繁的GetComponent调用
问题: 在Update方法中频繁调用GetComponent会导致性能下降。
// ❌ 错误示例 - 性能差
public class BadExample : MonoBehaviour
{
void Update()
{
// 每帧都调用GetComponent - 很慢!
Renderer renderer = GetComponent<Renderer>();
if (renderer != null)
{
renderer.material.color = Color.red;
}
}
}
// ✅ 正确示例 - 性能好
public class GoodExample : MonoBehaviour
{
private Renderer cachedRenderer;
void Start()
{
// 只在Start时获取一次
cachedRenderer = GetComponent<Renderer>();
}
void Update()
{
// 使用缓存的引用
if (cachedRenderer != null)
{
cachedRenderer.material.color = Color.red;
}
}
}
解决方案:
- 在Start或Awake中缓存组件引用
- 使用字段存储引用而不是重复获取
- 对于子对象,缓存Transform引用
2. GameObject.Find的滥用
问题: 在Update中使用GameObject.Find会严重影响性能。
// ❌ 错误示例 - 性能极差
public class BadFindExample : MonoBehaviour
{
void Update()
{
// 每帧都在整个场景中查找对象 - 非常慢!
GameObject player = GameObject.Find("Player");
if (player != null)
{
transform.LookAt(player.transform);
}
}
}
// ✅ 正确示例 - 性能好
public class GoodFindExample : MonoBehaviour
{
private Transform playerTransform;
void Start()
{
// 只在Start时查找一次
GameObject player = GameObject.Find("Player");
if (player != null)
{
playerTransform = player.transform;
}
}
void Update()
{
// 使用缓存的引用
if (playerTransform != null)
{
transform.LookAt(playerTransform);
}
}
}
// ✅ 更好的解决方案 - 使用标签
public class BetterTagExample : MonoBehaviour
{
private Transform playerTransform;
void Start()
{
// 使用标签查找比名称查找稍快
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
}
}
void Update()
{
if (playerTransform != null)
{
transform.LookAt(playerTransform);
}
}
}
3. 频繁的对象创建和销毁
问题: 在Update中频繁创建和销毁对象会导致GC压力。
// ❌ 错误示例 - 导致GC压力
public class BadPoolingExample : MonoBehaviour
{
public GameObject bulletPrefab;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
// 每次射击都创建新对象
GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation);
// 5秒后销毁
Destroy(bullet, 5f);
}
}
}
// ✅ 正确示例 - 使用对象池
public class GoodPoolingExample : MonoBehaviour
{
public GameObject bulletPrefab;
private Queue<GameObject> bulletPool = new Queue<GameObject>();
private int poolSize = 20;
void Start()
{
// 预先创建对象池
for (int i = 0; i < poolSize; i++)
{
GameObject bullet = Instantiate(bulletPrefab);
bullet.SetActive(false);
bulletPool.Enqueue(bullet);
}
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Shoot();
}
}
void Shoot()
{
GameObject bullet;
if (bulletPool.Count > 0)
{
bullet = bulletPool.Dequeue();
bullet.SetActive(true);
}
else
{
// 如果池空了,创建新对象(应该避免这种情况)
bullet = Instantiate(bulletPrefab);
}
// 设置子弹属性
bullet.transform.position = transform.position;
bullet.transform.rotation = transform.rotation;
// 子弹使用后返回池中(通过子弹脚本实现)
BulletController bulletCtrl = bullet.GetComponent<BulletController>();
if (bulletCtrl != null)
{
bulletCtrl.SetPool(bulletPool);
}
}
}
// 子弹控制器示例
public class BulletController : MonoBehaviour
{
private Queue<GameObject> pool;
private float lifeTime = 5f;
private float timer = 0f;
public void SetPool(Queue<GameObject> bulletPool)
{
pool = bulletPool;
timer = 0f;
}
void Update()
{
timer += Time.deltaTime;
if (timer >= lifeTime)
{
ReturnToPool();
}
}
void ReturnToPool()
{
if (pool != null)
{
gameObject.SetActive(false);
transform.SetParent(null); // 移除父对象
pool.Enqueue(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
4. 不必要的字符串操作
问题: 频繁的字符串拼接和操作会导致内存分配。
// ❌ 错误示例 - 频繁字符串操作
public class BadStringExample : MonoBehaviour
{
private int score = 0;
void Update()
{
// 每帧都创建新的字符串对象
string scoreText = "Score: " + score + " - Level: " + GetLevel() + " - Time: " + Time.time;
Debug.Log(scoreText);
}
int GetLevel() { return 1; }
}
// ✅ 正确示例 - 使用StringBuilder
public class GoodStringExample : MonoBehaviour
{
private int score = 0;
private System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
private string cachedScoreText = "";
void Update()
{
// 只在必要时更新字符串
if (ShouldUpdateScoreText())
{
UpdateScoreText();
}
Debug.Log(cachedScoreText);
}
bool ShouldUpdateScoreText()
{
// 只在分数改变时更新
return true; // 简化示例
}
void UpdateScoreText()
{
stringBuilder.Length = 0; // 清空但不重新分配内存
stringBuilder.Append("Score: ");
stringBuilder.Append(score);
stringBuilder.Append(" - Level: ");
stringBuilder.Append(GetLevel());
stringBuilder.Append(" - Time: ");
stringBuilder.Append(Time.time);
cachedScoreText = stringBuilder.ToString();
}
int GetLevel() { return 1; }
}
// ✅ 更好的解决方案 - 格式化字符串缓存
public class BetterStringExample : MonoBehaviour
{
private int score = 0;
private string cachedScoreText = "";
private float lastScoreTime = 0f;
private float updateInterval = 0.5f; // 0.5秒更新一次
void Update()
{
if (Time.time - lastScoreTime >= updateInterval)
{
cachedScoreText = string.Format("Score: {0} - Level: {1} - Time: {2:F1}",
score, GetLevel(), Time.time);
lastScoreTime = Time.time;
}
Debug.Log(cachedScoreText);
}
int GetLevel() { return 1; }
}
5. 空引用检查不当
问题: 忘记检查空引用会导致NullReferenceException。
// ❌ 错误示例 - 没有空引用检查
public class BadNullCheckExample : MonoBehaviour
{
public GameObject target;
private Renderer renderer;
void Start()
{
// renderer可能为null
renderer = target.GetComponent<Renderer>();
}
void Update()
{
// 如果renderer为null,这里会抛出异常
renderer.material.color = Color.red;
}
}
// ✅ 正确示例 - 正确的空引用检查
public class GoodNullCheckExample : MonoBehaviour
{
public GameObject target;
private Renderer renderer;
void Start()
{
if (target != null)
{
renderer = target.GetComponent<Renderer>();
}
}
void Update()
{
// 检查renderer是否为null
if (renderer != null && renderer.material != null)
{
renderer.material.color = Color.red;
}
}
}
// ✅ 更好的解决方案 - 使用扩展方法
public static class UnityObjectExtensions
{
public static bool IsNull(this UnityEngine.Object obj)
{
return obj == null;
}
public static bool IsNotNull(this UnityEngine.Object obj)
{
return obj != null;
}
}
public class BetterNullCheckExample : MonoBehaviour
{
public GameObject target;
private Renderer renderer;
void Start()
{
if (target.IsNotNull())
{
renderer = target.GetComponent<Renderer>();
}
}
void Update()
{
if (renderer.IsNotNull() && renderer.material.IsNotNull())
{
renderer.material.color = Color.red;
}
}
}
内存管理问题
1. 内存泄漏
问题: 事件订阅后没有取消订阅导致内存泄漏。
// ❌ 错误示例 - 事件订阅导致内存泄漏
public class BadEventExample : MonoBehaviour
{
void Start()
{
// 订阅事件但没有取消订阅
GameEvents.OnScoreChanged += OnScoreChanged;
}
void OnScoreChanged(int newScore)
{
Debug.Log($"New score: {newScore}");
}
// 没有在OnDestroy中取消订阅!
}
// ✅ 正确示例 - 正确处理事件订阅
public class GoodEventExample : MonoBehaviour
{
void Start()
{
GameEvents.OnScoreChanged += OnScoreChanged;
}
void OnScoreChanged(int newScore)
{
Debug.Log($"New score: {newScore}");
}
void OnDestroy()
{
// 重要:在对象销毁时取消事件订阅
GameEvents.OnScoreChanged -= OnScoreChanged;
}
}
// ✅ 更好的解决方案 - 使用Action字段
public class BetterEventExample : MonoBehaviour
{
private System.Action<int> scoreChangedCallback;
void Start()
{
scoreChangedCallback = OnScoreChanged;
GameEvents.OnScoreChanged += scoreChangedCallback;
}
void OnScoreChanged(int newScore)
{
Debug.Log($"New score: {newScore}");
}
void OnDestroy()
{
// 取消订阅
if (GameEvents.OnScoreChanged != null)
{
GameEvents.OnScoreChanged -= scoreChangedCallback;
}
}
}
// 全局事件系统示例
public static class GameEvents
{
public static System.Action<int> OnScoreChanged;
public static System.Action<string> OnPlayerDeath;
public static System.Action OnGameStart;
public static System.Action OnGameEnd;
}
2. 协程没有正确停止
问题: 协程启动后没有正确停止导致内存泄漏。
// ❌ 错误示例 - 协程没有正确停止
public class BadCoroutineExample : MonoBehaviour
{
void Start()
{
// 启动协程但没有保存引用
StartCoroutine(RunForever());
}
System.Collections.IEnumerator RunForever()
{
while (true)
{
Debug.Log("Running...");
yield return new WaitForSeconds(1f);
}
}
// 没有停止协程!
}
// ✅ 正确示例 - 正确管理协程
public class GoodCoroutineExample : MonoBehaviour
{
private System.Collections.IEnumerator runningCoroutine;
void Start()
{
// 保存协程引用
runningCoroutine = RunForever();
StartCoroutine(runningCoroutine);
}
System.Collections.IEnumerator RunForever()
{
while (true)
{
Debug.Log("Running...");
yield return new WaitForSeconds(1f);
}
}
void OnDestroy()
{
// 停止协程
if (runningCoroutine != null)
{
StopCoroutine(runningCoroutine);
}
}
}
// ✅ 更好的解决方案 - 使用Coroutine管理器
public class BetterCoroutineExample : MonoBehaviour
{
private List<Coroutine> activeCoroutines = new List<Coroutine>();
void Start()
{
StartManagedCoroutine(RunForever());
}
System.Collections.IEnumerator RunForever()
{
while (true)
{
Debug.Log("Running...");
yield return new WaitForSeconds(1f);
}
}
Coroutine StartManagedCoroutine(System.Collections.IEnumerator routine)
{
Coroutine coroutine = StartCoroutine(routine);
activeCoroutines.Add(coroutine);
return coroutine;
}
void StopManagedCoroutine(Coroutine coroutine)
{
if (activeCoroutines.Contains(coroutine))
{
StopCoroutine(coroutine);
activeCoroutines.Remove(coroutine);
}
}
void OnDestroy()
{
// 停止所有协程
foreach (Coroutine coroutine in activeCoroutines.ToArray())
{
if (coroutine != null)
{
StopCoroutine(coroutine);
}
}
activeCoroutines.Clear();
}
}
3. 资源没有正确释放
问题: 加载的资源没有正确释放导致内存泄漏。
// ❌ 错误示例 - 资源没有释放
public class BadResourceExample : MonoBehaviour
{
void Start()
{
// 加载资源但没有保存引用以供释放
Texture2D texture = Resources.Load<Texture2D>("Textures/MyTexture");
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.one * 0.5f);
}
// 没有释放资源!
}
// ✅ 正确示例 - 正确管理资源
public class GoodResourceExample : MonoBehaviour
{
private List<UnityEngine.Object> loadedAssets = new List<UnityEngine.Object>();
void Start()
{
LoadResources();
}
void LoadResources()
{
Texture2D texture = Resources.Load<Texture2D>("Textures/MyTexture");
if (texture != null)
{
loadedAssets.Add(texture);
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.one * 0.5f);
loadedAssets.Add(sprite);
}
}
void OnDestroy()
{
// 释放所有加载的资源
foreach (UnityEngine.Object asset in loadedAssets)
{
if (asset != null)
{
Resources.UnloadAsset(asset);
}
}
loadedAssets.Clear();
}
}
// ✅ 更好的解决方案 - 资源管理器
public class ResourceManager : MonoBehaviour
{
private Dictionary<string, UnityEngine.Object> loadedAssets = new Dictionary<string, UnityEngine.Object>();
private Dictionary<string, int> referenceCounts = new Dictionary<string, int>();
public T LoadAsset<T>(string path) where T : UnityEngine.Object
{
if (loadedAssets.ContainsKey(path))
{
// 增加引用计数
referenceCounts[path]++;
return loadedAssets[path] as T;
}
T asset = Resources.Load<T>(path);
if (asset != null)
{
loadedAssets[path] = asset;
referenceCounts[path] = 1;
}
return asset;
}
public void UnloadAsset(string path)
{
if (referenceCounts.ContainsKey(path))
{
referenceCounts[path]--;
if (referenceCounts[path] <= 0)
{
if (loadedAssets.ContainsKey(path))
{
Resources.UnloadAsset(loadedAssets[path]);
loadedAssets.Remove(path);
referenceCounts.Remove(path);
}
}
}
}
void OnDestroy()
{
// 清理所有资源
foreach (var asset in loadedAssets.Values)
{
if (asset != null)
{
Resources.UnloadAsset(asset);
}
}
loadedAssets.Clear();
referenceCounts.Clear();
}
}
4. 装箱和拆箱问题
问题: 频繁的装箱和拆箱操作影响性能。
// ❌ 错误示例 - 频繁装箱
public class BadBoxingExample : MonoBehaviour
{
void Start()
{
// 装箱操作 - 性能差
List<object> mixedList = new List<object>();
mixedList.Add(42); // int装箱为object
mixedList.Add(3.14f); // float装箱为object
mixedList.Add("string"); // string不装箱,但列表是object类型
// 拆箱操作 - 性能差
int value = (int)mixedList[0]; // 拆箱
}
}
// ✅ 正确示例 - 使用泛型避免装箱
public class GoodBoxingExample : MonoBehaviour
{
void Start()
{
// 使用泛型避免装箱
List<int> intList = new List<int>();
intList.Add(42); // 不装箱
intList.Add(100); // 不装箱
List<float> floatList = new List<float>();
floatList.Add(3.14f); // 不装箱
List<string> stringList = new List<string>();
stringList.Add("string"); // 不装箱
}
}
// ✅ 使用结构体避免装箱(适用于小对象)
[System.Serializable]
public struct PositionData
{
public Vector3 position;
public Quaternion rotation;
public int id;
public PositionData(Vector3 pos, Quaternion rot, int id)
{
position = pos;
rotation = rot;
this.id = id;
}
}
public class StructExample : MonoBehaviour
{
void Start()
{
// 结构体存储在栈上,避免装箱
PositionData data = new PositionData(Vector3.zero, Quaternion.identity, 1);
List<PositionData> dataList = new List<PositionData>();
dataList.Add(data); // 不装箱
}
}
Unity特定问题
1. Transform.position vs Transform.localPosition
问题: 混淆世界坐标和本地坐标。
// ❌ 错误示例 - 混淆坐标系统
public class BadTransformExample : MonoBehaviour
{
public Transform parent;
public Vector3 localOffset = Vector3.one;
void Update()
{
// 错误:试图在父对象旋转时保持本地偏移
// 这会导致位置计算错误
transform.position = parent.position + localOffset;
}
}
// ✅ 正确示例 - 正确使用本地坐标
public class GoodTransformExample : MonoBehaviour
{
public Transform parent;
public Vector3 localOffset = Vector3.one;
void Update()
{
// 正确:使用本地坐标
transform.localPosition = localOffset;
}
}
// ✅ 更好的解决方案 - 动态本地偏移
public class BetterTransformExample : MonoBehaviour
{
public Transform parent;
public Vector3 localOffset = Vector3.one;
void Update()
{
// 如果需要动态计算本地偏移
transform.position = parent.TransformPoint(localOffset);
}
}
2. Update vs FixedUpdate vs LateUpdate
问题: 在错误的更新方法中执行操作。
// ❌ 错误示例 - 在Update中处理物理
public class BadPhysicsExample : MonoBehaviour
{
public Rigidbody rb;
public float force = 10f;
void Update()
{
// 错误:在Update中应用物理力
// Update的帧率不稳定,物理计算会不准确
if (Input.GetKeyDown(KeyCode.Space))
{
rb.AddForce(Vector3.up * force, ForceMode.Impulse);
}
}
}
// ✅ 正确示例 - 在FixedUpdate中处理物理
public class GoodPhysicsExample : MonoBehaviour
{
public Rigidbody rb;
public float force = 10f;
void FixedUpdate()
{
// 正确:在FixedUpdate中应用物理力
// FixedUpdate以固定时间间隔调用,物理计算更准确
if (Input.GetKeyDown(KeyCode.Space))
{
rb.AddForce(Vector3.up * force, ForceMode.Impulse);
}
}
}
// ✅ Update、FixedUpdate、LateUpdate的正确使用
public class UpdateTimingExample : MonoBehaviour
{
private Transform playerTransform;
private Camera mainCamera;
void Start()
{
mainCamera = Camera.main;
}
void Update()
{
// Update: 处理输入、动画、UI更新等
// 帧率可变,适合处理与帧率相关的逻辑
HandleInput();
UpdateAnimation();
}
void FixedUpdate()
{
// FixedUpdate: 处理物理、刚体操作等
// 固定时间间隔,适合物理计算
ApplyPhysicsForces();
}
void LateUpdate()
{
// LateUpdate: 处理跟随相机、后处理等
// 在所有Update和FixedUpdate之后执行
FollowPlayer();
}
void HandleInput()
{
// 处理玩家输入
}
void UpdateAnimation()
{
// 更新动画参数
}
void ApplyPhysicsForces()
{
// 应用力和扭矩
}
void FollowPlayer()
{
// 相机跟随逻辑
if (mainCamera != null && playerTransform != null)
{
mainCamera.transform.position = playerTransform.position + Vector3.back * 10f;
}
}
}
3. 协程中的Time.time vs Time.deltaTime
问题: 在协程中错误使用时间函数。
// ❌ 错误示例 - 在协程中使用Time.time进行等待
public class BadCoroutineTimeExample : MonoBehaviour
{
void Start()
{
StartCoroutine(WaitUsingTime());
}
System.Collections.IEnumerator WaitUsingTime()
{
float startTime = Time.time;
float waitDuration = 2f;
// 错误:这种等待方式不够精确
while (Time.time - startTime < waitDuration)
{
// 空循环,效率低
yield return null;
}
Debug.Log("Wait completed");
}
}
// ✅ 正确示例 - 在协程中使用WaitForSeconds
public class GoodCoroutineTimeExample : MonoBehaviour
{
void Start()
{
StartCoroutine(WaitUsingWaitForSeconds());
}
System.Collections.IEnumerator WaitUsingWaitForSeconds()
{
// 正确:使用Unity内置的等待函数
yield return new WaitForSeconds(2f);
Debug.Log("Wait completed");
}
}
// ✅ 更好的解决方案 - 使用时间累积
public class BetterCoroutineTimeExample : MonoBehaviour
{
void Start()
{
StartCoroutine(WaitWithAccumulatedTime());
}
System.Collections.IEnumerator WaitWithAccumulatedTime()
{
float elapsedTime = 0f;
float waitDuration = 2f;
while (elapsedTime < waitDuration)
{
elapsedTime += Time.deltaTime;
yield return null;
}
Debug.Log("Wait completed");
}
}
4. 单例模式的常见错误
问题: 单例模式实现不当导致问题。
// ❌ 错误示例 - 简单的单例实现
public class BadSingletonExample : MonoBehaviour
{
public static BadSingletonExample Instance;
void Awake()
{
// 问题1: 没有检查是否已存在实例
// 问题2: 没有防止重复创建
Instance = this;
}
}
// ❌ 另一个错误示例 - 没有防止重复创建
public class AnotherBadSingletonExample : MonoBehaviour
{
public static AnotherBadSingletonExample Instance;
void Awake()
{
// 如果场景中有多个该组件的实例,都会执行这个
Instance = this;
// 这会导致后面的实例覆盖前面的实例
}
}
// ✅ 正确示例 - 线程安全的单例
public class GoodSingletonExample : MonoBehaviour
{
private static GoodSingletonExample _instance;
private static readonly object _lock = new object();
private static bool _applicationIsQuitting = false;
public static GoodSingletonExample Instance
{
get
{
if (_applicationIsQuitting)
{
Debug.LogWarning("单例在应用程序退出时被访问!");
return null;
}
lock (_lock)
{
if (_instance == null)
{
_instance = FindObjectOfType<GoodSingletonExample>();
if (_instance == null)
{
GameObject singletonObject = new GameObject("GoodSingletonExample");
_instance = singletonObject.AddComponent<GoodSingletonExample>();
}
if (_instance != null)
{
DontDestroyOnLoad(_instance);
}
}
return _instance;
}
}
}
void Awake()
{
if (_instance != null && _instance != this)
{
// 如果已经存在实例,销毁新的实例
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
void OnDestroy()
{
if (_instance == this)
{
_applicationIsQuitting = true;
}
}
}
// ✅ 泛型单例基类 - 可复用的单例实现
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
private static readonly object _lock = new object();
private static bool _applicationIsQuitting = false;
public static T Instance
{
get
{
if (_applicationIsQuitting)
{
Debug.LogWarning($"单例 {typeof(T)} 在应用程序退出时被访问!");
return null;
}
lock (_lock)
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject singletonObject = new GameObject(typeof(T).Name);
_instance = singletonObject.AddComponent<T>();
}
if (_instance != null)
{
DontDestroyOnLoad(_instance);
}
}
return _instance;
}
}
}
protected virtual void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
protected virtual void OnDestroy()
{
if (_instance == this)
{
_applicationIsQuitting = true;
}
}
}
// 使用泛型单例
public class GameManager : Singleton<GameManager>
{
public int score = 0;
public int lives = 3;
public void AddScore(int points)
{
score += points;
}
}
C#语言陷阱
1. 值类型 vs 引用类型
问题: 混淆值类型和引用类型的行为。
// ❌ 错误示例 - 对值类型的误解
public class ValueTypeMistakeExample : MonoBehaviour
{
void Start()
{
int x = 10;
int y = x; // 值复制
y = 20; // 只改变y,不影响x
Debug.Log($"x: {x}, y: {y}"); // x: 10, y: 20 - 正确
// 但是对引用类型:
List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = list1; // 引用复制,指向同一个对象
list2.Add(4); // 同时影响list1和list2
Debug.Log($"list1: {string.Join(",", list1)}"); // list1: 1,2,3,4
Debug.Log($"list2: {string.Join(",", list2)}"); // list2: 1,2,3,4
}
}
// ✅ 正确示例 - 理解值类型和引用类型
public class ValueTypeCorrectExample : MonoBehaviour
{
void Start()
{
// 值类型示例
int x = 10;
int y = x; // 完全独立的副本
y = 20;
Debug.Log($"x: {x}, y: {y}"); // x: 10, y: 20
// 引用类型示例 - 创建独立副本
List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = new List<int>(list1); // 创建新列表,复制元素
list2.Add(4); // 只影响list2
Debug.Log($"list1: {string.Join(",", list1)}"); // list1: 1,2,3
Debug.Log($"list2: {string.Join(",", list2)}"); // list2: 1,2,3,4
}
}
// ✅ 结构体作为值类型
public struct Point
{
public int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
public class StructExample : MonoBehaviour
{
void Start()
{
Point p1 = new Point(1, 2);
Point p2 = p1; // 值复制
p2.x = 10; // 只改变p2,不影响p1
Debug.Log($"p1: ({p1.x}, {p1.y})"); // p1: (1, 2)
Debug.Log($"p2: ({p2.x}, {p2.y})"); // p2: (10, 2)
}
}
2. 字符串不可变性
问题: 忽略字符串的不可变性导致性能问题。
// ❌ 错误示例 - 频繁字符串拼接
public class BadStringExample : MonoBehaviour
{
void Start()
{
string result = "";
// 每次拼接都创建新的字符串对象
for (int i = 0; i < 1000; i++)
{
result += "Item " + i + ", "; // 每次都创建新字符串
}
Debug.Log(result);
}
}
// ✅ 正确示例 - 使用StringBuilder
public class GoodStringExample : MonoBehaviour
{
void Start()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append("Item ");
sb.Append(i);
sb.Append(", ");
}
string result = sb.ToString();
Debug.Log(result);
}
}
// ✅ 使用string.Join替代循环拼接
public class BetterStringExample : MonoBehaviour
{
void Start()
{
List<string> items = new List<string>();
for (int i = 0; i < 1000; i++)
{
items.Add($"Item {i}");
}
string result = string.Join(", ", items);
Debug.Log(result);
}
}
3. Lambda表达式和闭包陷阱
问题: Lambda表达式中的闭包陷阱。
// ❌ 错误示例 - 循环中的闭包陷阱
public class BadClosureExample : MonoBehaviour
{
void Start()
{
List<System.Action> actions = new List<System.Action>();
// 问题:所有Lambda都捕获同一个变量i
for (int i = 0; i < 5; i++)
{
actions.Add(() => Debug.Log($"Value: {i}")); // 所有Lambda都引用同一个i
}
// 执行时,i的值是5(循环结束后的值)
foreach (var action in actions)
{
action(); // 输出5次 "Value: 5"
}
}
}
// ✅ 正确示例 - 解决闭包陷阱
public class GoodClosureExample : MonoBehaviour
{
void Start()
{
List<System.Action> actions = new List<System.Action>();
for (int i = 0; i < 5; i++)
{
int localCopy = i; // 创建局部副本
actions.Add(() => Debug.Log($"Value: {localCopy}")); // 捕获局部副本
}
foreach (var action in actions)
{
action(); // 正确输出 "Value: 0", "Value: 1", ...
}
}
}
// ✅ 使用方法组避免闭包
public class BetterClosureExample : MonoBehaviour
{
void Start()
{
List<System.Action> actions = new List<System.Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => LogValue(i)); // 传递i的当前值
}
foreach (var action in actions)
{
action();
}
}
void LogValue(int value)
{
Debug.Log($"Value: {value}");
}
}
4. 数组越界和空引用
问题: 没有正确处理数组边界和空引用。
// ❌ 错误示例 - 没有边界检查
public class BadArrayExample : MonoBehaviour
{
public int[] numbers = { 1, 2, 3 };
void Start()
{
// 问题1: 可能的数组越界
Debug.Log(numbers[5]); // 索引超出范围,抛出异常
// 问题2: 没有检查空引用
int[] nullArray = null;
Debug.Log(nullArray.Length); // 空引用异常
}
}
// ✅ 正确示例 - 安全的数组访问
public class GoodArrayExample : MonoBehaviour
{
public int[] numbers = { 1, 2, 3 };
void Start()
{
// 安全访问数组
if (numbers != null && numbers.Length > 0)
{
if (numbers.Length > 5)
{
Debug.Log(numbers[5]);
}
else
{
Debug.LogWarning("数组长度不足,无法访问索引5");
}
}
// 安全处理可能为空的数组
int[] arrayToCheck = GetArray();
if (arrayToCheck != null)
{
Debug.Log($"数组长度: {arrayToCheck.Length}");
}
else
{
Debug.LogWarning("数组为空");
}
}
int[] GetArray()
{
// 某些条件下可能返回null
return null;
}
}
// ✅ 使用扩展方法提供安全访问
public static class ArrayExtensions
{
public static T SafeGet<T>(this T[] array, int index, T defaultValue = default(T))
{
if (array != null && index >= 0 && index < array.Length)
{
return array[index];
}
return defaultValue;
}
public static bool IsValidIndex<T>(this T[] array, int index)
{
return array != null && index >= 0 && index < array.Length;
}
}
public class SafeArrayExample : MonoBehaviour
{
void Start()
{
int[] numbers = { 1, 2, 3 };
// 安全访问
int value = numbers.SafeGet(5, -1); // 返回默认值-1
Debug.Log($"安全访问结果: {value}");
// 检查索引有效性
if (numbers.IsValidIndex(2))
{
Debug.Log($"有效索引访问: {numbers[2]}");
}
}
}
多线程和异步问题
1. Unity主线程限制
问题: 在非主线程访问Unity API。
// ❌ 错误示例 - 在后台线程访问Unity API
public class BadThreadingExample : MonoBehaviour
{
void Start()
{
System.Threading.Thread thread = new System.Threading.Thread(BackgroundWork);
thread.Start();
}
void BackgroundWork()
{
// 错误:在后台线程访问Unity API
// 这会导致未定义行为和崩溃
transform.position = Vector3.one; // 不能在后台线程执行
Debug.Log("This is dangerous!"); // Debug.Log也不是线程安全的
}
}
// ✅ 正确示例 - 使用协程或主线程回调
public class GoodThreadingExample : MonoBehaviour
{
private Queue<System.Action> mainThreadActions = new Queue<System.Action>();
private object lockObject = new object();
void Start()
{
System.Threading.Thread thread = new System.Threading.Thread(BackgroundWork);
thread.Start();
}
void BackgroundWork()
{
// 在后台线程执行耗时操作
string result = ExpensiveOperation();
// 将需要在主线程执行的代码放入队列
lock (lockObject)
{
mainThreadActions.Enqueue(() => {
// 在主线程执行Unity API调用
transform.position = Vector3.one;
Debug.Log($"Result: {result}");
});
}
}
void Update()
{
// 在主线程处理后台线程的结果
lock (lockObject)
{
while (mainThreadActions.Count > 0)
{
System.Action action = mainThreadActions.Dequeue();
action?.Invoke();
}
}
}
string ExpensiveOperation()
{
// 模拟耗时操作
System.Threading.Thread.Sleep(1000);
return "Operation completed";
}
}
// ✅ 使用UnityWebRequest异步操作
public class AsyncWebRequestExample : MonoBehaviour
{
void Start()
{
StartCoroutine(FetchDataAsync());
}
System.Collections.IEnumerator FetchDataAsync()
{
using (UnityWebRequest request = UnityWebRequest.Get("https://api.example.com/data"))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// 在主线程处理结果
string result = request.downloadHandler.text;
ProcessResult(result);
}
else
{
Debug.LogError($"Request failed: {request.error}");
}
}
}
void ProcessResult(string result)
{
// 在主线程处理结果
Debug.Log($"Received: {result}");
}
}
2. 异步操作的正确处理
问题: 异步操作没有正确处理完成状态。
// ❌ 错误示例 - 没有正确处理异步操作
public class BadAsyncExample : MonoBehaviour
{
void Start()
{
StartCoroutine(LoadDataAsync());
// 立即使用数据,但异步操作可能还没完成
UseData(); // 数据可能还没加载完成!
}
System.Collections.IEnumerator LoadDataAsync()
{
// 模拟异步加载
yield return new WaitForSeconds(2f);
Debug.Log("Data loaded");
}
void UseData()
{
// 这里使用数据,但数据可能还没准备好
Debug.Log("Using data...");
}
}
// ✅ 正确示例 - 正确处理异步操作
public class GoodAsyncExample : MonoBehaviour
{
private bool dataLoaded = false;
private string loadedData = "";
void Start()
{
StartCoroutine(LoadDataAsync());
}
System.Collections.IEnumerator LoadDataAsync()
{
// 模拟异步加载
yield return new WaitForSeconds(2f);
loadedData = "Important data";
dataLoaded = true;
// 数据加载完成后执行依赖操作
OnDataLoaded();
}
void OnDataLoaded()
{
// 数据加载完成后的回调
UseData();
}
void UseData()
{
if (dataLoaded)
{
Debug.Log($"Using data: {loadedData}");
}
else
{
Debug.LogWarning("Data not loaded yet!");
}
}
}
// ✅ 使用async/await (需要Unity 2018.3+和.NET 4.x)
public class ModernAsyncExample : MonoBehaviour
{
async void Start()
{
string data = await LoadDataAsync();
UseData(data);
}
async System.Threading.Tasks.Task<string> LoadDataAsync()
{
// 模拟异步操作
await System.Threading.Tasks.Task.Delay(2000);
return "Async data loaded";
}
void UseData(string data)
{
Debug.Log($"Using async data: {data}");
}
}
UI系统问题
1. UI更新性能问题
问题: 频繁更新UI组件导致性能下降。
// ❌ 错误示例 - 频繁更新UI
public class BadUIUpdateExample : MonoBehaviour
{
public Text scoreText;
public Slider healthSlider;
public Image healthBarFill;
void Update()
{
// 每帧都更新UI组件,即使值没有改变
scoreText.text = "Score: " + GetScore(); // 每帧创建新字符串
healthSlider.value = GetHealthPercentage();
healthBarFill.fillAmount = GetHealthPercentage();
}
int GetScore() { return 100; }
float GetHealthPercentage() { return 0.5f; }
}
// ✅ 正确示例 - 优化UI更新
public class GoodUIUpdateExample : MonoBehaviour
{
public Text scoreText;
public Slider healthSlider;
public Image healthBarFill;
private int lastScore = -1;
private float lastHealthPercentage = -1f;
private string cachedScoreText = "";
void Update()
{
UpdateScoreUI();
UpdateHealthUI();
}
void UpdateScoreUI()
{
int currentScore = GetScore();
if (lastScore != currentScore)
{
lastScore = currentScore;
cachedScoreText = "Score: " + currentScore;
scoreText.text = cachedScoreText;
}
}
void UpdateHealthUI()
{
float currentHealth = GetHealthPercentage();
if (Mathf.Abs(lastHealthPercentage - currentHealth) > 0.01f) // 1%的变化阈值
{
lastHealthPercentage = currentHealth;
healthSlider.value = currentHealth;
healthBarFill.fillAmount = currentHealth;
}
}
int GetScore() { return 100; }
float GetHealthPercentage() { return 0.5f; }
}
// ✅ 使用UI更新管理器
public class UIUpdateManager : MonoBehaviour
{
private List<IUIUpdatable> updatableComponents = new List<IUIUpdatable>();
private float updateInterval = 0.1f; // 100ms更新一次
private float lastUpdateTime = 0f;
void Update()
{
if (Time.time - lastUpdateTime >= updateInterval)
{
UpdateAllUI();
lastUpdateTime = Time.time;
}
}
void UpdateAllUI()
{
foreach (IUIUpdatable updatable in updatableComponents)
{
updatable.UpdateUI();
}
}
public void RegisterUpdatable(IUIUpdatable updatable)
{
if (!updatableComponents.Contains(updatable))
{
updatableComponents.Add(updatable);
}
}
public void UnregisterUpdatable(IUIUpdatable updatable)
{
updatableComponents.Remove(updatable);
}
}
public interface IUIUpdatable
{
void UpdateUI();
}
public class OptimizedScoreUI : MonoBehaviour, IUIUpdatable
{
public Text scoreText;
private int currentScore = 0;
private int lastDisplayedScore = -1;
void Start()
{
UIUpdateManager manager = FindObjectOfType<UIUpdateManager>();
if (manager != null)
{
manager.RegisterUpdatable(this);
}
}
public void SetScore(int score)
{
currentScore = score;
}
public void UpdateUI()
{
if (lastDisplayedScore != currentScore)
{
lastDisplayedScore = currentScore;
if (scoreText != null)
{
scoreText.text = "Score: " + currentScore;
}
}
}
void OnDestroy()
{
UIUpdateManager manager = FindObjectOfType<UIUpdateManager>();
if (manager != null)
{
manager.UnregisterUpdatable(this);
}
}
}
2. UI事件处理陷阱
问题: UI事件处理不当导致内存泄漏或性能问题。
// ❌ 错误示例 - UI事件处理陷阱
public class BadUIEventExample : MonoBehaviour
{
public Button myButton;
void Start()
{
// 问题1: 多次添加相同事件处理器
myButton.onClick.AddListener(OnClick);
myButton.onClick.AddListener(OnClick); // 重复添加!
}
void OnClick()
{
Debug.Log("Button clicked");
}
void OnDestroy()
{
// 问题2: 没有移除事件监听器
// 这可能导致内存泄漏
}
}
// ✅ 正确示例 - 正确处理UI事件
public class GoodUIEventExample : MonoBehaviour
{
public Button myButton;
private bool eventListenerAdded = false;
void Start()
{
if (!eventListenerAdded)
{
myButton.onClick.AddListener(OnClick);
eventListenerAdded = true;
}
}
void OnClick()
{
Debug.Log("Button clicked");
}
void OnDestroy()
{
// 移除事件监听器防止内存泄漏
if (eventListenerAdded)
{
myButton.onClick.RemoveListener(OnClick);
eventListenerAdded = false;
}
}
}
// ✅ 使用事件管理器
public class UIEventManager : MonoBehaviour
{
private Dictionary<GameObject, List<System.Action>> eventHandlers =
new Dictionary<GameObject, List<System.Action>>();
public void AddButtonListener(Button button, System.Action onClick)
{
if (!eventHandlers.ContainsKey(button.gameObject))
{
eventHandlers[button.gameObject] = new List<System.Action>();
}
if (!eventHandlers[button.gameObject].Contains(onClick))
{
button.onClick.AddListener(onClick);
eventHandlers[button.gameObject].Add(onClick);
}
}
public void RemoveButtonListener(Button button, System.Action onClick)
{
if (eventHandlers.ContainsKey(button.gameObject))
{
button.onClick.RemoveListener(onClick);
eventHandlers[button.gameObject].Remove(onClick);
}
}
void OnDestroy()
{
// 清理所有事件监听器
foreach (var pair in eventHandlers)
{
Button button = pair.Key.GetComponent<Button>();
if (button != null)
{
foreach (System.Action handler in pair.Value)
{
button.onClick.RemoveListener(handler);
}
}
}
eventHandlers.Clear();
}
}
音频系统问题
1. 音频播放性能问题
问题: 频繁播放音频导致性能下降。
// ❌ 错误示例 - 频繁播放音频
public class BadAudioExample : MonoBehaviour
{
public AudioClip footstepSound;
void Update()
{
// 每帧都检查并可能播放音频
if (Input.GetKeyDown(KeyCode.Space))
{
AudioSource.PlayClipAtPoint(footstepSound, transform.position); // 每次都创建新的AudioSource
}
}
}
// ✅ 正确示例 - 优化音频播放
public class GoodAudioExample : MonoBehaviour
{
public AudioClip footstepSound;
private AudioSource audioSource;
void Start()
{
// 预先创建AudioSource
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f; // 3D音频
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
PlayAudio();
}
}
void PlayAudio()
{
if (audioSource != null && footstepSound != null)
{
// 重用AudioSource
audioSource.clip = footstepSound;
audioSource.Play();
}
}
}
// ✅ 使用音频对象池
public class AudioPoolExample : MonoBehaviour
{
private Queue<AudioSource> audioSourcePool = new Queue<AudioSource>();
private int poolSize = 10;
void Start()
{
// 预创建音频源池
for (int i = 0; i < poolSize; i++)
{
GameObject audioGO = new GameObject($"AudioSource_{i}");
audioGO.transform.SetParent(transform);
AudioSource audioSource = audioGO.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSourcePool.Enqueue(audioSource);
}
}
public void PlaySound(AudioClip clip, Vector3 position)
{
AudioSource audioSource;
if (audioSourcePool.Count > 0)
{
audioSource = audioSourcePool.Dequeue();
audioSource.transform.position = position;
}
else
{
// 如果池空了,创建新的(应该避免这种情况)
GameObject audioGO = new GameObject("AudioSource_Dynamic");
audioGO.transform.SetParent(transform);
audioSource = audioGO.AddComponent<AudioSource>();
}
audioSource.clip = clip;
audioSource.Play();
// 音频播放完成后返回池中
StartCoroutine(ReturnToPoolAfterPlay(audioSource, clip.length));
}
System.Collections.IEnumerator ReturnToPoolAfterPlay(AudioSource audioSource, float clipLength)
{
yield return new WaitForSeconds(clipLength + 0.1f); // 额外等待确保播放完成
audioSource.clip = null;
audioSource.Stop();
audioSource.transform.SetParent(transform); // 重置父对象
audioSourcePool.Enqueue(audioSource);
}
}
2. 音频资源管理
问题: 音频资源没有正确管理导致内存泄漏。
// ❌ 错误示例 - 音频资源管理不当
public class BadAudioResourceExample : MonoBehaviour
{
void Start()
{
// 直接加载音频资源,没有引用管理
AudioClip clip = Resources.Load<AudioClip>("Audio/MySound");
// clip没有被保存引用,可能被GC回收
}
}
// ✅ 正确示例 - 正确管理音频资源
public class GoodAudioResourceExample : MonoBehaviour
{
private AudioClip loadedClip;
private List<AudioClip> audioClips = new List<AudioClip>();
void Start()
{
// 正确加载并保存引用
loadedClip = Resources.Load<AudioClip>("Audio/MySound");
if (loadedClip != null)
{
audioClips.Add(loadedClip);
}
}
void OnDestroy()
{
// 释放音频资源
foreach (AudioClip clip in audioClips)
{
if (clip != null)
{
Resources.UnloadAsset(clip);
}
}
audioClips.Clear();
}
}
动画系统问题
1. 动画状态机陷阱
问题: 动画状态机配置不当导致问题。
// ❌ 错误示例 - 动画参数使用字符串
public class BadAnimationExample : MonoBehaviour
{
private Animator animator;
void Start()
{
animator = GetComponent<Animator>();
}
void Update()
{
// 使用字符串进行动画参数设置 - 性能较差
float speed = Input.GetAxis("Vertical");
animator.SetFloat("Speed", speed); // 每次都要计算字符串的哈希值
}
}
// ✅ 正确示例 - 使用哈希值优化
public class GoodAnimationExample : MonoBehaviour
{
private Animator animator;
private int speedHash; // 预计算哈希值
void Start()
{
animator = GetComponent<Animator>();
speedHash = Animator.StringToHash("Speed"); // 预计算哈希值
}
void Update()
{
float speed = Input.GetAxis("Vertical");
// 使用预计算的哈希值 - 性能更好
animator.SetFloat(speedHash, speed);
}
}
// ✅ 动画事件优化
public class OptimizedAnimationExample : MonoBehaviour
{
private Animator animator;
private int speedHash;
private int directionHash;
private int isRunningHash;
void Start()
{
animator = GetComponent<Animator>();
// 预计算所有动画参数的哈希值
speedHash = Animator.StringToHash("Speed");
directionHash = Animator.StringToHash("Direction");
isRunningHash = Animator.StringToHash("IsRunning");
}
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
float speed = new Vector2(horizontal, vertical).magnitude;
// 使用预计算的哈希值设置参数
animator.SetFloat(speedHash, speed);
animator.SetFloat(directionHash, horizontal);
animator.SetBool(isRunningHash, speed > 0.5f);
}
// 动画事件处理器
void OnFootstep()
{
// 播放脚步声
PlayFootstepSound();
}
void OnAttack()
{
// 处理攻击逻辑
PerformAttack();
}
void PlayFootstepSound()
{
// 播放脚步声逻辑
}
void PerformAttack()
{
// 攻击逻辑
}
}
2. 动画性能优化
问题: 动画更新频率过高或动画复杂度过高。
// ❌ 错误示例 - 动画更新过于频繁
public class BadAnimationUpdateExample : MonoBehaviour
{
private Animator animator;
private float lastAnimationUpdateTime = 0f;
private float animationUpdateInterval = 0.016f; // 每帧更新
void Update()
{
// 每帧都更新动画参数,过于频繁
UpdateAnimationParameters();
}
void UpdateAnimationParameters()
{
if (Time.time - lastAnimationUpdateTime >= animationUpdateInterval)
{
// 更新动画参数
float speed = CalculateSpeed();
animator.SetFloat("Speed", speed);
lastAnimationUpdateTime = Time.time;
}
}
float CalculateSpeed()
{
// 复杂的速度计算
return 1f;
}
}
// ✅ 正确示例 - 优化动画更新频率
public class GoodAnimationUpdateExample : MonoBehaviour
{
private Animator animator;
private float lastAnimationUpdateTime = 0f;
private float animationUpdateInterval = 0.1f; // 100ms更新一次,而不是每帧
void Update()
{
// 降低动画参数更新频率
if (Time.time - lastAnimationUpdateTime >= animationUpdateInterval)
{
UpdateAnimationParameters();
lastAnimationUpdateTime = Time.time;
}
}
void UpdateAnimationParameters()
{
float speed = CalculateSpeed();
animator.SetFloat("Speed", speed);
}
float CalculateSpeed()
{
// 速度计算
return 1f;
}
}
// ✅ 动画LOD系统
public class AnimationLODExample : MonoBehaviour
{
private Animator animator;
private Camera mainCamera;
private float lodDistance = 20f; // LOD距离
private bool isLODActive = false;
void Start()
{
animator = GetComponent<Animator>();
mainCamera = Camera.main;
}
void Update()
{
UpdateAnimationLOD();
}
void UpdateAnimationLOD()
{
if (mainCamera != null)
{
float distance = Vector3.Distance(transform.position, mainCamera.transform.position);
bool shouldUseLOD = distance > lodDistance;
if (shouldUseLOD != isLODActive)
{
isLODActive = shouldUseLOD;
if (isLODActive)
{
// 远距离时简化动画
animator.cullingMode = AnimatorCullingMode.CullCompletely;
}
else
{
// 近距离时正常动画
animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
}
}
}
}
}
物理系统问题
1. 碰撞检测性能问题
问题: 频繁的碰撞检测影响性能。
// ❌ 错误示例 - 频繁的射线检测
public class BadPhysicsExample : MonoBehaviour
{
void Update()
{
// 每帧都进行射线检测 - 性能差
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
Debug.Log($"Hit: {hit.collider.name}");
}
}
}
// ✅ 正确示例 - 优化碰撞检测频率
public class GoodPhysicsExample : MonoBehaviour
{
private float lastRaycastTime = 0f;
private float raycastInterval = 0.1f; // 100ms检测一次
void Update()
{
if (Time.time - lastRaycastTime >= raycastInterval)
{
PerformRaycast();
lastRaycastTime = Time.time;
}
}
void PerformRaycast()
{
if (Camera.main != null)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
Debug.Log($"Hit: {hit.collider.name}");
}
}
}
}
// ✅ 使用Physics.RaycastNonAlloc优化
public class OptimizedPhysicsExample : MonoBehaviour
{
private RaycastHit[] raycastHits = new RaycastHit[10]; // 重用数组
private float lastRaycastTime = 0f;
private float raycastInterval = 0.1f;
void Update()
{
if (Time.time - lastRaycastTime >= raycastInterval)
{
PerformOptimizedRaycast();
lastRaycastTime = Time.time;
}
}
void PerformOptimizedRaycast()
{
if (Camera.main != null)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 使用NonAlloc版本避免内存分配
int hitCount = Physics.RaycastNonAlloc(ray, raycastHits);
for (int i = 0; i < hitCount; i++)
{
Debug.Log($"Hit: {raycastHits[i].collider.name}");
}
}
}
}
2. 刚体配置问题
问题: 刚体配置不当导致物理行为异常。
// ❌ 错误示例 - 刚体配置不当
public class BadRigidbodyExample : MonoBehaviour
{
public Rigidbody rb;
void Start()
{
// 错误配置:同时启用isKinematic和useGravity
rb.isKinematic = true;
rb.useGravity = true; // 这两个设置冲突
// 错误:在Update中频繁修改物理属性
rb.drag = 0; // 阻力为0可能导致物体加速过快
rb.angularDrag = 0; // 角阻力为0可能导致旋转不稳定
}
void Update()
{
// 错误:在Update中直接修改position和rotation
// 这会破坏物理模拟
rb.position = transform.position + Vector3.forward * Time.deltaTime;
}
}
// ✅ 正确示例 - 正确配置刚体
public class GoodRigidbodyExample : MonoBehaviour
{
public Rigidbody rb;
public float moveForce = 10f;
void Start()
{
// 正确配置刚体属性
rb.isKinematic = false; // 不是运动学刚体
rb.useGravity = true; // 使用重力
rb.drag = 1f; // 适当的阻力
rb.angularDrag = 1f; // 适当的角阻力
rb.interpolation = RigidbodyInterpolation.Interpolate; // 平滑插值
}
void FixedUpdate()
{
// 在FixedUpdate中应用物理力
if (Input.GetKey(KeyCode.W))
{
rb.AddForce(Vector3.forward * moveForce);
}
}
}
// ✅ 运动学刚体的正确使用
public class KinematicRigidbodyExample : MonoBehaviour
{
public Rigidbody rb;
void Start()
{
// 运动学刚体:不受物理影响,但可以影响其他物理对象
rb.isKinematic = true;
rb.useGravity = false; // 运动学刚体不使用重力
}
void Update()
{
// 对于运动学刚体,使用MovePosition和MoveRotation
Vector3 newPosition = transform.position + Vector3.forward * Time.deltaTime;
rb.MovePosition(newPosition);
}
}
网络和多人游戏问题
1. 网络同步问题
问题: 网络同步实现不当导致问题。
// ❌ 错误示例 - 简单的网络同步(容易被作弊)
public class BadNetworkSyncExample : MonoBehaviour
{
public Vector3 networkPosition;
public float interpolationSpeed = 10f;
void Update()
{
// 直接插值到网络位置 - 容易出现延迟和抖动
transform.position = Vector3.Lerp(transform.position, networkPosition,
interpolationSpeed * Time.deltaTime);
}
}
// ✅ 正确示例 - 带有延迟补偿的网络同步
public class GoodNetworkSyncExample : MonoBehaviour
{
private Vector3 targetPosition;
private float lastUpdateTime = 0f;
private float updateInterval = 0.1f; // 100ms更新一次
private float interpolationSpeed = 5f;
// 服务器时间戳
private float serverTimeOffset = 0f;
void Update()
{
// 平滑插值到目标位置
transform.position = Vector3.Lerp(transform.position, targetPosition,
interpolationSpeed * Time.deltaTime);
}
// 从网络接收位置更新
public void OnPositionUpdate(Vector3 newPosition, float timestamp)
{
// 计算时间差并应用延迟补偿
float timeDiff = Time.time - timestamp + serverTimeOffset;
targetPosition = newPosition;
// 预测未来位置以补偿网络延迟
if (timeDiff > 0)
{
// 这里可以添加预测逻辑
targetPosition += PredictMovement(timeDiff);
}
}
Vector3 PredictMovement(float timeAhead)
{
// 简单的预测逻辑(实际项目中需要更复杂的预测)
return Vector3.zero;
}
}
// ✅ 网络对象池
public class NetworkObjectPool : MonoBehaviour
{
private Queue<GameObject> networkObjectPool = new Queue<GameObject>();
private GameObject prefab;
private int poolSize = 20;
void Start()
{
// 预创建网络对象池
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
networkObjectPool.Enqueue(obj);
}
}
public GameObject GetNetworkObject()
{
if (networkObjectPool.Count > 0)
{
GameObject obj = networkObjectPool.Dequeue();
obj.SetActive(true);
return obj;
}
else
{
// 如果池空了,创建新对象(应该避免)
return Instantiate(prefab);
}
}
public void ReturnNetworkObject(GameObject obj)
{
obj.SetActive(false);
obj.transform.SetParent(transform);
networkObjectPool.Enqueue(obj);
}
}
2. 网络消息处理
问题: 网络消息处理不当导致安全问题。
// ❌ 错误示例 - 没有验证的网络消息处理
public class BadNetMessageExample : MonoBehaviour
{
public int playerHealth = 100;
// 直接应用网络消息中的值,没有验证
public void OnHealthUpdate(int newHealth)
{
// 安全问题:客户端可以发送任意值
playerHealth = newHealth;
}
}
// ✅ 正确示例 - 验证网络消息
public class GoodNetMessageExample : MonoBehaviour
{
public int playerHealth = 100;
public int maxHealth = 100;
private int lastValidHealth = 100;
public void OnHealthUpdate(int newHealth, int damageSource)
{
// 验证健康值的合理性
if (IsValidHealthUpdate(newHealth, damageSource))
{
playerHealth = Mathf.Clamp(newHealth, 0, maxHealth);
lastValidHealth = playerHealth;
}
else
{
// 检测到可能的作弊,重置为上一个有效值
playerHealth = lastValidHealth;
Debug.LogWarning("Invalid health update detected, resetting to last valid value");
}
}
bool IsValidHealthUpdate(int newHealth, int damageSource)
{
// 检查健康值是否在合理范围内
if (newHealth < 0 || newHealth > maxHealth)
{
return false;
}
// 检查健康值变化是否合理
int healthChange = newHealth - lastValidHealth;
// 如果是恢复,检查恢复量是否合理
if (healthChange > 0 && healthChange > 50) // 假设单次恢复不超过50点
{
return false;
}
// 可以添加更多验证逻辑
return true;
}
}
实践练习
练习1: 性能监控工具
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
// 性能监控工具
public class PerformanceMonitor : MonoBehaviour
{
[Header("UI References")]
public Text fpsText;
public Text memoryText;
public Text drawCallText;
public Text objectCountText;
public Slider fpsSlider;
[Header("Settings")]
public float updateInterval = 0.5f;
public int fpsWarningThreshold = 30;
public int fpsCriticalThreshold = 20;
private float lastUpdateTime = 0f;
private List<float> fpsHistory = new List<float>();
private const int historySize = 50;
void Update()
{
if (Time.time - lastUpdateTime >= updateInterval)
{
UpdatePerformanceInfo();
lastUpdateTime = Time.time;
}
}
private void UpdatePerformanceInfo()
{
// 更新FPS
float currentFPS = 1.0f / Time.unscaledDeltaTime;
UpdateFPSInfo(currentFPS);
// 更新内存信息
UpdateMemoryInfo();
// 更新渲染统计
UpdateRenderStats();
// 更新对象计数
UpdateObjectCount();
}
private void UpdateFPSInfo(float fps)
{
if (fpsText != null)
{
fpsText.text = $"FPS: {fps:F1}";
// 根据FPS设置颜色
if (fps >= fpsWarningThreshold)
{
fpsText.color = Color.green;
}
else if (fps >= fpsCriticalThreshold)
{
fpsText.color = Color.yellow;
}
else
{
fpsText.color = Color.red;
}
}
// 更新FPS滑块
if (fpsSlider != null)
{
fpsSlider.value = Mathf.Clamp01(fps / 60f);
}
// 更新历史记录
fpsHistory.Add(fps);
if (fpsHistory.Count > historySize)
{
fpsHistory.RemoveAt(0);
}
}
private void UpdateMemoryInfo()
{
if (memoryText != null)
{
long memoryUsage = System.GC.GetTotalMemory(false);
string memoryStr = FormatBytes(memoryUsage);
memoryText.text = $"Memory: {memoryStr}";
}
}
private void UpdateRenderStats()
{
if (drawCallText != null)
{
// 注意:Unity没有直接API获取draw call数量
// 这里简化显示
drawCallText.text = "Draw Calls: N/A";
}
}
private void UpdateObjectCount()
{
if (objectCountText != null)
{
int objectCount = FindObjectsOfType<GameObject>().Length;
objectCountText.text = $"Objects: {objectCount}";
}
}
private string FormatBytes(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB" };
int counter = 0;
decimal number = (decimal)bytes;
while (Math.Abs(number) >= 1024m && counter < suffixes.Length - 1)
{
counter++;
number /= 1024m;
}
return string.Format("{0:n1} {1}", number, suffixes[counter]);
}
// 强制垃圾回收
public void ForceGarbageCollection()
{
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
}
// 卸载未使用的资源
public void UnloadUnusedAssets()
{
Resources.UnloadUnusedAssets();
}
}
练习2: 代码质量检查工具
using UnityEngine;
using System.Collections.Generic;
using System.Text;
using System.Linq;
// 代码质量检查工具
public class CodeQualityChecker : MonoBehaviour
{
[Header("Check Settings")]
public bool checkForGODObjects = true;
public int maxComponentCount = 10;
public bool checkForDeepNesting = true;
public int maxNestingLevel = 5;
public bool checkForExpensiveOperations = true;
private List<QualityIssue> issues = new List<QualityIssue>();
[System.Serializable]
public class QualityIssue
{
public string objectName;
public string componentName;
public string issueType;
public string description;
public int severity; // 1 = Low, 2 = Medium, 3 = High
public System.DateTime timestamp;
}
public void RunQualityCheck()
{
issues.Clear();
Debug.Log("Running code quality check...");
if (checkForGODObjects)
{
CheckForGODObjects();
}
if (checkForDeepNesting)
{
CheckForDeepNestingIssues();
}
GenerateQualityReport();
}
private void CheckForGODObjects()
{
GameObject[] allObjects = FindObjectsOfType<GameObject>();
foreach (GameObject go in allObjects)
{
Component[] components = go.GetComponents<Component>();
if (components.Length > maxComponentCount)
{
AddQualityIssue(go.name, "GOD Object",
$"Object has {components.Length} components, exceeding recommended {maxComponentCount}",
2);
}
}
}
private void CheckForDeepNestingIssues()
{
Transform[] allTransforms = FindObjectsOfType<Transform>();
foreach (Transform t in allTransforms)
{
int depth = GetTransformDepth(t);
if (depth > maxNestingLevel)
{
AddQualityIssue(t.name, "Deep Nesting",
$"Transform nesting level is {depth}, exceeding recommended {maxNestingLevel}",
2);
}
}
}
private int GetTransformDepth(Transform transform)
{
int depth = 0;
Transform current = transform;
while (current.parent != null)
{
depth++;
current = current.parent;
}
return depth;
}
private void AddQualityIssue(string objectName, string issueType, string description, int severity)
{
QualityIssue issue = new QualityIssue
{
objectName = objectName,
componentName = issueType,
issueType = issueType,
description = description,
severity = severity,
timestamp = System.DateTime.Now
};
issues.Add(issue);
}
private void GenerateQualityReport()
{
StringBuilder report = new StringBuilder();
report.AppendLine("=== Code Quality Report ===");
report.AppendLine($"Check Time: {System.DateTime.Now}");
report.AppendLine($"Issues Found: {issues.Count}");
if (issues.Count > 0)
{
report.AppendLine("\nIssue Details:");
var sortedIssues = issues.OrderByDescending(i => i.severity).ToList();
foreach (QualityIssue issue in sortedIssues)
{
string severityStr = issue.severity == 3 ? "High" :
issue.severity == 2 ? "Medium" : "Low";
report.AppendLine($"[{severityStr}] {issue.objectName}.{issue.componentName}: {issue.description}");
}
}
else
{
report.AppendLine("\n✅ No code quality issues found!");
}
Debug.Log(report.ToString());
}
public void GenerateFixSuggestions()
{
StringBuilder suggestions = new StringBuilder();
suggestions.AppendLine("=== Fix Suggestions ===");
int godObjectCount = issues.Count(i => i.issueType == "GOD Object");
int nestingIssues = issues.Count(i => i.issueType.Contains("Nesting"));
if (godObjectCount > 0)
{
suggestions.AppendLine($"• Split {godObjectCount} GOD objects into smaller components");
suggestions.AppendLine(" - Use composition over inheritance");
suggestions.AppendLine(" - Group related functionality into separate scripts");
}
if (nestingIssues > 0)
{
suggestions.AppendLine($"• Reduce {nestingIssues} deep nesting issues");
suggestions.AppendLine(" - Consider using object pooling to reduce hierarchy");
suggestions.AppendLine(" - Redesign scene hierarchy structure");
}
Debug.Log(suggestions.ToString());
}
public float GetQualityScore()
{
if (issues.Count == 0) return 100f;
int totalSeverity = issues.Sum(i => i.severity);
float score = Mathf.Clamp(100f - (issues.Count * 5 + totalSeverity * 10), 0f, 100f);
return score;
}
}
常见错误总结
性能相关(10个)
- GetComponent滥用 - 在Update中频繁调用GetComponent
- GameObject.Find滥用 - 在Update中使用GameObject.Find
- 频繁对象创建销毁 - 在Update中创建/销毁对象
- 字符串操作 - 频繁的字符串拼接和操作
- 空引用检查 - 忘记检查空引用导致异常
- 数组越界 - 访问数组时没有边界检查
- 协程管理 - 协程启动后没有正确停止
- 事件订阅 - 事件订阅后没有取消导致内存泄漏
- 物理检测 - 频繁的射线检测和碰撞检测
- UI更新 - 频繁更新UI组件
内存管理相关(8个)
- 资源未释放 - 加载的资源没有正确释放
- 装箱拆箱 - 频繁的值类型装箱操作
- 对象池 - 没有使用对象池导致GC压力
- 事件泄漏 - 事件订阅后没有取消
- 协程泄漏 - 协程没有正确停止
- 引用保持 - 保持不必要的对象引用
- 字符串缓存 - 没有缓存频繁使用的字符串
- 集合扩容 - 没有预分配集合大小
Unity特定(7个)
- 坐标系统 - 混淆世界坐标和本地坐标
- 更新方法 - 在错误的更新方法中执行操作
- 单例模式 - 单例实现不当
- 生命周期 - 错误的组件生命周期使用
- 物理配置 - 刚体配置不当
- 动画参数 - 使用字符串而非哈希值
- 场景管理 - 场景切换时的资源管理
C#语言(5个)
- 值引用类型 - 混淆值类型和引用类型
- 字符串不可变 - 忽略字符串的不可变性
- 闭包陷阱 - 循环中的闭包陷阱
- 数组访问 - 没有边界检查的数组访问
- 异常处理 - 不当的异常处理
网络相关(3个)
- 消息验证 - 网络消息没有验证
- 同步问题 - 网络同步实现不当
- 安全问题 - 客户端验证不足
解决方案速查表
| 问题类型 | 常见表现 | 解决方案 |
|---|---|---|
| 性能问题 | 游戏卡顿、帧率下降 | 缓存组件引用、使用对象池、减少Update调用 |
| 内存泄漏 | 内存使用持续增长 | 正确管理事件订阅、资源释放、协程停止 |
| 空引用异常 | NullReferenceException | 始终检查空引用、使用安全访问方法 |
| UI性能 | UI响应慢、卡顿 | 批量更新、减少不必要的更新、使用UI管理器 |
| 物理问题 | 碰撞异常、物理行为不正确 | 正确配置刚体、使用FixedUpdate、优化碰撞检测 |
预防措施
- 代码审查: 定期进行代码审查,发现潜在问题
- 性能测试: 定期进行性能测试,监控关键指标
- 单元测试: 编写单元测试,确保代码正确性
- 静态分析: 使用静态分析工具检测代码问题
- 监控工具: 使用Unity Profiler等工具监控性能
- 编码规范: 遵循统一的编码规范
- 文档记录: 记录常见问题和解决方案
- 持续学习: 关注Unity更新和最佳实践
通过识别和避免这些常见问题,可以显著提高Unity项目的质量和稳定性。