当前位置: 首页 > news >正文

Network Manager客户端制作小结

关联资料 

Unity 入门到精通(沈军) 

关键词

本地玩家信息:LocalClientId 

网络状态及网络序列化

网络游戏服务器和客户端
ClientRpc与ServerRpc

Network Manager ——预制体 

1、在Asset面板中Prefab创建一个Network Prefab List

          在Game Manager中添加Prefab

小结:

Player Prefab

  • 作用Player Prefab用于指定当有新玩家加入游戏时,应该实例化的玩家预制体(Prefab)。这个预制体通常包含玩家角色的所有必要组件,如角色控制器、网络身份组件(Network Identity)、动画系统、碰撞检测等。

  • 应用场景:在网络游戏中,当一个新玩家连接到服务器时,服务器会使用这个预制体来创建该玩家的游戏对象,并将其同步到所有客户端。

Network Prefabs Lists

  • 作用Network Prefabs Lists用于指定一组网络预制体(Network Prefabs)。这些预制体可以在游戏运行过程中被动态地实例化和销毁,通常用于游戏中的各种网络对象,如道具、武器、NPC等。

  • 应用场景:在网络游戏中,服务器或客户端可能需要在运行时生成某些对象,比如掉落的武器、生成的NPC等。这些对象需要被所有客户端同步,因此需要将它们的预制体添加到Network Prefabs Lists中。


2、将当前Scene面板上添加Game Manager物体

        select transport可以如下调制选择:


注意:挂载了Network Object的物体放置在哪个场景中,场景切换时这个物体就会出现哪个场景中。(也不严谨,如Player(clone)的情况出现)

游戏大厅制作 

声明变量

   public class LobbyCtrl : NetworkBehaviour[SerializeField]Transform _canvas;Transform _content;GameObject _oringinCell;Button _startBtn;Toggle _ready;Dictionary<ulong,PlayerListCell> _cellDictionary;Dictionary<ulong, PlayerInfo> _allPlayerInfos;

   LobbyCtrl 是一个类,继承自 NetworkBehaviourNetworkBehaviour 是 Unity 的一个基类,用于处理网络相关的功能(如多人游戏中的同步和通信)

   _content 是一个 Transform 类型的字段,可能用于引用 UI 列表的容器(例如一个 ScrollRect 的内容区域)

   _ready 是一个 Toggle 类型的字段,可能是一个“准备”开关,通常用于表示一种可以在两种状态之间切换的控件或方法

         _allPlayerInfos 是一个字典,键是 ulong 类型(通常是玩家的网络 ID),值是 PlayerInfo 类型(可能是玩家的信息数据结构)。


整体功能:

  1. 玩家列表管理:通过 _allPlayerInfos 存储玩家信息,并通过 _cellList 在 UI 中显示玩家列表。

  2. 准备状态管理:通过 _ready 开关,让玩家确认是否准备好开始游戏。

  3. 游戏开始逻辑:通过 _startBtn 按钮,触发游戏开始的逻辑(例如检查所有玩家是否准备好)。

  4. UI 更新:通过 _canvas_content,动态更新大厅的 UI。

Q1:为什么只暴露了 _canvas 而没有暴露 _content 的可能原因:

A1:

1. _canvas 是关键的 UI 根节点

_canvas 是一个 Transform,通常用于引用大厅的 UI 根节点(例如一个 Canvas)。这个字段可能需要开发者在 Unity 编辑器中手动设置,因为它是一个高层级的 UI 元素,直接决定了大厅界面的显示位置和结构。

2. _content 是动态生成的子节点

_content 可能是一个 Transform,用于引用一个 ScrollRect 的内容区域(例如玩家列表的容器)。这个字段通常可以通过代码动态获取,或者通过 _canvas 的子节点来找到,因此没有必要暴露到 Inspector 面板中。

Playerlnfo结构体及网络序列化
 

public struct PlayerInfo:INetworkSerializable{public ulong id;public bool isready;public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter{serializer.SerializeValue(ref id);serializer.SerializeValue(ref isready);}}
  • PlayerInfo 是一个 struct,表示玩家在客户端的信息。

    • 它实现了 INetworkSerializable 接口,这意味着它需要定义一个 NetworkSerialize 方法,用于在网络通信中序列化和反序列化数据

    • INetworkSerializable 接口才是可以该结构体可以序列化的关键,用于控制自定义数据类型的网络传输。

    • 注意此处序列化也作为结构体的一部分。

 网络序列化方法定义
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T 
: IReaderWriter
{serializer.SerializeValue(ref id);serializer.SerializeValue(ref isready);
}
  • 核心作用——网络数据同步:在客户端和服务器之间同步对象的 id 和 isready 字段

  • public void NetworkSerialize<T>(BufferSerializer<T> serializer):这是一个泛型方法,用于实现双向序列化(即序列化和反序列化)。

    • T:泛型类型参数,表示序列化器的具体实现类型,必须实现 IReaderWriter 接口。这通常是为了在序列化和反序列化时使用不同的实现(如 BufferSerializerReaderBufferSerializerWriter)。

    • BufferSerializer<T>:序列化器类,封装了序列化和反序列化的操作。它内部使用 FastBufferWriterFastBufferReader,提供了高性能的序列化支持,并且简化了操作,避免了手动边界检查。

    • serializer:方法的参数,表示具体的序列化器实例,用于读取或写入数据。

 
序列化逻辑:

  • serializer.SerializeValue(ref id):将 id 字段序列化到字节流中,或者从字节流中反序列化到 id 字段。

  • serializer.SerializeValue(ref isready):将 isready 字段序列化到字节流中,或者从字节流中反序列化到 isready 字段。

添加玩家的方法

    public void AddPlayer(PlayerInfo playerInfo){//两个冲突的方法二选一,都是给玩家信息词典添加当前玩家信息_allPlayerInfos[playerInfo.id] = playerInfo;_allPlayerInfos.Add(playerInfo.id, playerInfo);GameObject clone = Instantiate(_oringinCell);clone.transform.SetParent(_content, false);PlayerListCell cell = clone.GetComponent<PlayerListCell>();_cellList.Add(cell);cell.Initial(playerInfo);clone.SetActive(true);}
  • 这是一个名为 AddPlayer 的方法,接收一个 PlayerInfo 类型的参数 playerInfo,PlayerInfo 是一个自定义的类或结构体,包含玩家的 ID、状态(如 isready)等信息。

    • _allPlayerInfos 是一个字典(Dictionary),用于存储玩家信息,这里将 playerInfo 对象存储到字典中,键是 playerInfo.id(玩家的唯一标识符)
       

  • _allPlayerInfos.Add(playerInfo.id, playerInfo);

        将 playerInfo 对象添加到 _allPlayerInfos 字典中,以 playerInfo.id 作为键。 _allPlayerInfos 通常用于存储所有玩家的信息,方便后续通过玩家 ID 快速查找对应的玩家信息

  • GameObject clone = Instantiate(_oringinCell);

        通过 Instantiate 方法实例化 _oringinCell 对象,创建一个与 _oringinCell 相同的新的游戏对象,并将其存储在 clone 变量中。


 

  • clone.transform.SetParent(_content, false);

        将 clone 对象的变换(Transform)设置为 _content 的子对象,并且不重置 clone 对象的局部变换(false 参数表示不重置局部位置、旋转和缩放)。


 

  • PlayerListCell cell = clone.GetComponent<PlayerListCell>();

        从 clone 对象中获取 PlayerListCell 组件,并将其存储在 cell 变量中。(这里PlayerListCell 组件自然也是PlayerListCell类,赋值只是后续命名比较方便


 

  • _cellList.Add(cell);

        将获取到的 cell 对象添加到 _cellList 列表中。_cellList 通常用于存储所有已创建的玩家列表单元格对象,方便后续对这些单元格进行统一的操作,比如更新显示内容、销毁单元格等。


 

  • cell.Initial(playerInfo);

        调用当前cell对象中的 “单元栏编辑”方法,

PlayerListCell类 (单元栏修改)

public void Initial(LobbyCtrl.PlayerInfo playerInfo){PlayerInfo = playerInfo;_name = transform.Find("Name").GetComponent<TMP_Text>();_ready = transform.Find("Ready").GetComponent<TMP_Text>();_name.text = playerInfo.name;_ready.text = playerInfo.isready ? "ready" : "no ready";_gender = transform.Find("Gender").GetComponent<TMP_Text>();_gender.text = playerInfo.gender ==0? "male":"female";}
  • public LobbyCtrl.PlayerInfo PlayerInfo { get; private set; }

    • 声明了一个公共属性 PlayerInfo,用于存储传入的玩家信息对象,其类型是 LobbyCtrl.PlayerInfo,这是一个玩家信息结构体或类。

    • { get; private set; } 表示外部可以读取但不能修改这个值

  • Initial方法

    • Initial 方法接受一个PlayerInfo对象作为参数,将传入的玩家信息保存到类的PlayerInfo属性中

    • 即是将当前玩家的准备信息显示在单元栏上

OnNetworkSpawn方法

        这是一个Unity网络编程中的OnNetworkSpawn方法的实现,用于在网络对象被生成时初始化一些网络相关的逻辑 

public override void OnNetworkSpawn(){NetworkManager.OnClientConnectedCallback += OnClientConn;_cellList = new List<PlayerListCell>();_allPlayerInfos = new Dictionary<ulong, PlayerInfo>();_content = _canvas.Find("List/Viewport/Content");_oringinCell = _content.Find("Cell").gameObject;_startBtn = _canvas.Find("StartBtn").GetComponent<Button>();_ready = _canvas.Find("Ready").GetComponent<Toggle>();_startBtn.onClick.AddListener(OnStartClick);_ready.onValueChanged.AddListener(OnReadyToggle); PlayerInfo playerInfo= new PlayerInfo();playerInfo.id = NetworkManager.LocalClientId;playerInfo.isready = false;AddPlayer(playerInfo);base.OnNetworkSpawn();}

  • NetworkManager.OnClientConnectedCallback += OnClientConn;

        这行代码的作用是将一个名为 OnClientConn 的方法注册为 NetworkManagerOnClientConnectedCallback 事件的回调函数

  • NetworkManager.OnClientConnectedCallback:这是一个事件,当有客户端成功连接到服务器时会被触发(即有玩家加入房间),这个事件只在服务器端和连接的本地客户端上运行。

  • += OnClientConn;:这是将 OnClientConn 方法添加到 OnClientConnectedCallback 事件的监听列表中。当有客户端连接时,OnClientConn 方法会被自动调用。

        

  • 为什么将赋值操作放入 OnNetworkSpawn 方法中
    • 确保网络状态正确:在 OnNetworkSpawn 方法中进行赋值操作,可以确保此时网络状态已经正确设置,例如 IsClientIsServer 等属性已经可用。在 AwakeStart 方法中,这些网络状态可能还没有准备好,因此不适合进行网络相关的初始化。
    • 保证初始化顺序:对于动态生成的 NetworkObjectOnNetworkSpawn 方法会在 Start 方法之前被调用。这意味着如果在 Start 方法中进行初始化,可能会导致在 OnNetworkSpawn 中需要使用这些初始化的值时,这些值尚未被设置。

    • 统一初始化逻辑:无论是动态生成的 NetworkObject 还是场景中放置的 NetworkObjectOnNetworkSpawn 方法都能保证在网络对象生成后进行初始化。这使得初始化逻辑更加统一,避免了因生成方式不同而导致的初始化顺序问题。
       

  • _startBtn.onClick.AddListener(OnStartClick);
    _ready.onValueChanged.AddListener(OnReadyToggle); 

    AddListener:这是 UnityEvent 的一个方法,用于添加一个非持久性监听器(即运行时回调)。当事件被触发时,会调用指定的方法

    • _startBtn.onClick:这是 Unity UI 按钮组件(Button)的一个 UnityEvent 属性,表示当按钮被点击时触发后,会调用OnStartClick方法

    • _ready.onValueChanged:这是 Unity UI 切换按钮组件(Toggle)的一个 UnityEvent 属性,表示当切换按钮的值发生变化时(即从选中变为未选中,或从未选中变为选中),会调用OnReadyToggle方法。

       

  • 创建并添加本地玩家信息

    • PlayerInfo playerInfo = new PlayerInfo(); 创建一个新的玩家信息对象。

    • playerInfo.id = NetworkManager.LocalClientId; 设置玩家信息的ID为本地客户端的ID。

    • playerInfo.isready = false; 设置玩家的准备状态为未准备。

    • AddPlayer(playerInfo); 将本地玩家信息添加到玩家列表中

  • 设置玩家信息的ID为本地客户端的ID的作用

    1. 唯一标识本地客户端

    NetworkManager.LocalClientId 是一个唯一的标识符,用于区分不同的客户端。在多人游戏中,每个客户端都有一个唯一的 LocalClientId,这样可以确保每个玩家在网络通信中被正确识别。

    2. 便于玩家信息管理

    通过将 LocalClientId 赋值给玩家信息的 id,可以方便地在游戏逻辑中引用和管理本地玩家的信息。例如,当需要更新玩家的状态、发送消息或处理玩家行为时,可以通过这个 id 快速找到对应的玩家信息。

    3. 支持网络同步

    在网络游戏中,服务器和客户端之间需要同步玩家的状态和行为。使用 LocalClientId 作为玩家的标识符,可以确保在不同客户端之间同步数据时,能够准确地识别和更新每个玩家的状态
     

  • AddPlayer(playerInfo);
    base.OnNetworkSpawn();
    • 关于 AddPlayer(playerInfo) 放在 OnNetworkSpawn 方法中的原因是正确初始化

      • 确保网络对象已生成

        • OnNetworkSpawn 方法是在 NetworkObject 被生成时调用的。这意味着此时网络对象已经准备好,可以进行与网络相关的操作。

        • 在这个方法中添加玩家信息,可以确保玩家信息的初始化与网络对象的生成同步进行。

      • 初始化本地玩家信息

        • 当本地客户端连接到服务器时,需要为本地玩家创建一个 PlayerInfo 对象,并将其添加到玩家列表中。

        • OnNetworkSpawn 中创建并添加本地玩家的信息,可以确保本地玩家的信息在本地客户端上正确初始化。

      • 避免重复初始化

        • 如果将 AddPlayer(playerInfo) 放在其他方法(如 StartAwake)中,可能会导致在某些情况下玩家信息被重复初始化。

        • OnNetworkSpawn 确保了玩家信息只在 NetworkObject 被生成时初始化一次。
           

    • 为什么是base.OnNetworkSpawn();

      • base.OnNetworkSpawn(); 确保了父类 NetworkBehaviour 中的 OnNetworkSpawn 方法逻辑被执行

        • OnNetworkSpawnNetworkBehaviour 类中的一个虚方法,用于在网络对象被生成时执行初始化逻辑(这也是为什么override 方法)

        • 如果父类的 OnNetworkSpawn 方法中包含了一些重要的初始化逻辑,这些逻辑对于子类的正常运行是必要的,那么你应该调用base.OnNetworkSpawn()

      • OnNetworkSpawn 的执行时机在它在网络对象(NetworkObject)被生成时自动调用,生成网络对象通常有以下几种方式:

        • 服务器端调用 Spawn 方法

          • 服务器可以调用 NetworkObject.Spawn()NetworkObject.SpawnWithOwnership(clientId) 方法来生成网络对象

更新新玩家数据 (服务器和客户端的数据同步)

private void OnClientConn(ulong obj){PlayerInfo playerInfo = new PlayerInfo();playerInfo.id = obj;playerInfo.isready = false;AddPlayer(playerInfo);UpdateAllPlayerInfos();}void UpdateAllPlayerInfos(){foreach (var item in _allPlayerInfos){UpdatePlayerInfoClientRpc(item.Value);}}[ClientRpc]void UpdatePlayerInfoClientRpc(PlayerInfo playerInfo){if (!IsServer){if (_allPlayerInfos.ContainsKey(playerInfo.id)){_allPlayerInfos[playerInfo.id] = playerInfo;}else{AddPlayer(playerInfo);}}}private void UpdatePlayerCells(){foreach (var item in _allPlayerInfos){_cellDictionary[item.Key].Setready(item.Value.isready);}}private void OnReadyToggle(bool arg0){_cellDictionary[NetworkManager.LocalClientId].Setready(arg0);UpdatePlayerInfo(NetworkManager.LocalClientId, arg0);if(IsServer){UpdateAllPlayerInfos();}else{UpdateAllPlayerServerRpc(_allPlayerInfos[NetworkManager.LocalClientId]);}}[ServerRpc(RequireOwnership =false)]private void UpdateAllPlayerServerRpc(PlayerInfo player){_allPlayerInfos[player.id] = player;UpdateAllPlayerInfos();}private void UpdatePlayerInfo(ulong id,bool isready){PlayerInfo info = _allPlayerInfos[id];info.isready = isready;_allPlayerInfos[id] = info;}
  1. OnClientConn 是一个方法,可能是被调用来处理新玩家连接的逻辑。

  2. UpdateAllPlayerInfos 是一个遍历所有玩家信息并发送给客户端的函数。

  3. UpdatePlayerInfoClientRpc 是一个 [ClientRpc] 标注的方法,说明它是专门为客户端调用设计的,可能是用来同步玩家信息的。

ClientRpc与ServerRpc 

1、ClientRpc与ServerRpc 的区别

2、用法特征

  • 关键特性

    • 标记影响整个方法体的执行位置

    • 被标记的方法必须遵守命名规范(ServerRpc/ClientRpc后缀)

      • // 合法
        [ServerRpc]
        void UpdatePlayerStateServerRpc() {...}// 非法(编译错误)
        [ServerRpc]
        void UpdatePlayer() {...} // 缺少ServerRpc后缀

    • 只能存在于继承自NetworkBehaviour的类中

  • 多重RPC方法使用特性

    • public class PlayerManager : NetworkBehaviour
      {// 多个ServerRpc共存[ServerRpc]void ReadyStateServerRpc(bool isReady) {...}[ServerRpc(RequireOwnership = false)]void VoteStartServerRpc(int mapId) {...}// 多个ClientRpc共存[ClientRpc]void UpdatePlayersClientRpc() {...}[ClientRpc]void ShowCountdownClientRpc(int seconds) {...}
      }

玩家即时连接客户端响应方法 OnClientConn 

private void OnClientConn(ulong obj){PlayerInfo playerInfo = new PlayerInfo();playerInfo.id = obj;playerInfo.isready = false;AddPlayer(playerInfo);UpdateAllPlayerInfos();}
  1. 功能 :当一个客户端(玩家)连接到服务端时,会被触发执行。参数 obj 应该是一个用于标识玩家的唯一 ID。即此方法是对新加入的玩家进行初始化(通过后续方法进行修改某些属性后再更新)

  2. 代码逻辑

    • 创建一个 PlayerInfo 对象 playerInfo,用于存储该玩家的相关信息。

    • 将传入的玩家 ID 赋值给 playerInfo.id

    • playerInfo.isready 设置为 false,表示该玩家目前尚未准备好(可能是在游戏开始前的准备阶段等)。

    • 调用 AddPlayer(playerInfo) 方法,将这个新玩家的信息添加到一个存储所有玩家信息的集合中(假设 _allPlayerInfos 是一个存储玩家信息的字典或类似的集合)。

    • 调用 UpdateAllPlayerInfos() 方法,用于更新所有玩家的信息。

遍历玩家信息方法 UpdateAllPlayerInfos()

  1. 功能 :遍历所有玩家的信息,并通过网络调用将最新的玩家信息发送给客户端。

  2. 代码逻辑

    • 使用 foreach 循环遍历 _allPlayerInfos 中的每一项玩家信息。

    • 对于每一项,调用 UpdatePlayerInfoClientRpc(item.Value) 方法,这里的 item.Value 应该是玩家信息对象,通过这个方法将玩家信息发送给客户端。

    • 遍历_allPlayerInfos字典中的所有玩家信息,然后对每个玩家信息调用UpdatePlayerInfoClientRpc方法,通过ClientRpc特性将更新操作同步到所有客户端上

客户端远程调用方法 UpdatePlayerInfoClientRpc

    [ClientRpc]void UpdatePlayerInfoClientRpc(PlayerInfo playerInfo){if (!IsServer){if (_allPlayerInfos.ContainsKey(playerInfo.id)){_allPlayerInfos[playerInfo.id] = playerInfo;}else{AddPlayer(playerInfo);}}}
  1. 功能 :这是一个客户端远程调用(ClientRpc)方法,(服务器调用,客户端执行),用于在客户端接收并更新玩家信息。

  2. 代码逻辑

    • 通过 !IsServer 判断当前是否不是在服务端执行(因为 ClientRpc 是在服务端调用,但实际执行是在客户端)。

    • 检查 _allPlayerInfos 是否已经包含该玩家的 ID(playerInfo.id)。

      • 如果已经包含,则将 _allPlayerInfos 中对应 ID 的玩家信息更新为新的 playerInfo

      • 如果不包含,则通过 AddPlayer(playerInfo) 方法将该玩家信息添加到 _allPlayerInfos 中。

准备状态的更新方法OnReadyToggle

方法功能

  • 这是一个名为 OnReadyToggle 的方法,当玩家的准备状态发生改变时被调用,用于处理玩家准备状态的更新和同步。
     

代码逻辑

  1. 更新本地玩家状态_cellDictionary[NetworkManager.LocalClientId].Setready(arg0):根据传入的布尔值 arg0,更新本地客户端对应玩家的准备状态。 

  2. 更新本地玩家信息显示

    调用 UpdatePlayerInfo 方法,更新本地界面上显示的该玩家的准备状态信息,使玩家能够看到自己的准备状态已改变。
  3. 根据是否为服务器执行不同操作:
  • 服务器端操作:如果是服务器,直接调用 UpdateAllPlayerInfos() 方法来更新所有玩家的信息。
  • 客户端操作:如果不是服务器(即客户端),则调用 UpdateAllPlayerServerRpc方法,通过 Server RPC(远程过程调用)将本地玩家的准备状态信息发送给服务器。

服务器更新远程调用方法 

        用于更新服务器上的所有玩家信息。 

  1. [ServerRpc(RequireOwnership = false)]

    • 这是一个属性标记,表示该方法是一个 Server RPC 方法。这意味着该方法只能在服务器端执行,但可以被客户端调用。

    • RequireOwnership = false 表示调用此方法时,不需要拥有该对象的所有权。

      • 通常,只有拥有对象的客户端才能调用其 Server RPC 方法,但这里设置为 false,允许任何客户端调用此方法:举个实例,游戏聊天室的世界聊天

  2. private void UpdateAllPlayerServerRpc(PlayerInfo player)

    • 这是一个私有方法,名为 UpdateAllPlayerServerRpc,接收一个 PlayerInfo 类型的参数 player。该方法的目的是将客户端发送的玩家信息更新到服务器上。

    • _allPlayerInfos[player.id] = player;

      _allPlayerInfos 是一个字典(或类似结构),用于存储所有玩家的信息,其键为玩家的 ID,值为对应的 PlayerInfo 对象。
    • UpdateAllPlayerInfos();

      调用 UpdateAllPlayerInfos 方法,这个方法会负责将更新后的玩家信息广播给所有客户端

角色创建的流程 

模型预制体的处理

1、如果导入模型没有材质,可以根据如下方法找回材质。 

1.先找到该模型的材料


2.编辑—渲染—转化

   

2、为“父物体”添加“network objective”组件

1.对两个角色创建父级空物体后,为该父级空物体添加“网络对象组件”


2、再找到NetworkManger,将该父级空物体添加至Player Prefab中后在该场景中删除


3、运行该项目时,你会发现Lobby中有预制体克隆体出现

  • Player Prefab的作用
    你为NetworkManager配置的Player Prefab,实际上是一个预制体模板。当客户端连接服务器时,服务器会自动实例化该预制体,并在所有客户端同步生成对应的玩家对象56。

  • 克隆命名的原因
    Unity在实例化预制体时,默认会在原始名称后添加(Clone)后缀(如Player (Clone)),这是Instantiate()函数的原生行为,用于区分原始预制体和场景中的实例

    • 例如:通过脚本实例化代码 Instantiate(playerPrefab) 也会生成 Player(Clone)

游戏大厅页面栏的制作

1、创建两个Toggle UI类型,将二者放置父级空物体Gender中,并挂载组件Toggle Group

 

2、将二者Toggle组件的 Group挂载为Gender,这样就可以启动单选模式


3、在资产页面新建Render文件夹,调整其大小


4、新建camera以及image,且都将Texture属性挂载为Render文件夹中的Body,这样image就是照相机的视野了。


5、名字栏的制作

代码流程——初始化

  public struct PlayerInfo:INetworkSerializable{public ulong id;public bool isready;public int gender;public string name;public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter{serializer.SerializeValue(ref id);serializer.SerializeValue(ref isready);serializer.SerializeValue(ref gender);serializer.SerializeValue(ref name);}

        要在玩家信息结构体和序列化字段中加入该元素 

public override void OnNetworkSpawn(){Toggle male = _canvas.Find("Gender/Male").GetComponent<Toggle>();Toggle famale = _canvas.Find("Gender/Famale").GetComponent<Toggle>();male.onValueChanged.AddListener(OnMaleToggle);famale.onValueChanged.AddListener(OnFamaleToggle);//这里就是常规初始化操作了,声明一个玩家信息结构体并且初始化。使其玩家信息性别默认为0(在Unity编辑器中,将男性Toggle的“Is On”属性勾选(设置为true),女性Toggle不勾选(false)。这样在启动时就会默认选中男性。)PlayerInfo playerInfo= new PlayerInfo();playerInfo.gender = 0;
}

        声明一个Toggle类型的参数,将“资产”面板中创建的面板赋值给它,方便后续编写代码控制。

        与此同时在勾选male和female设置监听器,以此勾选选框时会调用相应方法

 代码流程——勾选性别(初始化及切换为男性)

private void OnMaleToggle(bool arg0)
{if (arg0) // 当男性选项被选中时(arg0为true){// 1. 获取本地玩家信息PlayerInfo playerInfo = _allPlayerInfos[NetworkManager.LocalClientId];// 2. 更新性别为男性(这里赋值是因为如果后面切换为女性,想要换回男性需要赋值为0)playerInfo.gender = 0;// 3. 更新玩家信息字典_allPlayerInfos[NetworkManager.LocalClientId] = playerInfo;// 4. 更新本地UI显示_cellDictionary[NetworkManager.LocalClientId].UPdateInfo(playerInfo);// 5. 网络同步逻辑if (IsServer) // 如果是服务器{UpdateAllPlayerInfos(); // 直接更新所有玩家信息}else // 如果是客户端{UpdateAllPlayerServerRpc(playerInfo); // 通过RPC通知服务器更新}// 6. 切换角色模型BodyCtrl.Instance.SwitchGender(0); // 将角色模型切换为男性}
}

初始化与OnMaleToggle方法的联动 

这在初始化时的方法相照应:

                male.onValueChanged.AddListener(OnMaleToggle); 
        因为Unity编辑器初始化时默认勾选为男性,且勾选性别时设置了监听器即触发OnMaleToggle,所以OnMaleToggle方法在此会调动一次。

 ClientId可以作为完美键值

        本地玩家ID完全可以作为字典的索引键,这是Unity Netcode游戏开发中的标准做法。不过需要提前进行正确的设置和初始化。(注意:这里的本地玩家指的是当前运行此代码的游戏实例所控制的玩家

 为什么服务器和客户端更新方法不同

1. 权威服务器架构(Authoritative Server)

  • 服务器是唯一真相源:游戏的关键数据(如玩家状态、位置等)必须由服务器统一管理,确保所有客户端看到一致的世界状态。

  • 客户端不可信:客户端可能被篡改(作弊),因此不能允许客户端直接修改全局数据。

 2. 代码逻辑差异

        若是服务器,则直接通知客户端更新;若是客户端,则需要通知服务器让客户端更新

if (IsServer) 
{UpdateAllPlayerInfos(); // 服务器直接更新
}
else 
{UpdateAllPlayerServerRpc(playerInfo); // 客户端通过RPC请求
}

       

代码流程——切换性别后更新玩家信息 

        对应上方是服务器和非服务器

void UpdateAllPlayerInfos(){//将能够开始游戏的条件设置为布尔值bool Cango=true;//检查所有玩家是否准备好,如果没有准备,则不能开始游戏foreach (var item in _allPlayerInfos){if (!item.Value.isready){Cango = false;}UpdatePlayerInfoClientRpc(item.Value);}if (Cango){_startBtn.gameObject.SetActive(true);}}[ServerRpc(RequireOwnership =false)]private void UpdateAllPlayerServerRpc(PlayerInfo player){_allPlayerInfos[player.id] = player;_cellDictionary[player.id].Setready(player.isready);UpdateAllPlayerInfos();} 

代码流程——更新状态栏

 private void UpdatePlayerCells(){foreach (var item in _allPlayerInfos){_cellDictionary[item.Key].UPdateInfo(item.Value);}}

代码流程——名字栏的完整流程制作 

using TMPro;//对结构体进行序列化,以便传输,加入新的玩家信息单元
//需要注意的是序列化也要作为结构体的一部分
public struct PlayerInfo:INetworkSerializable{public string name;public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter{serializer.SerializeValue(ref name);}
}//声明TMP_InputField元素
[SerializeField]
TMP_InputField _name;//获取并初始化  public override void OnNetworkSpawn()
{_name = _canvas.Find("Name").GetComponent<TMP_InputField>();//这里是将文本栏默认状态设置为了player0playerInfo.name ="player"+playerInfo.id;_name.text = playerInfo.name;
}//编辑名字结束的方法
private void OnEndEdit(string arg0){if (string.IsNullOrEmpty(arg0)){return;}PlayerInfo playerInfo = _allPlayerInfos[NetworkManager.LocalClientId];playerInfo.name=arg0;_allPlayerInfos[NetworkManager.LocalClientId] = playerInfo;       _cellDictionary[NetworkManager.LocalClientId].UPdateInfo(playerInfo);if (IsServer){UpdateAllPlayerInfos();//将单元栏的数据更新到单元栏词典中}else{UpdateAllPlayerServerRpc(playerInfo);}}//对新加入玩家初始化名字
private void OnClientConn(ulong obj){PlayerInfo playerInfo = new PlayerInfo();playerInfo.name = "player" + obj;AddPlayer(playerInfo);UpdateAllPlayerInfos();}

1、
       这就是将文本栏默认状态设置为了player0

                


2、_name.onEndEdit.AddListener(OnEndEdit);

  • 第一个 onEndEdit(小写开头):
    • 这是 TMP_InputField 的内置事件
      属于 Unity 事件系统,当用户结束编辑输入框时触发。

数据更新方法详解

   private void OnFamaleToggle(bool arg0){if (arg0){PlayerInfo playerInfo = _allPlayerInfos[NetworkManager.LocalClientId];playerInfo.gender= 1;_allPlayerInfos[NetworkManager.LocalClientId] = playerInfo;_cellDictionary[NetworkManager.LocalClientId].UPdateInfo(playerInfo);if (IsServer){UpdateAllPlayerInfos();}else{UpdateAllPlayerServerRpc(playerInfo);}BodyCtrl.Instance.SwitchGender(1);}}

 //状态栏的遍历更新private void UpdatePlayerCells(){foreach (var item in _allPlayerInfos){_cellDictionary[item.Key].UPdateInfo(item.Value);}}//单个状态栏赋值更新,在PlayerListCell类中public void UPdateInfo(PlayerInfo playerInfo){PlayerInfo = playerInfo;_name.text = playerInfo.name;_ready.text = PlayerInfo.isready ? "ready" : "no ready";_gender.text = PlayerInfo.gender == 0 ? "male" : "famale";}//客户端通知服务器更新
private void UpdateAllPlayerServerRpc(PlayerInfo player){_allPlayerInfos[player.id] = player;_cellDictionary[player.id].UPdateInfo(player);UpdateAllPlayerInfos();}void UpdateAllPlayerInfos(){//将能够开始游戏的条件设置为布尔值bool Cango=true;//检查所有玩家是否准备好,如果没有准备,则不能开始游戏foreach (var item in _allPlayerInfos){if (!item.Value.isready){Cango = false;}UpdatePlayerInfoClientRpc(item.Value);}if (Cango){_startBtn.gameObject.SetActive(true);}}//服务器通知客户端更新void UpdatePlayerInfoClientRpc(PlayerInfo playerInfo){if (!IsServer){if (_allPlayerInfos.ContainsKey(playerInfo.id)){_allPlayerInfos[playerInfo.id] = playerInfo;}else{AddPlayer(playerInfo);}UpdatePlayerCells();}}public void AddPlayer(PlayerInfo playerInfo){       _allPlayerInfos.Add(playerInfo.id, playerInfo);GameObject clone = Instantiate(_oringinCell);clone.transform.SetParent(_content, false);PlayerListCell cell = clone.GetComponent<PlayerListCell>();_cellDictionary.Add(playerInfo.id,cell);       cell.Initial(playerInfo);clone.SetActive(true);}

Q1:为什么OnFamaleToggle中赋值了,还要在UpdateAllPlayerServerRpc中重新赋值呢?

A1:因为ServerRpc 的参数是值传递,接收的是客户端数据的副本,所以要在方法中重新赋值以更新玩家信息数据。


Q2:UpdatePlayerInfoClientRpc中为何要allPlayerInfos.ContainsKey(playerInfo.id)这个判断条件?
A1:当新玩家加入时,客户端会错误地创建两个条目:

  • 一个来自 OnClientConnectedCallback
  • 一个来自 UpdatePlayerInfoClientRpc
    • 服务器需要让所有客户端(包括新客户端自己)知道新玩家的存在,因此服务器调用`UpdatePlayerInfoClientRpc`,将新玩家的`PlayerInfo`作为参数发送给所有客户端。(也可以说是UpdatePlayerInfoClientRpc方法的特性)

     所以该方法流程为:用判断条件检查是否存在该键,如果存在该键则对玩家信息进行赋值;如果不存在该键,说明是新加入了玩家,则添加新的玩家信息。

相关文章:

  • http2与websocket关系
  • NY339NY341美光固态闪存NW841NW843
  • RAG 升级之路:如何让问答机器人真正“智能”起来
  • 【网工】华为配置专题进阶篇④
  • 合并两个有序链表C++
  • Unity3D仿星露谷物语开发67之创建新的NPC
  • 变幻莫测:CoreData 中 Transformable 类型面面俱到(五)
  • 学习笔记丨AR≠VR:透视沉浸式技术的“虚实象限”法则
  • 【Golang面试题】Go语言实现请求频率限制
  • 记录:注册k8s cluster账号
  • NumPy玩转数据科学
  • Apollo:配置中心使用与介绍
  • C++11 Thread-Local Storage:从入门到精通
  • dify本地部署及添加ollama模型(ubuntu24.04)
  • Docker环境部署
  • Javaweb - 2 HTML
  • Windows 10 防火墙 0x8007045b 打不开
  • 数据库期末复习
  • AI是如何思考的,它的过程又是怎样
  • Python中布尔值在函数中的巧妙运用
  • 用阿里云做网站/运营推广的方式和渠道有哪些
  • 外贸网站 seo/海外营销推广服务
  • 贵阳网站建设外包/软文小故事200字
  • wordpress subway/深圳seo优化排名推广
  • 沈阳公司网站设计制作/软文推广渠道
  • 网站多级导航效果/网站页面设计