Network Manager客户端制作小结

关联资料 

Unity 入门到精通(沈军) 

c语言中的局部变量和全局变量_c语言全局变量-CSDN博客

关键词

本地玩家信息: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 面板中。


Q2:public class LobbyCtrl : NetworkBehaviour基类与给物体添加Network Object的关系

A2:声明 public class LobbyCtrl : NetworkBehaviour 不等于 直接添加了 NetworkObject

  1. NetworkBehaviour 需要 NetworkObject 基础
    当您创建一个继承自 NetworkBehaviour 的脚本(如 LobbyCtrl)时:

    • 该脚本 必须 挂载在已添加 NetworkObject 组件的游戏对象上

    • 如果忘记添加 NetworkObject,Unity 编辑器会显示错误提示

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 事件系统,当用户结束编辑输入框时触发。

代码流程——所有客户端玩家都准备好后跳转游戏页面 

   void UpdateAllPlayerInfos(){//将能够开始游戏的条件设置为布尔值bool Cango=true;//检查所有玩家是否准备好,如果没有准备,则不能开始游戏foreach (var item in _allPlayerInfos){if (!item.Value.isready){Cango = false;}UpdatePlayerInfoClientRpc(item.Value);}//如果准备好了,则出现“start”按钮if (Cango){_startBtn.gameObject.SetActive(true);}     }//按键后切换场景private void OnStartClick(){GameManager.Instance.LoadScene("Game");}

数据更新方法详解

   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 的参数是值传递,接收的是客户端数据的副本,所以要在方法中重新赋值以更新玩家信息数据。其中的“重复赋值”行为与网络同步机制相关。

  • OnFamaleToggle 中的赋值:本地立即生效(快速反馈给当前玩家)。

  • UpdateAllPlayerServerRpc 中的赋值:服务器端同步(确保全局状态一致)。


Q2:此时OnFamaleToggle中赋值,离开该方法后是否会恢复成默认值?

A2:因为Dictionary类型属于全局变量(如果声明定义在方法内部属于局部),是全局存储,修改其内容直接在全局存储中修改其内容。

        但如果,要是变量类型为值类型playerinfo直接来赋值,此时它没有存储到任何全局容器中,离开OnFamaleToggle方法后该值会丢失。


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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/86076.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/86076.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 开发环境全栈隔离架构:从 Anaconda 到 PyCharm 的四级防护体系

【深度探索】Windows 下 Python 多版本虚拟环境管理与隔离实战&#xff1a;支持 Anaconda、Poetry、Pipenv、venv、uv、Hatch、PyCharm、VS Code 全工具链方案-CSDN博客 【零基础】Python 多版本虚拟环境管理与隔离实战——支持 Anaconda、Poetry、Pipenv、venv、uv、Hatch、Py…

Redis集群性能优化实战指南

Redis集群性能优化实战指南 一、技术背景与应用场景 随着互联网服务规模不断扩大&#xff0c;缓存层承载了海量的读写请求。Redis因其高性能和丰富的数据结构被广泛用于缓存、排行榜、会话存储等场景。单节点Redis在并发、内存和持久化方面会遇到瓶颈&#xff0c;Redis集群&a…

使用 Telegraf 向 TDengine 写入数据

Telegraf 是一款十分流行的指标采集开源软件。在数据采集和平台监控系统中&#xff0c;Telegraf 可以采集多种组件的运行信息&#xff0c;而不需要自己手写脚本定时采集&#xff0c;降低数据获取的难度。 只需要将 Telegraf 的输出配置增加指向 taosAdapter 对应的 url 并修改…

华为云 Flexus+DeepSeek 征文|华为云Dify 平台 CCE 高可用集群部署与大模型高质量知识库构建指南

华为云 FlexusDeepSeek 征文&#xff5c;华为云Dify 平台 CCE 高可用集群部署与大模型高质量知识库构建指南 文章目录 华为云 FlexusDeepSeek 征文&#xff5c;华为云Dify 平台 CCE 高可用集群部署与大模型高质量知识库构建指南前言1、ModelArts Studio大模型开通2、Dify平台CC…

测试校招/应届刷题知识路线

需要重点关注计算机基础知识和测试相关技能 对于即将参加校招或应届的同学,测试岗位需要重点关注计算机基础知识和测试相关技能。 掌握操作系统、计算机网络、数据库等计算机基础,同时熟悉测试基础理论、测试方法、自动化测试工具 (如 Selenium、JMeter)和编程语言(如 Py…

华为云Flexus+DeepSeek征文 | 基于华为云的 Dify-LLM 企业级 AI 开发平台部署指南

前言 在人工智能技术迅猛发展的今天&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为推动企业智能化转型的核心引擎。然而&#xff0c;如何高效、安全地将 LLM 能力整合到实际业务场景中&#xff0c;仍然是许多开发者面临的挑战。Dify 作为一款开源的 LLM 应用开发平…

Dify×奇墨科技:开源+本土化,破解企业AI落地难题

在人工智能技术迅猛发展的今天&#xff0c;企业亟需高效、安全的AI应用开发工具。开源LLMOps平台Dify.AI凭借其灵活性与开放性&#xff0c;已成为开发者构建AI应用的首选之一。而它在中国市场的规模化落地&#xff0c;离不开本土合作伙伴的强力支撑——奇墨科技作为Dify中国大陆…

Vue3+Spring boot 前后端防抖增强方案

我将基于您提供的防抖切面代码进行改进&#xff0c;主要优化点包括使用Redis实现分布式防抖、增强键生成策略、改进异常处理等。以下是完整的改进方案&#xff1a; 1. 改进的防抖注解 (Debounce.java) java 复制 下载 package com.weiyu.anno;import java.lang.annotation…

Cppcheck 使用教程:本地 + CMake + GitHub Actions 自动分析实战

Cppcheck 是一款强大的 C/C 静态分析工具&#xff0c;专注于发现内存泄漏、未初始化变量、死代码、未使用函数等问题。它不依赖编译器&#xff0c;适合在持续集成和本地开发中快速定位潜在 Bug。 本文将手把手教你如何&#xff1a; 安装 Cppcheck 在命令行中使用 集成到 CMak…

Mac Parallels Desktop Kali 2025 代理设置

Mac Parallels Desktop Kali 2025 代理设置 核心步骤&#xff1a; kali设置桥接wifi 查看kali和主机ip 运行命令ifconfig查看kali ip&#xff1a; mac主机ip&#xff1a; kali设置proxy ip填写主机ip&#xff0c;port为主机proxy端口 enjoy

通义灵码编程智能体深度评测(Qwen3模型+终端操作+MCP工具调用实战)

1 引言 随着AI编程助手进入工具链集成时代&#xff0c;通义灵码作为阿里云推出的智能编程解决方案&#xff0c;其Qwen3模型与MCP(Multi-tool Calling Platform)的协同能力引发开发者关注。本文将基于真实开发场景&#xff0c;从代码理解、终端操作和工具链调用三个维度展开深度…

SpringBoot电脑商城项目--商品详情+加入购物车

商品详情 1. 持久层 1.1. 规划sql语句 根据id查询商品详情 1.2 mapper层编写抽象方法 /*** 根据商品id查询商品详情* param id 商品id* return 匹配的id商品详情&#xff0c;如果没有匹配的数据&#xff0c;则返回null*/Product findById(Integer id); 1.3 xml文件中编写sq…

上交卡尔动力联合提出FastDrive!结构化标签实现自动驾驶端到端大模型更快更强

最近将类人的推理能力融入到端到端自动驾驶系统中已经成为了一个前沿的研究领域。其中&#xff0c;基于视觉语言模型的方法已经吸引了来自工业界和学术界的广泛关注。 现有的VLM训练范式严重依赖带有自由格式的文本标注数据集&#xff0c;如图1(a)所示。虽然这些描述能够捕捉丰…

C# 委托(什么是委托)

什么是委托 可以认为委托是持有一个或多个方法的对象。当然&#xff0c;一般情况下你不会想要“执行”一个对 象&#xff0c;但委托与典型的对象不同。可以执行委托&#xff0c;这时委托会执行它所“持有"的方法。 本章将揭示创建和使用委托的语法和语义。在本章后面&am…

iTwin briefcase, checkpoint ,standalone

在 iTwin.js 中&#xff0c;briefcase 和 checkpoint 都是 IModel 的不同连接类型&#xff0c;但它们的用途和特性不同&#xff1a; Briefcase 用途&#xff1a;用于本地编辑和同步。通常是用户从 iModelHub 检出&#xff08;Check-out&#xff09;后在本地生成的可写副本。特…

媒体AI关键技术研究

一、引言 随着人工智能技术的迅猛发展&#xff0c;媒体行业正经历前所未有的变革。AI技术不仅重塑了内容生产和传播模式&#xff0c;更为媒体创意发展提供了全新可能。在数字化、移动化和信息爆炸的大背景下&#xff0c;传统媒体面临巨大挑战&#xff0c;而AI技术为行业带来了…

Cargo 与 Rust 项目

一、Rust 项目&#xff1a;现代化的系统编程单元 Rust 项目 是用 Rust 语言编写的软件工程单元&#xff0c;具有以下核心特征&#xff1a; 核心组件&#xff1a; src/ 目录&#xff1a;存放 Rust 源代码&#xff08;.rs 文件&#xff09; Cargo.toml&#xff1a;项目清单文件…

uni-app总结6-配合iOS App项目开发apple watch app

假设你已经用uni-app开发好了一个iOS端的app,现在想要开发一个配套的apple watch app。改怎么去开发呢?是不是一头雾水,这篇文章就会介绍一些apple watch app开发的知识以及如何在uni-app开发的iOS app基础上去开发配套的watch app。 一、apple watch 开发知识 apple watc…

神经网络的本质 逻辑回归 python的动态展示

神经网络的本质 逻辑回归 python的动态展示 逻辑回归运行图相关代码什么是逻辑回归和ai的关系逻辑回归公式流程与实际案例解析**一、逻辑回归的数学公式流程**1. **线性组合阶段**2. **激活函数&#xff08;Sigmoid&#xff09;**3. **概率预测与决策**4. **交叉熵损失函数**5.…

sql server中的with 锁各种区别

&#x1f4d8; SQL Server 常用 WITH (Hint) 用法与组合场景对照表 Hint 组合作用说明常见用途是否阻塞他人是否读脏数据备注WITH (NOLOCK)不加共享锁&#xff0c;允许读取未提交数据报表导出、大数据分页❌✅等价于 READ UNCOMMITTED&#xff0c;脏读风险高WITH (HOLDLOCK)保持…