3、NetworkBehaviour与NetworkIdentity:理解核心组件、注册网络对象、网络对象的生命周期管理

好,咱们今天来聊聊Unity联机开发里最基础、也最绕不开的两个组件:NetworkIdentityNetworkBehaviour

说实话,我刚接触UNET那会儿,以为这俩东西就是挂上去就完事了。结果呢?项目做到一半,对象同步不上、生命周期乱套、客户端凭空多出来一堆幽灵对象……嗯,都是血泪教训。今天我就把这块掰开揉碎了讲清楚。

3.1 NetworkIdentity:网络对象的“身份证”

每个要参与网络同步的游戏对象,都必须挂载一个 NetworkIdentity。它就像身份证,告诉系统:“我是谁?我在哪?谁管我?”

我个人习惯,在项目初期就把所有可能联网的预制体都加上这个组件。省得后面忘了加,排查起来特别痛苦。

核心属性

属性 说明 我的建议
Network ID 网络唯一ID,由系统自动分配 别手动改,会出大事
Scene ID 场景中静态对象的ID 场景预制体用这个做持久化
Server Only 仅在服务端存在 适合AI、触发器这类逻辑对象
Local Player Authority 本地玩家拥有控制权 玩家角色必须勾选

关键点:NetworkIdentity 的 isServerisClient 属性,决定了当前对象在哪个端生效。我见过有人直接在 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_PlayerNet_Prefab_Bullet。这样找起来方便,也不容易漏注册。

场景中的静态对象

如果场景里已经放好了对象(比如出生点、NPC),它们也需要 NetworkIdentity。系统会自动分配 Scene ID,并在客户端连接时同步。

警告:场景中的网络对象,如果修改了位置或状态,服务端重启后会自动恢复初始状态。别指望它能记住玩家离开时的位置。我踩过这个坑——玩家下线再上线,发现角色回到了出生点,装备全没了……

3.4 网络对象的生命周期管理

生命周期,说白了就是对象什么时候生、什么时候死、谁来管。

生成流程

  1. 服务端调用 NetworkServer.Spawn()
  2. 系统自动在所有客户端生成该对象
  3. 每个客户端执行 OnStartClient()
  4. 如果是本地玩家,再执行 OnStartLocalPlayer()

你想想看,这个过程是自动的。但有个坑:如果你在客户端直接 Instantiate 一个网络预制体,它不会自动同步到服务端。必须通过 NetworkServer.Spawn() 才行。

销毁流程

  1. 服务端调用 NetworkServer.Destroy()
  2. 所有客户端自动删除该对象
  3. 触发 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,到时候见。