
在本文的第一部分中,我们检查了模型的排列方式,使其易于使用,但是调试模型和将接口连接起来很简单。 在这一部分中,我们将考虑返回模型更改的命令,包括其所有的美丽和多样性。 和以前一样,对我们来说,优先事项是调试的便利性,以最大程度地减少程序员创建新功能所需的手势以及代码的可读性。
手机游戏的架构解决方案。 第1部分:模型手机游戏的架构解决方案。 第3部分:射流推力视图为什么要指挥
Command模式听起来很响亮,但实际上,它只是一个对象,在其中添加了请求的操作所需的所有内容并存储在那里。 我们选择这种方法,至少是因为我们的团队将通过网络发送,甚至我们还将获得一些游戏状态副本供官方使用。 因此,当用户单击按钮时,将创建命令类的实例并将其发送给接收者。 缩写MVC中字母C的含义有些不同。
通过网络预测结果并验证命令
在这种情况下,特定的代码不如想法重要。 这是个主意:
自重型游戏无法在响应按钮之前等待服务器的响应。 当然,互联网正在变得越来越好,您可以在世界范围内拥有许多服务器,而且我什至知道有几个成功的游戏正在等待服务器的响应,其中之一甚至是《召唤战争》,但您仍然不需要这样做。 因为对于移动互联网而言,延迟是5至15秒而不是例外,因此在莫斯科,至少游戏应该确实很棒,因此玩家不必关注它。
因此,我们有一个游戏状态,它表示接口所需的所有信息,并且命令将立即应用到该状态,并且仅在将它们发送到服务器之后才应用。 通常,勤奋的Java程序员坐在服务器上,用另一种语言一对一地复制所有新功能。 在我们的“鹿”项目中,他们的人数达到了3人,而移植时所犯的错误一直是难以捉摸的欢乐的源泉。 相反,我们可以做不同的事情。 我们在.Net服务器上运行,并在服务器端运行与客户端相同的命令代码。
上一篇文章中描述的模型为我们提供了一个有趣的自我测试机会。 在客户端上执行命令后,我们将计算GameState树中发生的更改的哈希值,并将其应用于团队。 如果服务器执行相同的命令代码,并且更改的哈希值不匹配,则说明出现了问题。
第一个优点:
- 该解决方案极大地加快了开发速度,并最大程度地减少了服务器程序员的数量。
- 例如,如果程序员犯了导致不确定性行为的错误,则他从Dictionary中获取了第一个值,或者使用了DateTime.now,并且通常使用了一些未明确写入命令字段的值,那么当它们在服务器上运行时,哈希将不匹配,并且我们将找出答案。
- 暂时可以完全不使用服务器来进行客户端开发。 您甚至可以在没有服务器的情况下进入友好的alpha版本。 这不仅对独立开发人员在夜间错过梦想游戏有用。 当我在Piksonik时,有时服务器程序员会丢失所有的聚合物,我们的游戏被迫进行适度的测试,而不是服务器的虚拟人物有时会愚蠢地捍卫整个游戏状态。
由于某种原因而被系统低估的缺点:
- 如果客户程序员做错了什么,而这在测试过程中是看不见的,例如,神秘盒子中的商品出现的可能性,那么就没有人第二次写同样的东西并发现错误。 自动移植代码需要对测试更加负责任的态度。
详细的调试信息
我们声明的优先事项之一是调试便利。 如果在团队执行过程中我们抓住了执行情况-一切都清楚了,我们回退了游戏状态,将完整状态发送到日志中,并序列化了将其丢弃的团队,那么一切都将变得方便而美观。 如果我们与服务器不同步,情况将更加复杂。 因为此后客户端已经完成了其他几条命令,所以不仅要执行执行导致灾难的命令之前先找出模型处于什么状态,而且我真的很想这样做。 在每个团队面前克隆一个游戏状态太复杂且昂贵。 为了解决该问题,我们使缝合在发动机罩下的方案变得复杂。
在客户端,我们将没有一个游戏状态,而是两个。 第一个用作渲染的主界面,命令立即应用到该界面。 之后,已应用的命令将排队等待发送到服务器。 服务器在其一侧执行相同的操作,并确认一切正常。 客户端收到确认信息后,将采用相同的命令并将其应用于第二个游戏状态,从而使其进入服务器已确认为正确的状态。 同时,我们还有机会比较为安全起见所做的更改的哈希值,还可以比较客户端上整棵树的完整哈希值,我们可以在执行命令后进行计算,该哈希值很轻且被认为足够快。 如果服务器没有说一切都很好,它将向客户端询问发生了什么情况的详细信息,然后客户端可以向客户端发送序列化的第二游戏状态,该状态与在客户端上成功执行命令之前的状态完全相同。
该解决方案看起来非常吸引人,但是它带来了两个需要在代码级别解决的问题:
- 在命令参数中,不仅可以有简单类型,还可以有模型链接。 在另一个游戏状态中,模型的其他对象在完全相同的位置。 我们通过以下方式解决此问题:在客户端上执行命令之前,我们先序列化所有数据。 其中可能有指向模型的链接,我们将从游戏状态的根源以路径到模型的形式编写这些链接。 我们在团队之前这样做,因为执行之后路径可能会改变。 然后,我们将此路径发送到服务器,服务器gamestate将能够在此过程中获取与其模型的链接。 类似地,当团队应用于第二游戏状态时,可以从第二游戏状态获得模型。
- 除基本类型和模型外,团队可能还具有指向集合的链接。 字典<键,模型>,字典<模型,键>,列表<模型>,列表<值>。 对于所有这些人,他们必须编写序列化器。 没错,您不能急于这样做,在实际项目中,此类领域很少出现。
- 一次向服务器发送命令不是一个好主意,因为用户生成命令的速度比Internet来回拖动命令的速度快,而在Internet恶劣的情况下,服务器无法解决的命令池将会增加。 而不是一次发送一个命令,我们将分几批发送它们。 在这种情况下,在收到服务器的响应后,出了点问题,有必要首先将服务器已确认的同一包中的所有先前命令应用到第二状态,然后才将控制第二状态擦除并将其发送到服务器。
方便且易于编写命令
命令执行代码是游戏中第二大,最负责的代码。 它会变得更加简单和清晰,并且程序员需要花费更多的精力来编写代码,代码编写的速度越快,错误的发生就会越少,并且,出乎意料的是,程序员会变得更加快乐。 除了位于单独的静态规则类中的常规部分和函数外,我还直接将执行代码置于命令本身中,通常以扩展形式使用它们来使用它们的模型类。 我将向您展示我的宠物项目中的一些示例命令,一个非常简单,另一个更为复杂:
namespace HexKingdoms { public class FCSetSideCostCommand : HexKingdomsCommand {
如果未禁用此命令,则此命令将保留此日志。
[FCSetSideCostCommand id=1 match=FCMatchModel[0] newCost=260] Execute:00:00:00.0027546 Apply:00:00:00.0008689 { "LOCAL_PERSISTENTS":{ "@changed":{ "0":{"SIDE_COST":260}, "1":{"POSSIBLE_COST":260}, "2":{"POSSIBLE_COST":260}}}}
日志中指示的第一时间是在模型中进行所有必要更改的时间,第二时间是由接口控制器确定所有更改的时间。 这应该在日志中显示,以免意外地做一些非常慢的事情,或者只是因为模型本身的大小而及时注意到操作是否开始花费太多时间。
除了在Id-shnik上调用Persistent对象之外,这大大降低了日志的可读性,顺便说一句,在这里可以避免这种情况,而且命令代码本身以及他对游戏状态所做的日志非常清晰。 请注意,在命令文本中,程序员不会进行任何额外的移动。 您需要的一切都由引擎盖下的引擎完成。
现在让我们看一个更大的团队的例子
namespace HexKingdoms { public class FCSetUnitForPlayerCommand : HexKingdomsCommand {
这是团队留下的日志:
[FCSetUnitForPlayerCommand id=3 screen=/UI_SCREENS[main] unit=militia count=1] Execute:00:00:00.0065625 Apply:00:00:00.0004573 { "LOCAL_PERSISTENTS":{ "@changed":{ "2":{ "UNITS":{ "@set":{"militia":1}}, "ASSIGNED":7}}}, "UI_SCREENS":{ "@changed":{ "main":{ "SELECTED_UNITS":{ "@set":{ "militia":{"@new":null, "TYPE":"militia", "REMARK":null, "COUNT":1, "SELECTED":false, "DISABLED":false, "HIGHLIGHT_GREEN":false, "HIGHLIGHT_RED":false, "BUTTON_ENABLED":false}}}}}}}
正如他们所说,这要清楚得多。 花些时间为团队配备方便,紧凑且内容丰富的日志。 这是幸福的关键。 该模型必须运行非常快,因此我们在存储和访问字段的方法上使用了各种技巧。 在最坏的情况下,命令每帧执行一次,实际上,执行的频率要少几次,因此我们将仅通过反射对命令字段进行序列化和反序列化,而不会花哨。 我们只按名称对字段排序,以便顺序固定,好吧,我们将在命令有效期内编译一次字段列表,并使用本机方法C#进行读写。
接口的信息模型。
让我们迈出使引擎复杂化的下一步,这一步骤看起来很吓人,但大大简化了接口的编写和调试。 通常,尤其是在相关的MVP模式中,该模型仅包含服务器控制的业务逻辑,有关接口状态的信息存储在演示者内部。 例如,您要订购五张票。 您已经选择了他们的电话号码,但尚未点击“订购”按钮。 关于您在表格中选择了多少张门票的信息可以存储在班级的秘密角落中的某个地方,用作模型与其显示之间的衬垫。 或者,例如,播放器从一个屏幕切换到另一个屏幕,但是模型没有任何变化,而悲剧发生时他所处的位置,调试程序员只能从训练有素的测试人员的口中得知。 我认为这种方法简单,易于理解,几乎总是使用并且有点恶意。 因为如果出现问题,则绝对不可能找出导致错误的Presenter的状态。 尤其是如果错误发生在战斗服务器上,且操作费用为1000美元,而不是在受控且可复制的环境中出现在测试仪上时,则不会发生。
除了此模型之外,我们禁止除模型之外的任何人包含有关接口状态的信息。 与往常一样,这具有必须克服的优点和缺点。
- (+1)最重要的优势是节省了数月的编程工作-如果出了点问题,程序员只需在事故发生前加载游戏状态即可,不仅获得与业务模型完全相同的状态,而且还获得与屏幕上最后一个按钮的整个界面完全相同的状态。
- (+2)如果某个团队在界面中进行了某些更改,则程序员可以轻松地转到日志并以方便的json形式查看发生了什么更改,如上一节所述。
- (-1)模型中出现了很多冗余信息,这些信息不是理解游戏的业务逻辑所必需的,服务器也不需要两次。
为了解决这个问题,我们将某些字段标记为notServerVerified,如下所示,例如:
public EDictionary<string, UIStateModel> uiScreens { get { return UI_SCREENS.Get(this); } } public static PDictionaryModel<string, UIStateModel> UI_SCREENS = new PDictionaryModel<string, UIStateModel>() { notServerVerified = true };
模型的这一部分及其下的所有内容都将与客户完全相关。
如果您还记得的话,您需要导出哪些内容以及哪些内容看起来像这样的标记:
[Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2 }
因此,在导出或计算散列时,您可以指定是导出整个树还是仅导出服务器检查的树的一部分。
从这里引起的第一个明显的麻烦是需要创建单独的命令,这些命令需要由服务器检查,不需要的命令也可以,但是还有一些需要不完全检查的命令。 为了不给程序员加载用于设置命令的不必要操作,我们将再次尝试对引擎盖进行所有必要的操作。
public partial class Command { public virtual void Apply(ModelRoot root) {} public virtual void ApplyClientSide(ModelRoot root) {} }
创建命令的程序员可以覆盖这些功能之一或全部。 当然,所有这些都是很棒的,但是我如何确保程序员不会弄乱任何东西,如果他弄乱了东西,他又如何能够帮助他快速轻松地修复它? 有两种方法。 我应用了第一个,但是您可能会喜欢第二个。
第一种方式
我们使用模型的出色功能:
- 引擎调用第一个函数,然后它接收游戏状态的服务器检查部分中的更改哈希。 如果没有变化,那么我们将与客户团队专门合作。
- 我们获得整个模型中更改的模型哈希,而不仅仅是服务器验证的哈希。 如果它与先前的哈希不同,则程序员搞砸了并更改了模型中服务器未检查的部分内容。 我们遍历状态树,将程序员作为notExecute的完整列表转储为notServerVerified = true字段以及位于他所更改的树下的字段的完整列表。
- 我们称第二个功能。 我们从模型中获得了检查部分中发生的更改的哈希值。 如果它与第一次调用后的哈希值不一致,则在第二个函数中,程序员可以执行任何操作。 如果在这种情况下想要获得非常有用的日志,则将整个模型回滚到其原始状态,将其序列化为文件,然后程序员将派上用场进行调试,然后将其整体克隆(两行-serialization-deserialization),现在我们首先应用第一个函数,然后提交更改,以使模型看起来不变,然后应用第二个函数。 然后,我们将所有更改以JSON的形式导出到服务器检查的部分中,并将其包含在滥用执行中,以便羞愧的程序员可以立即看到他更改了哪些内容以及更改的位置,不应更改的内容。
它看起来确实很吓人,但实际上它只有7行,因为执行此操作的函数已经准备就绪(除了从第二段遍历树之外)。 由于这是接待,因此我们可以让自己的行为不尽如人意。
第二种方式
更为残酷的是,现在在ModelRoot中,我们有一个锁定字段,但是我们可以将其划分为两个,一个只会将选中的字段锁定到服务器,而另一个只会将选中的字段锁定。 在这种情况下,做错了事的程序员将立即得到关于它的解释,并与他做事的地方有联系。 这种方法的唯一缺点是,如果在我们的树中将一个模型属性标记为不可验证,则即使未标记每个字段,也不会检查树下面的所有与哈希计算和变更控制有关的内容。 当然,锁不会进入层次结构,这意味着将必须标记树的未选中部分的所有字段,并且在某些地方使用UI中的相同类以及树的通常部分将无法工作。 作为一种选择,这样的构造是可能的(我将其简化):
public class GameState : Model { public RootModelData data; public RootModelLocal local; } public class RootModel { public bool locked { get; } }
然后证明每个子树都有自己的锁。 GameState继承了模型,因为它比为所有相同功能的单独实现要容易得多。
必要的改进
当然,负责处理团队的经理将不得不添加新功能。 更改的本质是,并非所有命令都将发送到服务器,而是仅发送创建已检查的更改的命令。 在其一侧的服务器将不会引发整个游戏状态树,而只会引发正在检查的部分,因此哈希值将仅针对正在检查的部分而重合。 在服务器上执行命令时,将仅启动命令两个功能中的第一个功能,并且在解析游戏状态中对模型的引用时,如果路径导致树的不可验证部分,则将null放置在命令变量中而不是模型中。 所有未发送的团队都将诚实地与通常的团队保持一致,但将被视为已确认。 一旦他们到达生产线并且没有未确认的对象,它们将立即应用于第二状态。
在实现上没有什么根本复杂的事情。 只是模型的每个字段的属性都具有一个条件,即遍历树。
另一个必要的改进-在树的选中和未选中部分中,您将需要为ParsistentModel提供单独的Factory,而NextFreeId会有所不同。
服务器启动的命令
如果服务器希望将其命令推送给客户端,则会出现一些问题,因为相对于服务器的客户端状态可能已经向前跳了几步。 主要思想是,如果服务器需要发送其命令,则它将服务器通知与下一个响应一起发送给客户端,并将其写入发送给该客户端的通知字段中。 客户端收到一个通知,在此基础上形成一个命令,并将其放在队列中的末尾,在客户端上已完成但尚未到达服务器的通知之后。 一段时间后,该命令会作为使用模型的正常过程的一部分发送到服务器。 接收到此命令进行处理后,服务器会将通知从传出队列中抛出。 如果客户端未在设置的时间内使用下一个软件包响应该通知,则会向其发送重新引导命令。 如果收到通知的客户端掉线,稍后连接或出于某种原因加载了游戏,则服务器将在将状态赋予状态之前将所有通知转换为命令,在其一侧执行它们,然后才将加入的客户端赋予新状态。 请注意,当玩家设法在服务器从他那里拿走钱的那一刻设法花掉钱时,该玩家可能会处于带有负资源的冲突状态。 巧合不太可能,但是DAU较大时,这几乎是不可避免的。 因此,在这种情况下,界面和游戏规则不应落伍。
需要知道服务器响应的执行命令
一个典型的错误是认为只能从服务器获得随机数。 从公用sid开始,什么都不会阻止您从客户端和服务器同时运行相同的伪随机数生成器。 而且,当前种子可以直接存储在游戏状态中。 有些人可能发现很难同步此生成器的响应。 实际上,为此,在同一文章中再有一个号码就足够了-到目前为止,确切地从生成器接收了多少个号码。 如果您的生成器由于某种原因无法收敛,那么您可能会遇到错误,并且代码无法确定地工作。 这个事实不应该隐藏在地毯下,而应该进行梳理并寻找错误。 对于绝大多数情况,甚至包括神秘的盒子,这种方法就足够了。
但是,有时此选项不合适。 例如,您正在获得非常昂贵的奖品,并且不希望狡猾的同志对游戏进行反编译,并且编写了一个机器人,该机器人可以告诉您如果立即打开菱形框会掉出什么,以及如果在之前的另一个位置旋转鼓会怎样。 您可以为每个随机变量分别存储种子,这可以防止正面黑客入侵,但是它对告诉您当前需要多少盒产品的机器人无济于事。 好吧,最明显的情况是您可能不想在客户端配置中大放异彩,了解一些罕见事件的可能性。 简而言之,有时有必要等待服务器响应。
此类情况不应通过引擎的其他功能来解决,而应通过将团队分成两部分来解决-第一种是准备情况,并将界面置于等待通知的状态,第二种实际上是在通知您所需答案的状态。 即使您在客户端上紧密阻塞了它们之间的接口,也可能会漏出另一个命令-例如,一个能量单位将及时恢复。
重要的是要了解这种情况不是规则,而是例外。 实际上,大多数游戏只需要一个团队等待答案-GetInitialGameState。 这样的命令的另一包是例如元游戏GetLeaderboard中的玩家间交互。 其他所有200个都是确定性的。
服务器数据存储和服务器优化的泥泞主题
我马上就承认自己是客户,有时我从熟悉的服务器服务员那里听说过这样的想法和算法,他们甚至都不会潜入我的脑海。 通过与同事的交流,我以某种方式得出了在理想情况下我的体系结构应如何在服务器端工作的示意图。 但是:有禁忌症,有必要咨询专业服务器。
首先是关于数据存储。 您的服务器端可能还有其他限制。 例如,您可能被禁止使用静态字段。 此外,命令和模型的代码是可自动移植的,但是客户端和服务器上的属性代码完全不必重合。 任何东西都可以隐藏在那里,例如,直到从内存缓存中延迟字段值的初始化为止。 属性字段还可以接收服务器使用的其他参数,但不会影响客户端的工作。
服务器的第一个主要区别:字段被序列化和反序列化。 合理的解决方案是将大多数状态树序列化为一个巨大的二进制或json字段。 同时,某些字段取自表。 这是必要的,因为某些字段的值对于玩家之间的交互服务正常工作是必不可少的。 例如,图标和级别不断被各种各样的人抽搐。 最好将它们保存在常规数据库中。 当某人决定调查自己的领土时,除了他之外的其他人很少会需要一个人的全部或部分但详细的状态。
此外,一次从基础场拉出场是不方便的,并且可能拖了很长时间。 一个非常不标准的解决方案,仅适用于我们的体系结构,可能包括以下事实:客户端在执行命令时,会收集有关单独存储在其getter设法接触的表中的所有字段的信息,并将此信息添加到命令中,以便服务器可以引发这组字段一个请求到数据库。 当然,要有合理的限制,以免乞求那些精打细算的弯弯曲曲的程序员造成的DDOS。
使用这种单独的存储,当一个玩家爬入另一个玩家的数据时(例如,从他那里偷钱),人们应该考虑交易的机制。 但是在一般情况下,我们通过通知来执行此操作。 也就是说,小偷立即收到了他的钱,被抢劫的人收到了一条通知,其中有指示要注销这笔钱。
服务器之间如何划分团队
现在是服务器的第二重要时刻。 有两种方法。 首先,为了处理任何请求(或请求包),将整个状态从数据库或高速缓存提升到内存,进行处理,然后返回到数据库。 操作是在许多不同的执行服务器上自动完成的,它们只有一个共同的基础,即使这样也不总是如此。 作为客户,将每个团队的整体状态提高到每个人都令人震惊,但是我看到了它是如何工作的,并且它非常可靠且可扩展。 第二种选择是,状态一旦在内存中就会上升并一直存在,直到客户机掉线后才偶尔将其当前状态添加到数据库中。
我无权告诉您这种方法的优缺点。如果有人在评论中向我解释为什么第一个人普遍享有生命权,那将是很好的。第二个选项引发了有关如何在偶然被证明是在不同服务器上引发的玩家之间进行交互的问题。例如,如果几个氏族成员正准备发动联合攻击,这可能至关重要。您不能向他人展示其党员的状态,而延迟10次保存。不幸的是,我不会在这里打开它,也不会通过上述通知进行交互,将命令从一台服务器发送到另一台服务器-现在,该命令无法保存在该服务器上引发的玩家的当前状态。如果服务器在不同位置具有相同级别的可用性,您可以管理平衡器,也可以尝试将播放器从一台服务器悄悄转移到另一台服务器。如果您更好地了解解决方案,请确保在评论中进行描述。与时间共舞
让我们从一个问题开始,我真的很想在面试时打倒大家:在这里,您有一个客户端和一个服务器,每个都有自己相当准确的时钟。如何找出它们之间的差异。解决在餐巾纸上的咖啡店中解决此问题的尝试揭示了程序员的最佳和最差的素质。事实是该问题没有正式的数学正确解。但是,受访者通常都知道,通常在第五个问题上花一分钟,直到提出问题后再进行。以及他遇到这种洞察力的方式以及下一步要做的事情-谈到角色中最重要的事情-当项目中出现真正的问题时,这个人会做什么。我所知道的最佳解决方案使我无法找出确切的区别,而是通过从大量请求到客户端到服务器的最佳数据包的时间,再到从服务器到客户端的最佳数据包的时间,弄清它的范围。总体而言,这将使您获得几十毫秒的精度。这比手机游戏的元游戏所需要的要好很多倍,这里我们没有VR多人游戏或CS,但是对于程序员来说,代表时钟同步困难的规模和性质仍然不错。最有可能的是,您很长时间就能知道平均延迟为ping的一半,并且偏差的截止值超过30%。您可能会遇到的第二个很酷的情况是进行打滑游戏,并在手机上传送时钟。在这两种情况下,应用程序中的时间都会急剧而突然地改变,因此必须正确解决。至少要使游戏重新启动,但是最好不要在每次滑过后重新启动游戏,因此您不能在应用程序中使用自应用程序启动以来经过的时间。第三,由于某种原因,这种情况是一些程序员理解的问题,尽管对此有一个正确的解决方案:操作绝对不能在服务器时间上执行。例如,当生产请求到达服务器时,开始生产产品。否则,请与您的确定性方法道别,并根据客户端和服务器在是否可能单击奖励方面的不同意见,每天捕获35,000个不同步信息。正确的决定是团队记录有关执行时间的信息。服务器依次检查当前服务器时间与命令中的时间之间的时差是否在允许的时间间隔内,如果是,则使用客户端声明的时间执行该命令。采访的另一项任务是:超时后,客户端将尝试重新启动-30秒。服务器可接受的时差的限制是多少?提示1:间隔不对称。提示2:再次阅读本节的第一段,指定如何延长间隔,以免每天在边缘效果上捕获3000个错误。为了使它美观且正确地工作,最好在命令调用参数中添加一个附加参数-调用时间。像这样:
public interface Command { void Apply(ModelRoot root, long time); }
顺便说一句,我对您的建议是不要在模型中使用本机Unity类型的时间,否则您会感到厌烦。最好在需要使用方便的转换方法时将UnixTime存储在服务器时间中,并将它们存储在模型中的特殊PTime字段中,该字段与PValue <long>不同,唯一的区别在于,当导出为JSON时,它将多余的信息添加到方括号中,导入:时间以人类可读的格式。你不能服从我。我警告过你第四种情况:在比赛状态中,有些情况下必须在没有玩家参与的情况下启动团队,例如及时恢复能量。实际上,这是非常普遍的情况。我想要一个领域,很方便地练习。例如PTimeOut,可以在其中记录应该在其后创建和执行命令的时间点。在代码中,它可能看起来像这样: public class MyModel : Model { public static PTimeOut RESTORE_ENERGY = new PTimeOut() {command = (model, property) => new RestoreEnergyCommand() { model = model}} public long restoreEnergy { get { return RESTORE_ENERGY.Get(this); } set { RESTORE_ENERGY.Set(this, value); }} }
当然,在播放器的初始加载期间,服务器必须首先激发所有这些命令的创建和执行,然后才将状态提供给播放器。这里的陷阱是,所有这些著名地干扰了玩家在这段时间内可能收到的通知。因此,有必要先拧开第一次通知时间之前的时间,如果您需要同时拉出一堆命令,然后从通知本身创建命令,然后拧开时间直到下一个通知,然后计算出来,依此类推。如果整个假期都不适合服务器超时,并且如果玩家对通知进行了大量练习,则有可能做到这一点,我们会将当前状态从内存写入数据库,然后使用命令重新连接到客户端。所有这些命令都必须以某种方式了解它们需要创建并执行的内容。我稍微有些曲折但又方便的解决方案是,该模型还有一个挑战,那就是遍及模型的整个层次结构,在执行每个命令后以及在计时器上都会抽动。当然,这几乎是在更新中绕树走动的额外开销,相反,您可以订阅或取消订阅currentTime事件,因为该字段中的每个更改都不会出现在游戏状态之外: public partial class Model { public void SetCurrentTime(long time); } vs public partial class RootModel { public event Action<long> setCurrentTime; }
这很好,但是问题在于,从模型树中永久删除并包含此类字段的模型将仍然订阅该事件,并且必须正确解决。在发送命令之前,请检查它们是否仍在树中,并且与该事件或控制反转之间的链接不牢固,因此,它们不会对GC仍然不可访问。附录1,来自现实生活的典型案例
我从评论到第一部分。在玩具中,通常不是在命令模型后立即执行某些操作,而是在某种动画结束时立即执行。在我们的实践中,有一种情况是神秘盒子打开,当然,只有在动画播放到最后时,金钱才应该改变。我们的一位开发人员决定简化生活,不更改命令值,而是告诉服务器他已更改了该值,并在动画结束时运行了回调,该回调将模型中的值校正为所需的值。简而言之,做得好。他在这些神秘的盒子上做了两个星期,然后由于他的活动又出现了三个非常难以捉住的错误,尽管事实上“改写正常”的时间当然是,但我们不得不再花三个星期来捉住它们,没有人能强调。从它生动地跟随我认为结论是,从一开始就以正常的反应性进行所有操作都更好。所以,我的决定是这样的。当然,金钱不是位于一个单独的字段中,而是存货词典中的对象之一,但是现在这并不那么重要。该模型的一部分由服务器检查,业务逻辑在此基础上工作,而另一部分仅在客户端上存在。一旦做出决定,就会在主模型中立即赚钱,并且在“延期放映”列表的第二部分中,创建一个元素,其元素的数量与开始时的动画外观相同,并在动画结束时启动删除该元素的命令。这样的纯客户注释“尚未显示此金额”。在实际字段中,不仅会显示该字段的值,而且还会显示该字段的值减去所有客户端延迟。之所以分成两个团队,是因为如果客户在第一队之后重新启动,但在第二队之前重新启动,该玩家收到的所有款项将记入他的帐户,没有任何标记和例外。在代码中,将是这样的: public class OpenMisterBox : Command { public BoxItemModel item; public int slot;
到底我们有什么?
有两个视图,在其中一个视图中播放了一些动画,其结尾是等待将钱显示在完全不同的视图中,而后者不知道谁和为什么要显示不同的含义。一切都是反应性的。在任何时候,您都可以将GameState的完整状态加载到游戏中,并且它将从我们上次中断的地方开始完全播放,包括动画启动。真相将从一开始就开始,因为我们不删除动画阶段,但是如果我们确实需要它,我们甚至可以删除它。合计
通过带有规则的模型,团队和静态文件来设计游戏的业务逻辑,将它们用漂亮的详细且自动生成的日志四面包围,并包含程序员看到新功能时犯下的许多典型错误的信息性执行,在我看来,这是生存的正确方法白光。不仅因为您可以更快地提交新功能几倍。这仍然非常重要,因为如果您可以轻松下载和调试新功能,那么游戏设计人员将有时间在相同的程序员的陪同下进行几倍的游戏设计实验。在充分尊重我们工作的前提下,游戏是否失败取决于我们,但是游戏是否射击取决于游戏碟,因此需要为它们提供实验空间。现在,我请您为我回答非常重要的问题。如果您有关于如何做我做不好的事情的想法,或者只是想评论我的答案,我会在评论中等待您。有关大量语法错误的合作建议和说明,请参阅PM。