3、NetworkBehaviour与NetworkIdentity:理解核心组件、注册网络对象、网络对象的生命周期管理
好,咱们今天来聊聊Unity联机开发里最基础、也最绕不开的两个组件:NetworkIdentity 和 NetworkBehaviour。
说实话,我刚接触UNET那会儿,以为这俩东西就是挂上去就完事了。结果呢?项目做到一半,对象同步不上、生命周期乱套、客户端凭空多出来一堆幽灵对象……嗯,都是血泪教训。今天我就把这块掰开揉碎了讲清楚。
3.1 NetworkIdentity:网络对象的“身份证”
每个要参与网络同步的游戏对象,都必须挂载一个 NetworkIdentity。它就像身份证,告诉系统:“我是谁?我在哪?谁管我?”
我个人习惯,在项目初期就把所有可能联网的预制体都加上这个组件。省得后面忘了加,排查起来特别痛苦。
核心属性
| 属性 | 说明 | 我的建议 |
|---|---|---|
| Network ID | 网络唯一ID,由系统自动分配 | 别手动改,会出大事 |
| Scene ID | 场景中静态对象的ID | 场景预制体用这个做持久化 |
| Server Only | 仅在服务端存在 | 适合AI、触发器这类逻辑对象 |
| Local Player Authority | 本地玩家拥有控制权 | 玩家角色必须勾选 |
关键点:NetworkIdentity 的 isServer 和 isClient 属性,决定了当前对象在哪个端生效。我见过有人直接在 Update 里写 if(isServer) 做逻辑判断,结果客户端也执行了——因为没判断 isClient 为 false 的情况。
3.2 NetworkBehaviour:网络行为的“执行者”
有了身份证,还得有行为。NetworkBehaviour 就是干这个的。它继承自 MonoBehaviour,但多了网络相关的生命周期回调。
说白了,你平时写的 Start()、Update() 在联网环境下可能不靠谱。你得用 OnStartServer()、OnStartClient() 这些。
常用回调方法
public class PlayerController : NetworkBehaviour
{
void Start()
{
// 普通MonoBehaviour的Start
// 但注意:这里可能在客户端和服务端都执行
}
public override void OnStartServer()
{
// 仅在服务端初始化时调用
// 我习惯在这里初始化血量、状态等服务器权威数据
hp = 100;
}
public override void OnStartClient()
{
// 客户端上生成时调用
// 适合做UI绑定、特效初始化
if (!isLocalPlayer)
{
// 非本地玩家,禁用本地输入
GetComponent<PlayerInput>().enabled = false;
}
}
public override void OnStartLocalPlayer()
{
// 仅本地玩家调用
// 设置摄像机跟随、UI绑定等
Camera.main.GetComponent<FollowTarget>().target = transform;
}
}
我的小技巧:在 OnStartClient 里,用 isLocalPlayer 判断是不是自己控制的角色。这样能避免把别人的输入也处理了。我曾经犯过这个错,结果两个玩家同时控制一个角色……画面太美不敢看。
3.3 注册网络对象:预制体与场景对象
网络对象不是凭空出现的。你得告诉系统:“嘿,这个预制体我要用来生成网络对象!”
注册预制体
在 NetworkManager 的 Registered Spawnable Prefabs 列表里,把需要网络生成的预制体拖进去。注意:每个预制体都必须有 NetworkIdentity。
我建议把所有网络预制体放在一个专门的文件夹里,命名规范一点。比如 Net_Prefab_Player、Net_Prefab_Bullet。这样找起来方便,也不容易漏注册。
场景中的静态对象
如果场景里已经放好了对象(比如出生点、NPC),它们也需要 NetworkIdentity。系统会自动分配 Scene ID,并在客户端连接时同步。
警告:场景中的网络对象,如果修改了位置或状态,服务端重启后会自动恢复初始状态。别指望它能记住玩家离开时的位置。我踩过这个坑——玩家下线再上线,发现角色回到了出生点,装备全没了……
3.4 网络对象的生命周期管理
生命周期,说白了就是对象什么时候生、什么时候死、谁来管。
生成流程
- 服务端调用
NetworkServer.Spawn() - 系统自动在所有客户端生成该对象
- 每个客户端执行
OnStartClient() - 如果是本地玩家,再执行
OnStartLocalPlayer()
你想想看,这个过程是自动的。但有个坑:如果你在客户端直接 Instantiate 一个网络预制体,它不会自动同步到服务端。必须通过 NetworkServer.Spawn() 才行。
销毁流程
- 服务端调用
NetworkServer.Destroy() - 所有客户端自动删除该对象
- 触发
OnNetworkDestroy()回调
重点:千万不要在客户端直接 Destroy() 网络对象!那样只会删除本地副本,服务端和其他客户端还在。我曾经在测试时这么干过,结果服务端一直报空引用异常,查了半天才发现是客户端私自销毁了对象。
对象池与复用
频繁生成销毁网络对象,性能开销很大。我建议用对象池。但注意:对象池里的对象,在回收时要调用 NetworkServer.UnSpawn() 而不是 Destroy()。这样系统会把它标记为“未生成”,但对象本身还在内存里。
// 回收网络对象
void ReturnToPool(GameObject obj)
{
NetworkServer.UnSpawn(obj);
obj.SetActive(false);
// 放回对象池
pool.Enqueue(obj);
}
// 从对象池取出
GameObject GetFromPool(GameObject prefab)
{
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
NetworkServer.Spawn(obj);
return obj;
}
// 没有可用对象,新建
var newObj = Instantiate(prefab);
NetworkServer.Spawn(newObj);
return newObj;
}
避坑指南:我曾经在对象池里忘了调用 NetworkServer.UnSpawn(),结果对象虽然隐藏了,但网络ID还在占用。后来生成新对象时,ID冲突导致客户端同步错乱。嗯,从那以后我每次回收都先 UnSpawn。
3.5 实战建议总结
- 所有网络预制体必须挂 NetworkIdentity,这是铁律
- 用 NetworkBehaviour 替代 MonoBehaviour,除非你确定这个脚本不参与网络
- 服务端是权威,生成和销毁都走服务端
- 对象池是好东西,但别忘了 UnSpawn
- 生命周期回调的顺序:OnStartServer → OnStartClient → OnStartLocalPlayer
好了,这一章的内容就这些。NetworkIdentity 和 NetworkBehaviour 是联机开发的基石,理解透了,后面的同步、RPC、状态管理才能顺风顺水。下一章咱们聊聊数据同步的核心——SyncVar 和 Command,到时候见。