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
是一个类,继承自NetworkBehaviour
。NetworkBehaviour
是 Unity 的一个基类,用于处理网络相关的功能(如多人游戏中的同步和通信)
_content
是一个Transform
类型的字段,可能用于引用 UI 列表的容器(例如一个 ScrollRect 的内容区域)
_ready
是一个Toggle
类型的字段,可能是一个“准备”开关,通常用于表示一种可以在两种状态之间切换的控件或方法
_allPlayerInfos
是一个字典,键是ulong
类型(通常是玩家的网络 ID),值是PlayerInfo
类型(可能是玩家的信息数据结构)。
整体功能:
玩家列表管理:通过
_allPlayerInfos
存储玩家信息,并通过_cellList
在 UI 中显示玩家列表。准备状态管理:通过
_ready
开关,让玩家确认是否准备好开始游戏。游戏开始逻辑:通过
_startBtn
按钮,触发游戏开始的逻辑(例如检查所有玩家是否准备好)。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
接口。这通常是为了在序列化和反序列化时使用不同的实现(如BufferSerializerReader
和BufferSerializerWriter
)。
BufferSerializer<T>
:序列化器类,封装了序列化和反序列化的操作。它内部使用FastBufferWriter
和FastBufferReader
,提供了高性能的序列化支持,并且简化了操作,避免了手动边界检查。
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
的方法注册为NetworkManager
的OnClientConnectedCallback
事件的回调函数
NetworkManager.OnClientConnectedCallback
:这是一个事件,当有客户端成功连接到服务器时会被触发(即有玩家加入房间),这个事件只在服务器端和连接的本地客户端上运行。
+= OnClientConn;
:这是将OnClientConn
方法添加到OnClientConnectedCallback
事件的监听列表中。当有客户端连接时,OnClientConn
方法会被自动调用。
- 为什么将赋值操作放入
OnNetworkSpawn
方法中
- 确保网络状态正确:在
OnNetworkSpawn
方法中进行赋值操作,可以确保此时网络状态已经正确设置,例如IsClient
、IsServer
等属性已经可用。在Awake
和Start
方法中,这些网络状态可能还没有准备好,因此不适合进行网络相关的初始化。保证初始化顺序:对于动态生成的
NetworkObject
,OnNetworkSpawn
方法会在Start
方法之前被调用。这意味着如果在Start
方法中进行初始化,可能会导致在OnNetworkSpawn
中需要使用这些初始化的值时,这些值尚未被设置。统一初始化逻辑:无论是动态生成的
NetworkObject
还是场景中放置的NetworkObject
,OnNetworkSpawn
方法都能保证在网络对象生成后进行初始化。这使得初始化逻辑更加统一,避免了因生成方式不同而导致的初始化顺序问题。
_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)
放在其他方法(如Start
或Awake
)中,可能会导致在某些情况下玩家信息被重复初始化。
OnNetworkSpawn
确保了玩家信息只在NetworkObject
被生成时初始化一次。
为什么是base.OnNetworkSpawn();
base.OnNetworkSpawn();
确保了父类NetworkBehaviour
中的OnNetworkSpawn
方法逻辑被执行
OnNetworkSpawn
是NetworkBehaviour
类中的一个虚方法,用于在网络对象被生成时执行初始化逻辑(这也是为什么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;}
OnClientConn
是一个方法,可能是被调用来处理新玩家连接的逻辑。
UpdateAllPlayerInfos
是一个遍历所有玩家信息并发送给客户端的函数。
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();}
功能 :当一个客户端(玩家)连接到服务端时,会被触发执行。参数
obj
应该是一个用于标识玩家的唯一 ID。即此方法是对新加入的玩家进行初始化(通过后续方法进行修改某些属性后再更新)代码逻辑
创建一个
PlayerInfo
对象playerInfo
,用于存储该玩家的相关信息。将传入的玩家 ID 赋值给
playerInfo.id
。将
playerInfo.isready
设置为 false,表示该玩家目前尚未准备好(可能是在游戏开始前的准备阶段等)。调用
AddPlayer(playerInfo)
方法,将这个新玩家的信息添加到一个存储所有玩家信息的集合中(假设_allPlayerInfos
是一个存储玩家信息的字典或类似的集合)。调用
UpdateAllPlayerInfos()
方法,用于更新所有玩家的信息。
遍历玩家信息方法 UpdateAllPlayerInfos()
功能 :遍历所有玩家的信息,并通过网络调用将最新的玩家信息发送给客户端。
代码逻辑
使用
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);}}}
功能 :这是一个客户端远程调用(ClientRpc)方法,(服务器调用,客户端执行),用于在客户端接收并更新玩家信息。
代码逻辑
通过
!IsServer
判断当前是否不是在服务端执行(因为 ClientRpc 是在服务端调用,但实际执行是在客户端)。检查
_allPlayerInfos
是否已经包含该玩家的 ID(playerInfo.id
)。
如果已经包含,则将
_allPlayerInfos
中对应 ID 的玩家信息更新为新的playerInfo
。如果不包含,则通过
AddPlayer(playerInfo)
方法将该玩家信息添加到_allPlayerInfos
中。
准备状态的更新方法OnReadyToggle
方法功能
这是一个名为
OnReadyToggle
的方法,当玩家的准备状态发生改变时被调用,用于处理玩家准备状态的更新和同步。
代码逻辑
更新本地玩家状态
_cellDictionary[NetworkManager.LocalClientId].Setready(arg0)
:根据传入的布尔值arg0
,更新本地客户端对应玩家的准备状态。更新本地玩家信息显示:
调用UpdatePlayerInfo
方法,更新本地界面上显示的该玩家的准备状态信息,使玩家能够看到自己的准备状态已改变。- 根据是否为服务器执行不同操作:
- 服务器端操作:如果是服务器,直接调用
UpdateAllPlayerInfos()
方法来更新所有玩家的信息。- 客户端操作:如果不是服务器(即客户端),则调用
UpdateAllPlayerServerRpc
方法,通过 Server RPC(远程过程调用)将本地玩家的准备状态信息发送给服务器。
服务器更新远程调用方法
用于更新服务器上的所有玩家信息。
[ServerRpc(RequireOwnership = false)]
这是一个属性标记,表示该方法是一个 Server RPC 方法。这意味着该方法只能在服务器端执行,但可以被客户端调用。
RequireOwnership = false
表示调用此方法时,不需要拥有该对象的所有权。
通常,只有拥有对象的客户端才能调用其 Server RPC 方法,但这里设置为
false
,允许任何客户端调用此方法:举个实例,游戏聊天室的世界聊天
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方法的特性)
所以该方法流程为:用判断条件检查是否存在该键,如果存在该键则对玩家信息进行赋值;如果不存在该键,说明是新加入了玩家,则添加新的玩家信息。