一、频繁生成销毁的同步痛点
做联网游戏的时候,Mirror作为Unity常用的网络同步库,每帧都要同步角色、技能这些对象,要是每次都新建再销毁,就像每次寄快递都买新盒子,用完直接扔——不仅要花时间造盒子,扔的盒子还会占空间,系统得花精力清理(就是GC),多了就会卡帧,玩家一卡就骂。尤其是技能弹道、临时同步对象这种高频创建销毁的东西,同步开销会翻倍。
二、对象池和Mirror的结合实现
技术栈:C#、Unity 2021、Mirror v100.1.0 对象池其实就是把不用的对象收起来,下次直接拿新的,不用再从头造,Mirror的网络对象只要做好同步的状态管理就行。
using UnityEngine;
using Mirror;
using System.Collections.Generic;
// 网络对象池,专门管理Mirror同步对象的复用
public class NetworkSyncObjectPool : MonoBehaviour
{
// 要复用的预制体,必须是Mirror的NetworkIdentity子类
public NetworkIdentity reusablePrefab;
// 初始池大小,提前备好的对象数量
public int initialPoolCount = 30;
// 存储空闲对象的队列,先进先出方便管理
private Queue<NetworkIdentity> idleObjectQueue = new Queue<NetworkIdentity>();
void Awake()
{
// 初始化池:提前生成对象,不激活,避免每帧动态创建的开销
for (int i = 0; i < initialPoolCount; i++)
{
NetworkIdentity newObj = Instantiate(reusablePrefab, transform);
newObj.gameObject.SetActive(false);
Mirror.NetworkServer.UnSpawn(newObj.gameObject); // 暂时从Mirror同步列表移除,避免自动同步
idleObjectQueue.Enqueue(newObj);
}
}
// 从池取对象,没空闲就新建
public NetworkIdentity GetObject(Vector3 spawnPos, Quaternion spawnRot)
{
NetworkIdentity targetObj;
// 有空闲对象直接取,跳过新建步骤
if (idleObjectQueue.Count > 0)
{
targetObj = idleObjectQueue.Dequeue();
}
// 池空了就临时新建,但要控制频率,避免频繁扩张
else
{
targetObj = Instantiate(reusablePrefab, transform);
Mirror.NetworkServer.UnSpawn(targetObj.gameObject);
}
// 重置对象状态!这步是核心,不然旧数据会残留
ResetObjectState(targetObj);
// 设置位置旋转,激活对象
targetObj.transform.SetPositionAndRotation(spawnPos, spawnRot);
targetObj.gameObject.SetActive(true);
// 让Mirror把这个对象同步给所有客户端
Mirror.NetworkServer.Spawn(targetObj.gameObject);
return targetObj;
}
// 把不用的对象归还到池,而不是直接销毁
public void ReturnObject(NetworkIdentity usedObj)
{
// 先从Mirror同步列表移除,停止同步
Mirror.NetworkServer.UnSpawn(usedObj.gameObject);
// 再次重置状态,确保归还后是干净的
ResetObjectState(usedObj);
// 隐藏对象,放回池
usedObj.gameObject.SetActive(false);
idleObjectQueue.Enqueue(usedObj);
}
// 重置对象的所有动态数据,避免复用残留
private void ResetObjectState(NetworkIdentity obj)
{
// 示例:弹道对象的状态重置
if (obj.TryGetComponent<SkillBullet>(out var bullet))
{
bullet.ResetBulletData();
}
// 可以加其他自定义组件的重置逻辑,比如移动组件、buff组件等
}
}
// 示例:技能弹道的自定义脚本,和对象池配合
public class SkillBullet : NetworkBehaviour
{
public float damage = 25f;
public float maxLifeTime = 3f;
private float currentLife;
// 重置方法,被对象池调用,必须写得彻底
public void ResetBulletData()
{
damage = 25f; // 重置伤害
currentLife = 0f; // 重置生命周期
// 重置移动速度、碰撞标记、特效状态等
GetComponent<Rigidbody>().velocity = Vector3.zero;
}
void Update()
{
// 只有服务器控制弹道生命周期,Mirror的同步逻辑在SyncVar里
if (isServer)
{
currentLife += Time.deltaTime;
// 生命周期到了,归还对象到池,不是销毁
if (currentLife >= maxLifeTime)
{
Object.FindObjectOfType<NetworkSyncObjectPool>().ReturnObject(GetComponent<NetworkIdentity>());
}
}
}
// 碰撞触发,命中后归还对象
void OnCollisionEnter(Collision collision)
{
if (isServer)
{
// 命中逻辑:给目标造成伤害(省略具体数值判断)
// 归还对象到池
Object.FindObjectOfType<NetworkSyncObjectPool>().ReturnObject(GetComponent<NetworkIdentity>());
}
}
}
三、核心应用场景
3.1 技能弹道同步
联网游戏里的技能子弹、激光这些,每放一次就新建一次,用完就销毁,换成对象池后,服务器和客户端直接复用,不用每次走Mirror的新建同步流程,减少延迟。
3.2 角色状态缓冲同步
有时候需要临时同步角色的临时状态(比如护盾、减速),这些临时对象用完就丢,用对象池回收后,下次再用不用新建,降低GC压力。
3.3 临时网络消息中转
Mirror里的临时消息对象,每帧处理完就销毁,换成对象池后,中转速度提升,避免消息排队延迟。
四、技术的优缺点
4.1 优点
首先是减少GC,对象复用避免了频繁的内存分配和回收,游戏帧率更稳定;其次是降低Mirror的同步开销,不用每次新建网络对象,同步消息的体积更小;还有就是扩容灵活,根据场景调整池大小,不会出现新建对象的卡顿。
4.2 缺点
实现稍微复杂,得写对象重置逻辑,漏写就会出现旧数据残留的bug;池大小不好控制,太小不够用,还是会新建,太大占内存,比如同时放几百个闲置对象,会占不必要的内存。
五、注意事项
5.1 必须彻底重置对象状态
这是最容易踩的坑,比如子弹的伤害是上次的50,这次变成新的,就会出现技能伤害异常,所以每个组件都要写重置逻辑,确保归还后是全新的。
5.2 同步权限控制
Mirror的网络对象有Authority,池里的对象被借走后,不能随便改权限,不然会出现同步错误,比如客户端控制了服务器的对象,导致状态混乱。
5.3 池大小动态调整
固定大小的池适合小型游戏,但大型游戏要动态调整,比如副本里弹道多,就临时扩容,空闲的时候缩容,避免内存浪费。
六、总结
对象池和Mirror结合,是解决联网游戏同步性能问题的实用方案,适合高频创建销毁同步对象的场景,只要注意状态重置、权限控制和池大小调整,就能有效减少同步开销,提升游戏的流畅度。
Comments