You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Photon Fusion实现卡牌游戏非场景依赖数据的多端同步咨询

针对Photon卡牌游戏数据同步的解决方案

方案一:仅主机维护全局数据(推荐卡牌游戏场景)

卡牌游戏对数据权威性要求高(防作弊),只让Master Client(主机)保存完整的GameCore数据副本是更合理的选择,客户端仅存储自身需要的局部数据(比如自己的手牌、其他玩家的公开状态)。具体实现步骤:

  1. 区分主机与客户端的GameCore行为
    在GameCore的启动逻辑里判断角色,非主机直接清空或禁用完整数据存储:
private void Start()
{
    if (!PhotonNetwork.IsMasterClient)
    {
        // 非主机不维护全量playerList,仅保留本地玩家信息
        playerList.Clear();
        var localPlayer = PhotonNetwork.LocalPlayer;
        playerList.Add(new Player(localPlayer.NickName));
    }
}
  1. 通过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能序列化,或拆解为基础类型同步。

  1. 新玩家加入时的初始化同步
    主机在新玩家加入时,主动发送该玩家所需的局部数据:
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,通过自定义事件同步变更:

  1. 数据变更时发送自定义事件
// 客户端执行弃牌操作
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);
    }
}
  1. 注意事项
  • 该方式可能出现数据不一致(如网络延迟),需额外添加校验逻辑。
  • 其他玩家的手牌等敏感数据不能用此方式同步,避免客户端篡改或偷窥。

关键优化点

  • 调整单例逻辑:非主机的GameCore实例仅保留必要数据,避免资源浪费。
  • 权限控制:所有全局数据修改操作必须由主机验证后执行,防止恶意篡改。
  • 序列化兼容:确保自定义结构体符合Photon的序列化要求,必要时实现ISerializationCallbackReceiver接口处理复杂类型。

内容的提问来源于stack exchange,提问作者Doug Ray

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 19:30:58