3年前,我开始研发剑与剑;Ravens是一款开源的在线多玩家游戏,改编自《我爱》战略棋盘游戏《权力的游戏:棋盘游戏》(第二版),由Christian T.Petersen设计,由Fantasy Flight Games出版。截至2022年2月,每天约有500名玩家聚集在该平台上,自发布以来,已经玩了2000多个游戏。当我停止积极开发S&;R、 由于开源社区的努力,该平台仍在增加新功能。
我在开发S&;R、 我想把我学到的一些知识分享给可能有兴趣做类似项目的人。关于它是如何工作的,有很多话要说,但这篇博文将重点介绍我如何设计游戏中的网络部分。我将首先以更正式的方式描述这个问题。我将继续解释如何在s&;R、 以及描述我发现或想象的其他可能的解决方案。我将详细介绍其优点和优点;每种方法的缺点,并总结出我认为最好的方法(剧透警告:这是最后一种)👀).
在单人游戏中,所有东西都生活在一台计算机中。玩家的动作将应用于游戏状态,对该游戏状态的修改将反映在玩家的屏幕上。在在线多人游戏环境中,情况就不同了。每个玩家都在自己的电脑上玩,电脑上都有自己当前的游戏信息,并且都有自己的UI来显示游戏的当前状态。
用户界面客户端服务器程序。。。游戏状态游戏状态用户界面客户端程序。。。游戏状态查看器不支持完整的SVG 1.1。UI根据游戏状态的本地副本向玩家显示游戏的当前状态。客户端负责与服务器的通信,既发送玩家的动作,又接收有关游戏状态的新信息。
我们感兴趣的问题是如何将客户端游戏的不同本地状态与服务器的游戏状态同步。更具体地说:当服务器将玩家的动作应用于其游戏状态时,它必须如何将对游戏状态的修改传达给客户端。
最明显的解决方案是将服务器接收到的任何操作应用于游戏,并将游戏状态的不同更新传输给客户端。下图显示了它的作用。
客户端A客户端B服务器攻击王';他和Fo一起着陆。。。从Wint中删除Footman。。。在King'中添加步兵;s洛杉矶。。。在King'开始战斗;s洛杉矶。。。从Wint中删除Footman。。。在King'中添加步兵;s洛杉矶。。。在King'开始战斗;s洛杉矶。。。查看器不支持完整的SVG 1.1。这是刀剑&;乌鸦。它简单、直观,并且很容易知道您正在向不同的客户发送或不发送哪种数据。这也使得拥有秘密数据变得微不足道(即,应该只有一部分玩家知道的数据)。如果一名玩家抽到一张牌,并将其放在他的(秘密)手中,那么你可以只将抽到的牌传送给该玩家,这样其他玩家就不会知道这是哪张牌。
这种方法的第一个缺点是,必须对游戏状态的所有可能更新进行编码。当然,在JS中有一些方法可以自动实现这一点,例如使用装饰程序来控制对游戏状态变量的访问,但这可能会降低代码的可读性。
第二个缺点是,由于您可能会为一个操作发送多个更新,因此在收到所有更新之前,客户端的本地游戏状态可能暂时处于无效状态。在上图中,在更新“冬城移除步兵”和“国王添加步兵”之间';着陆时,缺少一名步兵,这将修改UI中显示的步兵数量。虽然这个特殊问题可以通过发送一个组合更新来解决(例如,将步兵从冬城移动到国王登陆),但并非所有更新都可以轻松连接。
解决这一问题的更好方法是合并由于该操作而完成的所有更新,并立即发送它们。这就是下一种方法的本质。
增量更新传播方法的工作原理是计算游戏的新状态与应用动作之前的游戏状态之间的增量。然后将该增量发送给客户端,以便他们可以将其应用于自己的本地游戏状态。这就是游戏引擎boardgame的方式。木卫一工作。
客户端A客户端B服务器攻击王';他和Fo一起着陆。。。移除《国王与#39》中的男仆;s着陆,。。。移除《国王与#39》中的男仆;s着陆,。。。查看器不支持完整的SVG 1.1。这解决了前面方法中描述的两个缺点。我们不再需要对所有可能的更新进行编码,因为一旦您更改了某些内容,它将在操作处理后在增量中进行计算。您不再会获得暂时的无效状态,因为更新将立即以原子方式应用。
然而,我们失去了一件事,那就是管理秘密国家很容易。如果要防止某些机密信息被发送到特定客户机,服务器必须在将其发送到客户机之前过滤掉任何潜在的私人信息。
它依赖于这样一个假设,即处理玩家的动作是确定性的,这意味着对于给定的游戏状态,应用一个动作将始终为我们提供相同的游戏结果状态。我们可以利用这个属性来避免将游戏状态的更新传播给客户端。相反,服务器可以应用它从客户机收到的操作,然后将此操作传播给客户机,然后客户机可以应用该操作来派生自己的新游戏状态。由于应用动作是确定性的,客户端将达到与服务器相同的游戏状态。1.
客户端A客户端B服务器攻击王';他和Fo一起着陆。。。攻击国王';是降落机智。。。攻击国王';是降落机智。。。查看器不支持完整的SVG1.1。首先,我们不需要在代码中编写额外的网络逻辑。唯一需要实现的是传播玩家所做的动作。
其次,带宽消耗与对游戏状态所做修改的大小无关。如果一个玩家的动作在我们的游戏状态中改变了1000个实体,服务器仍然只需要传输动作,而不需要传输更改。这就是为什么确定性锁步被用于实时战略游戏的原因,比如《帝国时代》。虽然回合制游戏(棋盘游戏更不常见)在执行动作时会有很多移动实体,但这为回合制游戏开辟了新的可能性。
第三,由于实际的游戏代码是在客户端上运行的,我们可以对游戏状态进行不同更新的动画。例如,如果一个玩家的动作会将他们的钱减少10,然后增加40,我们可以在客户端播放两个不同的动画,而在之前的解决方案中,我们只会从服务器收到这样一个事实,即筹集的钱是30,这阻止了客户端这样做。
第四,当玩家决定执行一个动作时,客户端可以在将动作发送到服务器后直接将其应用到自己的游戏状态,而无需等待服务器的确认。这一过程被称为“乐观更新”,让我们能够为玩家提供无滞后体验。
总的来说,这个解决方案相当优雅。我们只需要实现玩家行为的传播,一旦完成,我们就可以专注于实现游戏性,而不需要接触网络代码!
不过,有一个很大的缺点。为了确保客户端和服务器在处理一个动作后到达相同的游戏状态,我们必须确保它们最初都拥有完全相同的游戏状态。一开始,这可能会让你觉得不可能有秘密状态。事实上,如果我们的网络解决方案依赖于所有参与者之间的游戏状态相同,我们怎么可能只有服务器端的状态?
通过允许客户机与服务器略有不同,我们可以非常优雅地解决这个问题。如果一个动作需要一个或多个客户端之前隐藏的状态,我们可以让服务器通过向客户端发送游戏状态的这一特定部分来协调差异。
让我们用《剑与剑》中的一个例子来说明这一点;乌鸦。当一个玩家将他们的军队移动到另一个玩家的领地时,他们会触发一场战斗。解决S&;R涉及两名玩家同时从他们手中选择一名将军来领导他们的军队。这种机制会导致激烈的心智游戏,两名玩家都会尝试猜测对手将选择哪种将军,以便选择合适的计数器,同时怀疑对手是否不会计划这样做,并选择计数器的计数器,要求你选择计数器的计数器,以此类推。
显然,如果对方球员还没有做出选择,保密对方球员的选择是很重要的。
客户A客户B服务器选择泰温·兰尼斯特(A)选择卡片(A)选择泰温·兰尼斯特选择玛格利·泰瑞尔(B)选择玛格利·泰瑞尔。。。(B) 选择一张卡片。。。客户端和服务器有相同的游戏状态,安全地进行客户端B和#39;s的游戏状态不同于服务器';s game state Viewer不支持完整的SVG 1.1。当客户端A将其操作发送到服务器(选择Tywin Lannister)时,我们会将此操作传播给两名玩家,但不会在从发送到客户端B的消息中筛选出所选的领导者之前,客户机B的游戏状态与服务器的游戏状态不同,因为它不知道A选择了哪个领导者。当客户B发送他们的操作(选择Margaery Tyrell)时,我们应用相同的逻辑,从发送给客户A的消息中过滤出所选的领导者。由于两个玩家都选择了他们的领导者,我们可以通过发送其他玩家的选择来调和游戏状态的差异。在这个小动作之后,所有的客户机都有相同的游戏状态,可以决定性地解决剩余的战斗。
请注意,虽然我们可以选择在A选择了客户B的负责人后不向其发送任何信息,但发送此信息可以在B的UI中显示A已经选择了他们的负责人。
如果我必须开发剑&;乌鸦从无到有,我会用确定性的方法。只需实现一次网络,并完成它,这是非常优雅和吸引人的。因为AGoT:TBG是一个相当复杂的游戏,有很多不同的阶段,必须将每个交互连接起来,产生大量样板代码,这相当于代码的很大一部分。最重要的是,我从来没有设法轻松地将动画(片段移动、卡片从手到板移动等)添加到UI中,这对AGoT:TBG没有太大帮助,因为一个动作可以有很多状态更新。
使用确定性方法的另一个好处是,你可以很容易地创建一个库来处理基于回合的游戏中的所有网络部分,让开发人员专注于开发游戏本身的机制。我已经开始研究这样一个图书馆了Ravens。不幸的是,由于外部环境,我没有继续开发它。
如果您实际上正在实现一个基于回合的多人游戏,我希望本文能够帮助您选择一个合理的架构。如果没有,那么我希望内容和写作足够有趣!
虽然游戏中的随机性(洗牌、掷骰子等)可能会打破决定论属性,但您可以使用种子伪随机数生成器来确保随机掷骰始终是相同的客户端和服务器端。↩