Unity手游之路<一>C#版本Protobuf

本文探讨了Protobuf-net在Unity C#开发中的使用,通过序列化类为二进制文件并进行反序列化,展示了其高效、数据冗余少、编程简单的特点。重点介绍了序列化和反序列化的过程,以及使用Protobuf-net实现这一功能的方法。
一个游戏包含了各种数据,包括本地数据和与服务端通信的数据。今天我们来谈谈如何存储数据,以及客户端和服务端的编码方式。根据以前的经验,我们可以用字符串,XML,json...甚至可以直接存储二进制。各种方式都有各自的优劣,有些性能比较好,但是实现方式比较麻烦。有些数据冗余太多。
  • 简介
今天我们来学习一种广泛使用的数据格式:Protobuf。简单来说,它就是一种二进制格式,是google发起的,目前广泛应用在各种开发语言中。具体的介绍可以参见:https://code.google.com/p/protobuf/ 。我们之所以选择protobuf,是基于它的高效,数据冗余少,编程简单等特性。关于C#的protobuf实现,网上有好几个版本,公认比较好的是Protobuf-net。

  • 实例
先来看一个最简单的例子:把一个类用Protobuf格式序列化到一个二进制文件。再读取二进制数据,反序列化出对象数据。

从网上参考了一个例子 http://blog.youkuaiyun.com/ddxkjddx/article/details/7239798

//----------------实体类----------------------

[csharp] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. using UnityEngine;  
  2. using System.Collections;  
  3. using ProtoBuf;  
  4. using System;  
  5. using System.Collections.Generic;  
  6.   
  7.   
  8. [ProtoContract]  
  9. public class Test {  
  10.   
  11.   
  12.     [ProtoMember(1)]  
  13.     public int Id  
  14.     {  
  15.         get;  
  16.         set;  
  17.     }  
  18.   
  19.   
  20.     [ProtoMember(2)]  
  21.     public List<String> data  
  22.     {  
  23.         get;  
  24.         set;  
  25.     }  
  26.   
  27.   
  28.     public override string ToString()  
  29.     {  
  30.         String str = Id+":";  
  31.         foreach (String d in data)  
  32.         {  
  33.             str += d + ",";  
  34.         }  
  35.         return str;  
  36.     }  
  37.       
  38. }  
//-----------测试类---------------------------
[csharp] view plain copy 在CODE上查看代码片 派生到我的代码片
  1. using UnityEngine;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4. using System.IO;  
  5. using ProtoBuf;  
  6. using System;  
  7.   
  8.   
  9. public class ProtobufNet : MonoBehaviour {  
  10.   
  11.   
  12.     private const String PATH = "c://data.bin";  
  13.   
  14.   
  15.     void Start () {  
  16.         //生成数据  
  17.         List<Test> testData = new List<Test>();  
  18.         for (int i = 0; i < 100; i++)  
  19.         {  
  20.             testData.Add(new Test() { Id = i, data = new List<string>(new string[]{"1","2","3"}) });  
  21.         }  
  22.         //将数据序列化后存入本地文件  
  23.         using(Stream file = File.Create(PATH))  
  24.         {  
  25.             Serializer.Serialize<List<Test>>(file, testData);  
  26.             file.Close();  
  27.         }  
  28.         //将数据从文件中读取出来,反序列化  
  29.         List<Test> fileData;  
  30.         using (Stream file = File.OpenRead(PATH))  
  31.         {  
  32.             fileData = Serializer.Deserialize<List<Test>>(file);  
  33.         }  
  34.         //打印数据  
  35.         foreach (Test data in fileData)  
  36.         {  
  37.            Debug.Log(data);  
  38.         }  
  39.     }  
  40.       
  41. }  

  • 总结

Protobuf-net 利用Attributes来实现序列化字段,对程序员的负担减轻,代码侵入性也降低。接下来,我将会写一个简单的Unity c/s demo,其中的通信编码就是用到Protobuf,

using System; //需要用到MemoryStream using System.IO; using UnityEngine; //引入ProtoBuf命名空间 using ProtoBuf; /// /// 测试类 /// public class TestProtobuf : MonoBehaviour { /// /// 用于测试的数据类 /// [ProtoContract] //声明这个类能被序列化 public class UserData { //声明每个需要被序列化的成员,编号从1开始 [ProtoMember(1)] public int id; [ProtoMember(2)] public string name; [ProtoMember(3)] public int level; } //测试代码 void Start() { //将要被序列化的UserData示例 UserData user1 = new UserData (); user1.id = 1; user1.name = "User1"; user1.level = 10; //打印user1 Debug.Log (string.Format ("user1-> id:{0}, name:{1}, level:{2}", user1.id, user1.name, user1.level)); //序列化 byte[] buff = null; using (MemoryStream ms = new MemoryStream ()) { Serializer.Serialize (ms, user1); ms.Position = 0; int length = (int)ms.Length; buff = new byte[length]; ms.Read (buff, 0, length); } //输出字节数组 Debug.Log (string.Format("Serialized data-> {0}", BitConverter.ToString(buff))); //反序列化 UserData user2 = default(UserData); using (MemoryStream ms = new MemoryStream (buff)) { user2 = Serializer.Deserialize (ms); } //打印反序列化生成的user2 Debug.Log (string.Format ("user2-> id:{0}, name:{1}, level:{2}", user2.id, user2.name, user2.level)); } } 作者:qufangliu 链接:https://www.jianshu.com/p/d9be1b3d2446 來源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
<think> 我们正在讨论基于Unity和Mirror的联机桌游网络架构。Mirror是个基于Unity的高层网络库,它简化了多人游戏的开发,支持多种传输层协议(如TCP、UDP、WebSockets等)。对于联机桌游,我们通常需要处理多个玩家在同个房间中,进行回合制操作,并且需要状态同步和权威服务器验证。 架构选择: 1. **权威服务器模型**:在Mirror中,通常使用服务器作为权威,客户端发送命令,服务器执行逻辑并同步状态。这可以防止作弊。 2. **房间/大厅系统**:使用Mirror的NetworkRoomManager来管理房间,包括玩家加入、离开、准备状态等。 3. **状态同步**:使用Mirror的SyncVar、SyncList和自定义的NetworkBehaviour来同步游戏状态。 以下是个简单的基于Mirror的联机桌游架构实现: 步骤: 1. 创建网络管理器:使用NetworkRoomManager作为基础,管理房间和玩家。 2. 创建游戏房间:当房间人数满足条件时开始游戏。 3. 玩家控制:每个玩家在服务器上有个网络标识,通过命令(Command)和客户端RPC(ClientRpc)进行通信。 代码示例: 首先,创建个继承自NetworkRoomManager的类,用于管理房间: ```csharp using Mirror; using UnityEngine; public class MyRoomManager : NetworkRoomManager { // 当房间内所有玩家都准备就绪时,服务器会调用此方法开始游戏 public override void OnRoomServerPlayersReady() { base.OnRoomServerPlayersReady(); // 所有玩家都准备就绪,开始游戏 ServerChangeScene(GameplayScene); } } ``` 然后,创建个代表房间内玩家的对象,注意房间内玩家和游戏内玩家可能是不同的对象(在进入游戏场景后,房间玩家对象会被替换为游戏玩家对象): ```csharp using Mirror; using UnityEngine; public class RoomPlayer : NetworkRoomPlayer { [SyncVar(hook = nameof(HandleReadyStateChanged))] public bool IsReady = false; public override void OnStartAuthority() { base.OnStartAuthority(); // 本地玩家可以操作准备按钮 } [Command] public void CmdChangeReadyState(bool ready) { IsReady = ready; // 通知房间管理器检查是否所有玩家都准备好了 MyRoomManager.Instance.NotifyReadyState(); } private void HandleReadyStateChanged(bool oldValue, bool newValue) { // 更新UI显示准备状态 } } ``` 在游戏场景中,创建游戏玩家对象: ```csharp using Mirror; using UnityEngine; public class GamePlayer : NetworkBehaviour { [SyncVar] public string PlayerName; // 当玩家被创建时,由服务器设置玩家名称 public override void OnStartServer() { base.OnStartServer(); // 从房间玩家对象中获取名称(实际中可能需要通过连接传递) // 这里假设我们通过其他方式传递,比如在房间玩家中设置 } // 示例:玩家执行个动作(例如出牌) [Command] public void CmdPlayCard(int cardId) { // 在服务器上验证动作合法性 if (IsValidPlay(cardId)) { // 执行出牌逻辑 RpcShowCardPlayed(cardId); // 通知所有客户端展示出牌 } } [ClientRpc] private void RpcShowCardPlayed(int cardId) { // 在客户端上展示出牌 } private bool IsValidPlay(int cardId) { // 验证逻辑,例如是否是该玩家的回合,卡牌是否在手中等 return true; } } ``` 解释: 1. **MyRoomManager**:继承自NetworkRoomManager,负责管理房间场景和游戏场景的切换。当所有玩家准备就绪后,切换到游戏场景。 2. **RoomPlayer**:代表房间内的玩家,处理准备状态。通过CmdChangeReadyState命令来改变准备状态,并同步给其他客户端。 3. **GamePlayer**:游戏场景中的玩家对象。通过CmdPlayCard发送命令到服务器,服务器验证后执行并同步结果到所有客户端(使用ClientRpc)。 针对桌游的特性,我们还需要: - 游戏状态管理:使用个专门的GameManager来管理游戏状态(如当前回合、玩家顺序等),并同步到所有客户端。 - 断线重连:Mirror提供了断线处理,但需要我们自己实现状态恢复。可以在服务器上保存游戏状态,当玩家重连时同步当前状态。 优化建议: 1. **消息压缩**:对于卡牌等数据,使用较小的数据类型(如byte代替int)并使用Mirror的序列化压缩。 2. **状态同步优化**:只同步变化的状态,例如使用SyncList或SyncDictionary来同步手牌变化,而不是整个手牌列表。 3. **预测与回滚**:对于需要快速响应的操作(如抽牌),可以在客户端预测,如果服务器验证失败则回滚。 相关问题: 1. 如何处理玩家中途加入游戏? 2. 在Mirror中如何实现游戏状态的保存和加载? 3. 如何防止玩家作弊? 4. 如何处理网络延迟导致的玩家操作不同步? 注意:以上代码是简化示例,实际开发中需要根据具体游戏规则进行扩展。 </think>### 基于Unity和Mirror的联机桌游网络架构方案 Mirror是Unity专用的高性能网络库(取代已弃用的UNet),特别适合开发**权威服务器架构**的联机桌游。以下是优化方案: #### 推荐架构 ```mermaid graph TD A[Unity客户端] --> B[Mirror NetworkManager] B --> C[游戏房间服务器] C --> D[Redis状态存储] C --> E[MySQL玩家数据] F[Web管理后台] --> D F --> E ``` #### 核心组件实现(C#) ##### 1. 网络管理器初始化 ```csharp using Mirror; using UnityEngine; public class GameNetworkManager : NetworkManager { public override void OnStartServer() { base.OnStartServer(); Debug.Log("服务器已启动"); // 注册自定义消息处理器 NetworkServer.RegisterHandler<PlayerActionMessage>(OnPlayerAction); } public override void OnClientConnect(NetworkConnection conn) { base.OnClientConnect(conn); Debug.Log($"玩家 {conn.connectionId} 已连接"); // 发送玩家初始化数据 conn.Send(new PlayerInfoMessage { playerName = PlayerPrefs.GetString("PlayerName"), avatarId = PlayerPrefs.GetInt("Avatar") }); } private void OnPlayerAction(NetworkConnection conn, PlayerActionMessage msg) { // 服务器验证并处理玩家动作 if (GameRoomManager.ValidateAction(conn, msg)) { GameRoomManager.ApplyAction(conn, msg); // 广播状态更新 NetworkServer.SendToAll(new GameStateUpdateMessage { state = GameRoomManager.CurrentState }); } } } ``` ##### 2. 游戏房间管理器(服务器端) ```csharp public static class GameRoomManager { private static readonly Dictionary<int, Room> rooms = new Dictionary<int, Room>(); public static void CreateRoom(int roomId) { rooms[roomId] = new Room(roomId); } public static bool ValidateAction(NetworkConnection conn, PlayerActionMessage action) { // 验证玩家权限 if (!rooms.TryGetValue(action.roomId, out Room room)) return false; // 验证回合顺序 if (room.CurrentPlayer != conn.connectionId) return false; // 验证动作合法性 return action.Type switch { ActionType.PlayCard => ValidateCardPlay(room, action), ActionType.RollDice => true, // 骰子无需额外验证 _ => false }; } public static void ApplyAction(NetworkConnection conn, PlayerActionMessage action) { Room room = rooms[action.roomId]; switch (action.Type) { case ActionType.PlayCard: room.PlayCard(conn.connectionId, action.cardId); break; case ActionType.RollDice: int rollResult = new System.Random().Next(1, 7); room.SetDiceResult(rollResult); break; } // 保存状态到Redis RedisClient.SaveRoomState(room); } } public class Room { public int RoomId { get; } public GameState CurrentState { get; private set; } public int CurrentPlayer { get; private set; } public void PlayCard(int playerId, int cardId) { // 卡牌游戏逻辑实现 CurrentState.hands[playerId].Remove(cardId); CurrentState.tableCards.Add(cardId); // 切换到下个玩家 MoveToNextPlayer(); } } ``` ##### 3. 客户端动作发送 ```csharp public class PlayerController : NetworkBehaviour { [Command] public void CmdPlayCard(int cardId) { // 客户端发送动作到服务器 NetworkClient.Send(new PlayerActionMessage { Type = ActionType.PlayCard, cardId = cardId, roomId = CurrentRoom.Id }); } [TargetRpc] public void RpcUpdateGameState(NetworkConnection target, GameStateUpdateMessage state) { // 接收服务器状态更新 GameUI.UpdateState(state); } } ``` #### Mirror架构优势 1. **权威服务器保障**: - 所有游戏逻辑在`GameRoomManager`执行 - 客户端通过`[Command]`发送动作请求 - 服务器通过`[TargetRpc]`同步状态 2. **高效消息传输**: - 内置消息压缩(可配置) - 支持WebSocket/TCP/UDP协议 - 自动处理网络异常 3. **状态同步优化**: ```csharp [SyncVar(hook = nameof(OnScoresChanged))] public int[] playerScores; private void OnScoresChanged(int[] oldScores, int[] newScores) { // 仅当分数变化时触发UI更新 ScoreBoard.UpdateScores(newScores); } ``` 4. **断线重连处理**: ```csharp public override void OnServerAddPlayer(NetworkConnection conn) { if (PlayerSession.TryRestore(conn, out PlayerData data)) { // 恢复玩家状态 conn.Send(new GameStateUpdateMessage { state = data.LastState }); } else { // 新玩家初始化 base.OnServerAddPlayer(conn); } } ``` #### 部署方案 | 组件 | 实现方式 | 说明 | |------------------|-----------------------------|-------------------------------| | 游戏服务器 | Linux + Docker容器 | 使用Mirror的Headless模式 | | 状态存储 | Redis + Protobuf序列化 | 存储房间状态(<5ms读写) | | 玩家匹配 | Photon Matchmaking服务 | 集成Mirror的匹配API | | 防作弊系统 | 行为分析 + 指令哈希校验 | 校验客户端指令序列 | #### 性能优化技巧 1. **消息压缩**: ```csharp public class DiceRollMessage : MessageBase { [Compression(Level = 3)] public byte playerId; [BitCount(3)] // 骰子值1-6只需3bit public byte diceValue; } ``` 2. **预测回滚**(针对实时操作): ```csharp public void ClientPredictCardPlay(int cardId) { // 客户端预测执行 localHand.Remove(cardId); // 服务器确认后修正 StartCoroutine(WaitForServerConfirmation(cardId)); } ``` 3. **分帧同步**(大型桌游): ```csharp IEnumerator SyncLargeState() { for(int i=0; i<state.chunks.Length; i++) { conn.Send(state.chunks[i]); yield return null; // 每帧发送个分块 } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值