一、先搞懂Photon Fusion的两个“权力”

很多人刚上手多人游戏开发,尤其是用Photon Fusion的时候,会碰到明明自己写的逻辑在单机测没问题,上线多人玩就出各种奇怪的bug——比如你按跳跃键,别人的角色突然跳了,或者你明明跳了,角色又卡在半空重复。这本质上是把Photon Fusion里的两个核心权限搞混了:Input Authority和State Authority。 用生活化的例子讲,Input Authority就像你玩手游时握在手里的遥控器,谁持有这个遥控器,谁才能发送“我要动”“我要跳”这类输入指令;而State Authority就像游戏里的“裁判/规则制定者”,谁持有这个权限,谁才能修改游戏的最终状态——比如角色的位置、血量、是否跳跃完成这些结果性的数据。这两个权限必须各司其职,不能混在一起。

二、踩坑现场:混在一起的后果有多严重

我之前做过一款多人休闲跳跃小游戏,就是没分清楚这两个权限,出了一堆离谱的bug,给大家还原一下当时的错误写法和实际问题。

2.1 错误的代码写法(踩坑版本)

这里用的技术栈是:Unity + Photon Fusion,所有示例都基于这个栈,不会混搭。错误代码的问题就是把输入修改和状态修改放在了同一个地方,跨了权限:

using UnityEngine;
using Photon.Pun;

// 这个脚本绑定在玩家角色上,错误地把输入和状态修改放在一起
public class BadPlayerController : MonoBehaviourPun
{
    private Rigidbody2D rb;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // 错误1:直接在客户端修改状态,不管权限
        // 如果当前玩家持有Input Authority就改,否则也偷偷改
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 错误2:把跳跃的状态直接存在客户端,没交给State Authority
            rb.velocity = new Vector2(rb.velocity.x, 10f);
        }
    }
}

我当时的想法很简单:谁按了空格键,就让谁跳,结果没想到,因为每个客户端都会检测空格键,而且Photon Fusion的同步机制是默认会把状态同步给所有人,导致多个客户端同时修改角色的速度,就出现了“玩家A按空格,玩家B的角色也跳”的离谱情况,而且角色的速度被多个客户端覆盖,就会卡住或者来回飘。

2.2 实际出现的同步异常问题

当时上线后碰到的典型问题有三个: 第一,角色动作错乱:比如玩家A按跳跃键,玩家B的角色会触发跳跃动画,因为双方的客户端都在处理输入和修改状态; 第二,状态不一致:比如玩家跳起来后,明明已经掉下来了,另一个玩家看到的还卡在半空,因为状态只在自己客户端改了,没经过State Authority验证; 第三,偶尔的卡死:比如玩家跳的时候,突然被另一个客户端的状态覆盖,导致角色速度变成0,卡在半空不能动,只能重新登录。

三、权责分离的解决方案:把遥控器和裁判分开

搞懂了两个权限的区别,解决方法就很简单了:把输入的“发指令”和状态的“改结果”完全分开,谁管遥控器(Input Authority),谁只管发指令,不能碰最终状态;谁管裁判(State Authority),谁只管验证指令并修改状态,不能管输入发送。

3.1 明确权限的职责边界

Input Authority的唯一职责:接收本地玩家的输入,生成输入事件,把事件发给State Authority,不能直接修改游戏状态; State Authority的唯一职责:接收合法的输入事件,验证事件的有效性(比如是否符合游戏规则),然后修改游戏状态,把最终状态同步给所有玩家。

3.2 正确的代码写法(解决方案版本)

还是用同一个技术栈Unity+Photon Fusion,这次把两个职责分开,写两个脚本:一个负责输入发指令,一个负责状态修改。

using UnityEngine;
using Photon.Pun;

// 脚本1:只负责处理玩家输入,发送事件(Input Authority专属)
public class PlayerInputSender : MonoBehaviourPun
{
    // 检测本地玩家的跳跃输入,只有持有Input Authority的客户端才会执行
    void Update()
    {
        if (photonView.IsMine && Input.GetKeyDown(KeyCode.Space))
        {
            // 不修改状态,只是发送输入事件给State Authority
            photonView.RPC("RpcSendJumpInput", RpcTarget.MasterClient);
        }
    }

    // RPC函数,只有State Authority(这里是主机MasterClient)能收到
    [PunRPC]
    private void RpcSendJumpInput()
    {
        // 主机作为State Authority,处理这个输入事件,修改状态
        if (photonView.Owner != null)
        {
            photonView.GetComponent<GameStateUpdater>().ProcessJumpInput(photonView.Owner);
        }
    }
}
using UnityEngine;
using Photon.Pun;

// 脚本2:只负责处理游戏状态修改(State Authority专属,这里交给主机)
public class GameStateUpdater : MonoBehaviourPun
{
    private Rigidbody2D rb;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    // 只有State Authority的客户端能执行这个方法,验证并修改状态
    public void ProcessJumpInput(Photon.Realtime.Player targetPlayer)
    {
        // 验证:只有持有这个角色权限的玩家发送的输入才合法
        if (photonView.Owner == targetPlayer)
        {
            // 现在才修改角色的状态,最终结果交给State Authority,不会混乱
            rb.velocity = new Vector2(rb.velocity.x, 10f);
            // 同步状态给所有玩家,Photon Fusion会帮我们处理同步
            photonView.RPC("RpcSyncJumpState", RpcTarget.AllBuffered, rb.velocity.y);
        }
    }

    // 同步跳跃状态给所有客户端
    [PunRPC]
    private void RpcSyncJumpState(float velocityY)
    {
        rb.velocity = new Vector2(rb.velocity.x, velocityY);
    }
}

修改之后,bug果然就没了:每个客户端只处理自己的输入(遥控器),主机作为裁判处理所有状态修改,不会出现多个客户端改同一个状态的情况,同步也稳定了。

四、权责分离的应用场景

这种方法适合几乎所有的多人网络游戏,尤其是需要玩家操作、状态会变化的游戏: 第一个场景:多人休闲竞技游戏,比如刚才的跳跃游戏、五子棋、俄罗斯方块,每个玩家的输入由自己管,落子、消行等状态由主机或者当前回合玩家作为State Authority; 第二个场景:大型多人在线角色扮演游戏(MMORPG),玩家的角色移动、技能释放的输入由自己管,角色的血量、装备状态由服务器作为State Authority,避免作弊; 第三个场景:协作类游戏,比如多人解谜游戏,每个玩家的输入(点击、拖拽)自己管,解谜的进度、线索状态由主机管,确保所有玩家看到的进度一致。

五、技术的优缺点

5.1 优点

第一,同步稳定性大幅提升:因为只有State Authority能修改状态,不会出现多个客户端同时修改同一个数据导致的冲突,避免了画面错乱、状态不一致的问题; 第二,减少作弊风险:因为最终的状态只能由权威端修改,客户端不能随便改自己的状态,比如角色的血量、攻击力,作弊者没法通过修改本地数据来影响游戏; 第三,开发职责清晰:每个脚本只做自己的事,输入的归输入,状态的归状态,后续维护和修改的时候,不用到处找代码,降低了bug产生的概率。

5.2 缺点

第一,初期学习成本稍高:刚上手的时候要搞懂两个权限的区别和配置,容易搞混什么时候该发RPC,什么时候该处理状态; 第二,要处理权限转移的逻辑:比如主机断开连接的时候,需要把State Authority转移给其他玩家,这部分逻辑要自己写,增加了开发量; 第三,轻量游戏可能觉得繁琐:比如小团队做一个2人的小游戏,不需要太复杂的同步,用权责分离的话会觉得步骤多,不如直接单机改方便。

六、注意事项

  1. 严格区分两个权限:Input Authority只用来发输入事件,绝对不能修改状态,State Authority只用来处理状态,绝对不能直接接收输入(除非是权威端自己的输入);
  2. 输入事件要带来源:比如RPC事件里要带上发送输入的玩家ID,这样State Authority能验证这个输入是不是合法的,避免恶意输入;
  3. 处理权限转移:当当前的State Authority断开连接时,要及时把权限转移给其他玩家,不然游戏会卡住,这时候可以用Photon Fusion的 photonView.TransferOwnership()方法;
  4. 不要在客户端直接修改同步变量:Photon Fusion里的NetworkTransform这类组件,只能由State Authority修改,客户端不能直接改,不然会被同步覆盖;
  5. 测试权限配置:开发的时候要多在不同玩家身上测试,比如让不同的客户端持有Input Authority,看会不会出现状态错乱,及时调整权限。

七、总结

多人游戏的同步问题本质上是权责的混乱,Photon Fusion的Input Authority和State Authority就是为了帮开发者理清权责而设计的。很多人刚上手的时候会因为贪方便,把两者混在一起,结果出现各种难以调试的同步bug,反而浪费更多时间。只要记住“遥控器管输入,裁判管状态”的原则,把职责分开,不仅能解决大部分同步异常的问题,还能让后续的开发和维护变得更轻松。不管是做休闲小游戏还是大型网游,权责分离都是网络开发里的核心思维,一定要牢记。