使用 Photon Fusion 的 Unity 棋盘游戏

Via*_*dow 1 network-programming unity-game-engine photon

我希望使用 Photon Fusion 制作一款在线多人棋盘游戏。基本的游戏玩法包括人们点击商店按钮购买东西,然后点击下一个回合按钮将回合传递给下一个玩家。我还想只向当前轮到的人显示某些文本框。

然而,我真的很难找到一个关于如何完成这项工作的教程。抱歉,如果这是一个愚蠢的问题,任何指点将不胜感激。网络是我的弱点之一。

我曾尝试对此进行研究(Photon 文档、Youtube、reddit 等),但我发现的几乎所有内容都使用 Photon 的预测运动和用于 fps 或跑酷游戏的键盘按钮。

作为网络方面的新手,我正在努力弄清楚如何制作一个场景,该场景在每个回合中使用由不同人控制的按钮,并为每个人移动游戏对象。

gjt*_*uia 7

长话短说

使用RPCs[Networked]变量和OnChanged回调在主机和客户端之间来回通信。

您可以创建一个[Networked]变量来保存PlayerRef当前玩家的OnChanged回调。当[Networked]变量发生变化时,所有玩家都会调用OnChanged回调。然后每个玩家都可以用 来检查是否轮到他们了Runner.LocalPlayer。如果轮到他们,则仅显示该玩家的按钮和文本框。

当玩家按下下一回合按钮时,RPC从该玩家呼叫主机,然后主机将[Networked]再次更改当前玩家。下一个玩家将检测到此更改并显示该玩家相应的 UI。

您可以在按下下一个回合按钮时隐藏当前玩家的 UI,也可以在回调期间隐藏OnChanged。它是由你决定。

强烈建议您查看 Fusion Imposter 游戏示例。它基本上是我们之间的克隆,每个玩家都可以完成单独的任务。向每个玩家显示的 UI 是不同的,它们共同影响已完成任务总数的网络状态。它还使用 RPC 从客户端到主机进行通信。

有用的链接

(真的)很长的答案

我明白你的意思。Photon Fusion 的大多数示例和文档都面向具有连续输入的游戏。但在使用 Fusion 一段时间之后,我学到了一些东西。希望他们可以帮助您找到问题的解决方案。

做一件事有很多种方法,没有绝对“正确”的方法。当您将来遇到另一个问题时,当前问题的解决方案可能会改变,这没关系。代码库是有生命力的,并且一直在变化,在添加新功能的同时不断重构。因此,首先了解 Photon Fusion 多人游戏网络背后的概念非常重要,这样您就可以找到一个解决方案来让事情现在正常进行,修复错误,并在将来需要更改或尝试其他解决方案时做出决定。

概念概述

一般来说,任何联网的东西都只能通过StateAuthority. 例如,只有StateAuthority可以更改[Networked]变量并生成NetworkObjects。我们可以称之为网络状态。

网络状态在所有客户端之间同步,客户端看到的大部分内容只是响应网络状态的变化。例如,假设我们有一个GameObject带有NetworkTransform组件的组件。transform由于该组件,它GameObject是网络状态的一部分NetworkTransform。如果StateAuthority更改transform.positionthis 的GameObject,由于transform是网络状态的一部分,因此所有客户端中 this 的位置GameObject也会随着网络状态的变化而改变。所有客户都会看到同样的GameObject感动。然后,该位置GameObject被视为在所有客户端之间同步。如果任何客户端尝试更改transform.position此游戏对象的 ,则不会发生任何事情,因为只有StateAuthority可以更改网络状态。

对于要更改或影响网络状态的客户端,在 Fusion 中可以通过两种方式执行此操作。

1. 网络输入

第一种方法是通过NetworkInput. 这很可能是您在 Fusion 文档中最常遇到的内容。通过NetworkInput,客户可以影响NetworkObjects他们所拥有的事情InputAuthority首先从客户端收集输入,然后将其发送到主机并在循环期间应用FixedUpdateNetwork,更新客户端所拥有的 NetworkObject 的网络状态InputAuthority,并同步所有其他客户端的网络状态。

Fusion 以一种非常强大的方式做到了这一点,预测是开箱即用的,这样即使主机中的网络状态尚未改变,客户端也可以获得即时反馈。

但概念仍然是一样的。网络状态只能通过 更改StateAuthority。网络状态在所有客户端之间同步。NetworkObjects客户端可能会影响其所拥有的网络状态InputAuthority,但最终是StateAuthority允许对网络状态进行这些更改并在所有其他客户端之间同步这些更改。

但是,正如您所说,大多数文档都围绕收集键盘输入展开。有一小段展示了如何使用 UI 轮询输入,但在这种情况下,我的猜测是,这适用于带有 UI 移动按钮的手机游戏。对于您点击按钮购买物品和下一步按钮的情况没有用。

2. 远程过程调用

第二种方法是通过RPC. 在文档中,您可以感觉到 Fusion 非常不鼓励使用RPCs. 我能理解为什么。

RPCs

  • 未勾选对齐
  • 不是网络状态的一部分

因此,RPCs不适合基于刻度的模拟游戏,例如 fps 和跑酷游戏。在这些类型的游戏中,NetworkInput在大多数情况下确实绰绰有余,因为玩家主要通过键盘输入和鼠标点击与世界交互。

RPCs不成为网络状态的一部分也是一个问题。例如,假设GameObject场景中有NetworkBehaviour脚本但没有NetworkTransform组件。客户端可以调用 来直接在所有其他客户端中RPC更改transform.position此设置。GameObject事实上,所有客户都可以看到GameObject从旧位置到新位置的转变。但是,如果新客户端加入游戏, 则将GameObject保留在其旧位置,因为 (1) 的位置GameObject不是网络状态的一部分,并且 (2) 不是RPC网络状态的一部分并且只会被触发一次。RPC对于加入游戏的新客户,不会再次触发。然后,该位置GameObject被视为在所有客户端之间不同步。

继续前面的例子,我们如何在GameObject不使用 的情况下同步 的位置NetworkTransform?永远记住,网络状态只能通过 更改StateAuthority,然后在所有客户端之间同步。将位置添加到网络状态的一种方法是创建一个存储位置的变量并使用回调[Networked]更改位置。GameObjectOnChanged

-> 客户调用RPC发送StateAuthorityVector3仓位

->StateAuthority接收RPC并更改[Networked]位置变量

-> 所有客户端检测到变量[Networked]已更改

-> 所有客户端调用回调OnChanged来更新transform.positionGameObject

然后,该位置GameObject现在在所有客户端之间同步。不同之处在于RPC是用来改变网络状态,然后在所有客户端之间同步,而不是 RPC 直接改变 的位置GameObject

对于新加入游戏的客户,

-> 新客户端将transform.position位置设置[Networked]Spawned()

这就是保持同步位置所需的全部GameObject操作,即使对于没有收到RPC. 这是因为结果RPC存储在[Networked]变量中并且是网络状态的一部分。

总的来说,RPCs如果

  • 方法调用不需要刻度对齐
  • 方法调用不频繁调用
  • 其结果RPC可以存储在网络状态中以便在客户端之间同步。

我的建议

事实上,所有有关预测运动和键盘按钮的文档根本不适合您的情况。您应该高度考虑使用RPCs而不是NetworkInput. 您的游戏不是基于刻度的模拟,因此RPCs非常适合您的情况。

困难的部分是设计游戏的架构,例如决定如何将网络状态存储在[Networked]变量中,以及应该调用哪些方法RPC以及是否应该使用 OnChanged 来反映客户端的更改或使用RPC从主机到特定的更改客户。

请记住,它们不是网络状态的一部分,您应该找到某种方法来存储网络状态中RPCs的结果。RPC在大多数情况下,客户端会调用主机RPC,主机更改网络状态,然后客户端将根据更改的网络状态进行操作。

在极少数情况下,您可以RPC从主机直接调用客户端,或者极少数情况下从客户端调用另一个客户端。再说一次,这是你必须做出的决定,如果效果不好,可以稍后更改你的解决方案。

如果我处于你的情况,我会有一个[Networked]变量来存储PlayerRef当前轮到的玩家的。我们可以这样称呼它_currentPlayer

currentPlayer发生变化时

-> 触发OnChanged每个玩家的回调

-> 每个玩家检查是否等于currentPlayer他们自己的本地PlayerRefRunner.LocalPlayer

-> 如果轮到他们了,则仅显示该玩家的 UI

public class GameManager : NetworkBehaviour 
{
    [Networked(OnChanged = nameof(OnCurrentPlayerChanged))] 
    private PlayerRef _currentPlayer {get; set;}

    // ...

    private static void OnCurrentPlayerChanged(Changed<GameManager> changed)
    {
        changed.Behaviour.OnCurrentPlayerChanged();   
    }

    private void OnCurrentPlayerChanged()
    {
        // If it is my turn
        if (_currentPlayer === Runner.LocalPlayer)
        {
            // show the buttons / textboxes
        }

        // If it is not my turn
        else
        {
            // you may want to hide the buttons and textboxes for other players
        }
    }
}


Run Code Online (Sandbox Code Playgroud)

当按钮被按下时,

-> 玩家可以呼叫RPCs主机。

-> 然后主机可以更改网络状态,例如。更新玩家拥有的硬币数量或移动游戏对象。

-> 然后网络状态将在所有客户端之间同步,因此每个人都可以看到相同的游戏对象移动。

// Called locally on client only
public void OnButtonPress()
{
    int someDataFromThePlayer = ...; // Whatever data you want to sent to the host
    RPC_OnPlayerButtonPressed(someRandomDataFromThePlayer);
}

// Called on the host only
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
private void RPC_OnPlayerButtonPressed(int someRandomDataFromThePlayer)
{
    // Do whatever you want here with someRandomDataFromThePlayer and change the network state
    // The data does not have to be an int. Check the docs for the supported types.
}
Run Code Online (Sandbox Code Playgroud)

如果一名玩家结束了自己的回合

-> 玩家可以呼叫RPCs主机。

-> 主机可以更改_currentPlayer为下一个

-> 所有玩家都跟注OnChanged

-> 之前打开 UI 的玩家将关闭

-> 当前关闭 UI 的玩家现在将打开

public class GameManager : NetworkBehaviour 
{
    [Networked(OnChanged = nameof(OnCurrentPlayerChanged))] 
    private PlayerRef _currentPlayer {get; set;}

    // ...

    // Called locally on client only
    public void OnEndTurnButtonPress()
    {
        RPC_OnPlayerEndTurn();
    }

    // Called on the host only
    [Rpc(RpcSources.All, RpcTarget.StateAuthority)]
    private void RPC_OnPlayerEndTurn()
    {
        PlayerRef nextPlayer = ...; // Somehow get the PlayerRef of the next player
        _currentPlayer = nextPlayer; // Triggers the OnChanged below on all clients
    }

    // ...

    private static void OnCurrentPlayerChanged(Changed<GameManager> changed)
    {
        changed.Behaviour.OnCurrentPlayerChanged();   
    }

    private void OnCurrentPlayerChanged()
    {
        // If it is my turn
        if (_currentPlayer === Runner.LocalPlayer)
        {
            // show the buttons / textboxes
        }

        // If it is not my turn
        else
        {
            // you may want to hide the buttons and textboxes for other players
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

案例研究:Fusion Imposter

融合冒名顶替者

尽管 Fusion 的许多文档和示例都显示了具有连续输入的游戏,但我发现这个很棒的示例为不同的玩家显示了不同的 UI。不同玩家的用户界面也会共同影响网络状态,网络状态会在所有玩家之间同步和显示。

这个游戏基本上是3D的AmongUs。玩家四处走动执行个人任务,而冒名顶替者则试图杀死所有人。

这是相当先进的。但这里是一个概述和我对玩家在 TaskStation 附近按 E 时发生的情况的理解。

  1. PlayerMovement.FixedUpdateNetwork()
  • 检查玩家是否按下 E。如果是,将呼叫TryUse()本地。
  1. PlayerMovement.TryUse()
  • 检查最近的可交互项是否是TaskStation. 如果是这样,请拨打Interact()本地电话
  1. TaskStation.Interact()=>TaskUI.Begin()
  • 仅在本地为该玩家打开 UI。
  • 当玩家完成任务时,TaskBase.Completed()被调用。
  1. TaskBase.Completed()=>GameManager.Instance.CompleteTask()
  • RPC玩家向被叫的主机发起呼叫Rpc_CompleteTask()
  1. GameManager.Rpc_CompleteTask()
  • 仅在主机上调用,它会更新网络变量TasksCompleted,从而触发TasksCompletedChanged OnChanged回调。
  • 检查是否所有任务都已完成。如果完成所有任务,船员获胜。
  1. GameManager.TasksCompletedChanged()
  • 更新所有玩家的总任务栏 UI 填充量。

正如您所看到的,每个玩家都可以拥有自己的 UI 来完成自己的事情。每个玩家通过 与主机通信RPC,这会更改网络状态,然后在所有客户端之间同步。

概括

哈哈,我得意忘形了,似乎写了一篇非常非常长的文章。

以下是要点

  • 网络状态只能通过以下方式更改StateAuthority
  • 网络状态在所有客户端之间同步
  • 客户端只能通过NetworkInput或更改/影响网络状态RPC
  • 如果您的游戏不是基于刻度的模拟,那么RPCs这是一个很好的解决方案
  • RPC从玩家到主机 -> 主机更改网络状态 -> 所有玩家检测网络状态的变化,并OnChanged自行决定如何在本地处理此变化

希望这可以帮助!

参考

如果你们彼此相爱,那么所有人都会知道你们是我的弟子。约翰福音 13:35 (ESV)