基于Photon Fusion实现卡牌游戏非场景依赖数据的多端同步咨询
针对Photon卡牌游戏数据同步的解决方案
方案一:仅主机维护全局数据(推荐卡牌游戏场景)
卡牌游戏对数据权威性要求高(防作弊),只让Master Client(主机)保存完整的GameCore数据副本是更合理的选择,客户端仅存储自身需要的局部数据(比如自己的手牌、其他玩家的公开状态)。具体实现步骤:
- 区分主机与客户端的GameCore行为
在GameCore的启动逻辑里判断角色,非主机直接清空或禁用完整数据存储:
private void Start() { if (!PhotonNetwork.IsMasterClient) { // 非主机不维护全量playerList,仅保留本地玩家信息 playerList.Clear(); var localPlayer = PhotonNetwork.LocalPlayer; playerList.Add(new Player(localPlayer.NickName)); } }
- 通过RPC同步数据变更
所有核心操作(发牌、弃牌等)由主机发起或验证,再通过Photon RPC同步到客户端:
// 主机端:处理玩家弃牌逻辑后同步至所有客户端 public void OnPlayerDiscardCard(int playerId, Card discardedCard) { if (!PhotonNetwork.IsMasterClient) return; // 更新主机本地数据 var targetPlayer = playerList.First(p => p.name == PhotonNetwork.GetPlayer(playerId).NickName); targetPlayer.playerCards.Remove(discardedCard); // 同步到所有客户端 photonView.RPC(nameof(RPC_UpdatePlayerCards), RpcTarget.All, playerId, discardedCard); } [PunRPC] private void RPC_UpdatePlayerCards(int playerId, Card discardedCard) { // 客户端仅更新对应玩家的手牌数据 var targetPlayer = playerList.First(p => p.name == PhotonNetwork.GetPlayer(playerId).NickName); targetPlayer.playerCards.Remove(discardedCard); // 触发UI刷新逻辑 }
注意:Card、Player结构体需标记[Serializable],复杂自定义类型(如Stick)需确保Photon能序列化,或拆解为基础类型同步。
- 新玩家加入时的初始化同步
主机在新玩家加入时,主动发送该玩家所需的局部数据:
public override void OnPlayerEnteredRoom(Player newPlayer) { if (PhotonNetwork.IsMasterClient) { // 发送新玩家自身的手牌数据 var newPlayerData = playerList.First(p => p.name == newPlayer.NickName); photonView.RPC(nameof(RPC_InitPlayerData), newPlayer, newPlayerData); // 发送其他玩家的公开信息(仅分数、手牌数量,不包含具体卡牌) foreach (var player in playerList.Where(p => p.name != newPlayer.NickName)) { photonView.RPC(nameof(RPC_AddOtherPlayerInfo), newPlayer, player.name, player.score, player.playerCards.Count); } } } [PunRPC] private void RPC_InitPlayerData(Player playerData) { playerList.Add(playerData); } [PunRPC] private void RPC_AddOtherPlayerInfo(string playerName, int score, int cardCount) { var otherPlayer = new Player(playerName); otherPlayer.score = score; // 用空卡牌占位,仅展示手牌数量 for (int i = 0; i < cardCount; i++) { otherPlayer.playerCards.Add(new Card()); } playerList.Add(otherPlayer); }
方案二:客户端各自维护数据,通过事件同步
若为休闲卡牌、无需严格防作弊,可让每个客户端维护独立playerList,通过自定义事件同步变更:
- 数据变更时发送自定义事件
// 客户端执行弃牌操作 public void DiscardCard(Card card) { // 先更新本地数据 var localPlayer = playerList.First(p => p.name == PhotonNetwork.LocalPlayer.NickName); localPlayer.playerCards.Remove(card); // 发送自定义事件给所有客户端 var eventData = new Dictionary<string, object> { {"PlayerId", PhotonNetwork.LocalPlayer.ActorNumber}, {"DiscardedCard", card} }; PhotonNetwork.RaiseEvent(1, eventData, RaiseEventOptions.Default, SendOptions.SendReliable); } // 监听自定义事件 private void OnEnable() { PhotonNetwork.NetworkingClient.EventReceived += OnEventReceived; } private void OnEventReceived(EventData photonEvent) { if (photonEvent.Code == 1) { var data = (Dictionary<string, object>)photonEvent.CustomData; int playerId = (int)data["PlayerId"]; Card discardedCard = (Card)data["DiscardedCard"]; // 更新本地对应玩家的数据 var targetPlayer = playerList.First(p => p.name == PhotonNetwork.GetPlayer(playerId).NickName); targetPlayer.playerCards.Remove(discardedCard); } }
- 注意事项
- 该方式可能出现数据不一致(如网络延迟),需额外添加校验逻辑。
- 其他玩家的手牌等敏感数据不能用此方式同步,避免客户端篡改或偷窥。
关键优化点
- 调整单例逻辑:非主机的
GameCore实例仅保留必要数据,避免资源浪费。 - 权限控制:所有全局数据修改操作必须由主机验证后执行,防止恶意篡改。
- 序列化兼容:确保自定义结构体符合Photon的序列化要求,必要时实现
ISerializationCallbackReceiver接口处理复杂类型。
内容的提问来源于stack exchange,提问作者Doug Ray
相关产品推荐
相关产品推荐

