
在先前的文章中,我们描述了如何方便且功能强大地布置模型,适合于哪种命令系统,充当控制器,是时候讨论替代MVC缩写的第三个字母了。
实际上,Assetstore有一个现成的非常复杂的UniRX库,该库实现了反应性和统一的控制反转。 但是,我们将在本文结尾处讨论它,因为针对我们的案例,这个功能强大,功能强大且兼容RX的工具非常多余。 无需拉起RX,就可以完全完成我们需要的一切,如果您拥有RX,则可以轻松地做到这一点。
手机游戏的架构解决方案。 第1部分:模型手机游戏的架构解决方案。 第2部分:命令及其队列当一个人刚开始编写第一款游戏时,存在一个可以绘制整个表格(或表格的一部分)并在每次重要变化时都将其拉出的功能似乎是合乎逻辑的。 随着时间的流逝,界面的尺寸不断增大,模具的形状和零件变成一百个,然后变成两百个,当皮夹改变其状态时,必须重新绘制其中的四分之一。 然后经理来说:“就像在游戏中一样”,如果按钮内有一个区域,其中有一个子区域,那么您需要在按钮上做一个小红点,现在您有足够的资源通过单击来做某事这很重要。 仅此而已...
脱离绘画概念的过程分为几个阶段。 首先,解决单一领域的问题。 例如,您在模型中有一个字段,在其中应显示其所有内容的文本字段。 好的,我们启动一个对象,该对象订阅该字段的更新,每次更新时,它将结果添加到文本字段中。 在代码中,如下所示:
var observable = new ChildControl(FCPlayerModel.ASSIGNED, Player); observable.onChange(i => Assigned.text = i.ToString())
现在,我们不需要遵循重绘,只需创建此设计,然后模型中发生的所有事情都将落入界面中。 很好,但是很麻烦,它包含许多显然不必要的手势,程序员必须用双手写100,500次,有时甚至会出错。 让我们将这些广告包装在扩展功能中,该功能会将多余的字母隐藏在内部。
Player.Get(c, FCPlayerModel.ASSIGNED).Action(c, i => Assigned.text = i.ToString());
好多了,但这还不是全部。 将模型字段移到文本字段是一种非常常见的操作,我们将为其创建单独的包装函数。 在我看来,现在的结果非常简短而且很好。
Player.Get(c, FCPlayerModel.ASSIGNED).SetText(c, Assigned);
在这里,我展示了主要思想,在创建余生的接口时,我将以其为指导:“如果程序员必须做至少两次以上的操作,则将其包装在特殊的便捷短函数中。”
垃圾收集
反应式接口工程的一个副作用是创建了一堆对象,这些对象已订阅了某个对象,因此没有特殊的踢就不会留下内存。 对于我自己来说,在远古时代,我想出了一种方法,它虽然不那么漂亮,但却简单易用。 创建任何表单时,将创建与此表单相关联创建的所有控制器的列表;为简便起见,它简称为“ c”。 所有特殊的包装函数都将此列表作为第一个必需的参数,当以DisconnectModel的形式出现时,它将传递所有控件的列表,并毫不留情地使用公共祖先中的代码对其进行破坏。 没有美丽和优雅,但价格便宜,可靠且相对实用。 如果您需要IView而不是控制表来输入并将其提供给所有这些位置,则可以提高安全性。 本质上是同一件事,忘记填写相同的内容是行不通的,但是更难破解。 我怕忘记,但我不是很害怕有人会故意破坏系统,因为需要用腰带和其他非软件方法来战斗这样聪明的人,所以我只限于c。
可以从UniRX中提取一种替代方法。 每个包装器都会创建一个新对象,该对象具有指向它所侦听的上一个对象的链接。 最后,调用AddTo(组件)方法,该方法将整个控件链归因于某些可破坏的对象。 在我们的示例中,此类代码如下所示:
Player.Get(FCPlayerModel.ASSIGNED).SetText(Assigned).AddTo(this);
如果该链的最后一个所有者决定销毁,他将向分配给他的所有控件发送命令“如果没有人听我的话,请杀死自己,以使自己除掉”。 并且整个链条都被乖乖清理了。 因此,当然要简洁得多,但是从我的角度来看,有一个重要的缺陷。 AddTo可能会被意外遗忘,直到为时已晚,没人会知道。
实际上,您可以使用肮脏的Unity hack,而无需在View中添加任何其他代码:
public static T AddTo<T>(this T disposable, Component component) where T : IDisposable { var composite = new CompositeDisposable(disposable); Observable .EveryUpdate() .Where(_ => component == null) .Subscribe(_ => composite.Dispose()) .AddTo(composite); return disposable; }
如您所知,在Unity中指向Unicomponent组件或GameObject的链接为空。 但是您需要了解,此hakokostyl会为销毁的每个控件链创建一个Update侦听器,这已经有点礼貌了。
模型独立界面
但是,我们可以轻松实现的理想情况是,可以随时加载完整的GameState(服务器验证的模型和UI的数据模型),并且应用程序将处于完全相同的状态,直到所有按钮的状态。 这有两个原因。 首先是一些程序员喜欢将其生命周期与表单本身的生命周期完全相同的事实,存储在表单控制器内部,甚至存储在视图本身中。 第二个问题是,即使表单的所有数据都在其模型中,创建和填写表单本身的命令仍采用显式函数调用的形式,并带有一些其他参数,例如,列表中的哪个字段应重点关注。
如果您确实不想要调试的便利,则无需处理。 但是我们并不是那样,我们希望像使用模型进行基本操作一样方便地调试接口。 为此,以下重点。 在模型的UI部分中,设置了一个变量,例如.main,并在其中作为命令的一部分,放置了要查看的表单的模型。 该变量的状态由特殊的控制器监视,如果模型出现在该变量中,则根据其类型,实例化所需的形式,将其放置在需要的位置,然后将其发送给ConnectModel(模型)。 如果从模型中释放了变量,则控制器将从画布中删除该表单并使用它。 因此,不会发生绕过模型的操作,并且您对接口所做的所有操作在ExportChanges模型上都清晰可见。 然后,我们遵循“已完成两次包装的所有工作”的原则,并在接口的所有级别使用完全相同的控制器。 如果模具在另一个模具中有放置的位置,则会为其创建UI模型,并在父模具的模型中创建一个变量。 与列表完全相同。
这种方法的副作用是,将两个文件添加到任何表单,一个文件具有该表单的数据模型,另一个文件通常是包含指向UI元素的链接的monobah,后者在其ConnectModel函数中接收了该模型后,将为所有对象创建所有反应式控制器模型字段和所有UI元素。 嗯,它甚至更紧凑,因此使用起来也很方便,这可能是不可能的。 如果可能,在注释中写。
列表控件
典型的情况是模型包含一些元素的列表。 由于我希望所有事情都非常方便地完成,最好是在一行上完成,因此我还想为列表做一些方便处理的事情。 一条线是可能的,但事实证明它太长了。 根据经验,事实证明,几乎所有案件的多样性都仅由两种类型的控制措施覆盖。 第一个监视集合的状态并调用三个lambda函数,第一个在将某些元素添加到集合中时调用,第二个在元素离开集合时调用,最后一个在集合的元素更改顺序时调用第三个。 第二种最常见的控件类型监视列表,并且是列表的订阅源-具有特定编号的页面。 也就是说,例如,它跟随长度为102个元素的List,并且它本身从20号到29号返回10个元素的List。 生成的事件与列表本身完全相同。
当然,遵循“为完成两次的工作创建包装器”的原则,出现了许多方便的包装器,例如,一个仅接受Factory输入,在模型类型及其视图之间建立对应关系以及到Canvas的链接的包装器。您需要添加元素。 和许多其他类似的包装,典型情况下只有大约十二个包装。
更复杂的控制
有时会出现多余的情况,无法通过模型来表达,尽管很明显。 在这里,可以对值执行某种操作的控件以及监视其他控件的控件可以帮助您。 例如,一种典型情况:一个动作有一个价格,并且只有当帐户中的钱多于其价格时该按钮才处于活动状态。
item.Get(c, FCUnitItem.COST).Join(c, Player.Get(c, MONEY)).Func(c, (cost, money) => cost <= money).SetActive(c, BuyButton);
实际上,这种情况非常典型,按照我的原则,有一个现成的包装器,但是随后我展示了它的内容。
我们拿了要购买的物品,创建了一个对象,该对象已订阅其字段之一,并且值类型为long。 他们又添加了一个控件,该控件的类型也很长,该方法返回了一个具有一对值的控件,并且当它们中的任何一个发生更改时都会触发Changed事件,然后Func为计算函数的输入中的任何更改创建一个对象,如果计算了最终值,则创建Changed事件。功能已更改。
编译器将根据输入数据类型和结果表达式的类型成功构建必要的控件类型。 在极少数情况下,lambda函数返回的类型不明显时,编译器会要求您显式地对其进行澄清。 最后,最后一个调用侦听布尔控件,具体取决于它是打开还是关闭按钮。
实际上,项目中的实际包装程序接受两个按钮进行输入,一个用于有钱的情况,而另一个在没有足够钱的情况下,打开“购买货币”模式窗口的命令也挂在第二个按钮上。 所有这一切都在同一行中。
显而易见,使用Join和Func可以构建任意复杂的结构。 在我的代码中,有一个函数会生成复杂的控件,并考虑到身边的玩家数量来计算玩家可以购买的数量,并且规则是如果所有人的总和不超过总预算,每个人都可以超出预算10%。 这是没有必要执行此操作的示例,因为调试模型中发生的事情非常简单容易,因此在响应式控件中捕获错误也同样困难。 您甚至会赶上执行,并花费大量时间来了解导致执行的原因。
因此,使用复杂控件的一般原则如下:在对表单进行原型制作时,可以在响应控件上使用结构,尤其是当您不确定将来它们会变得更加复杂时,但是一旦您怀疑如果它破坏了,您将不了解发生了什么,您应该立即将这些操作转移到模型中,并将控件中以前执行的计算放入静态规则类的扩展方法中。
这与完美主义者之间所钟爱的“立即就做”的原则大不相同,因为我们生活在游戏开发世界中,当您开始跳过某种形式时,您绝对不能确定三天内会做什么。 正如我的一位同事所说:“如果每当游戏设计师改变主意时我得到5美分,我就已经是一个非常富有的人。” 实际上,这还不错,但反之亦然。 游戏应该通过反复试验来开发,因为如果您没有进行愚蠢的克隆,那么您将无法想象玩家真正需要什么。
多个视图的一个数据源
对于如此多的原型案例,您需要单独讨论。 碰巧在不同的视图中呈现了作为接口模型一部分的元素的同一模型,这取决于发生这种情况的位置和环境。 我们使用的原则是“一种类型,一种视图”。 例如,您有一张武器购买卡,其中包含相同的简单信息,但是在不同的存储模式下,它应该由不同的预制件代表。 该解决方案包括两种情况的两个部分。
第一种是将该视图放置在两个不同的视图中时,例如,一个简短列表形式的商店和一个带有大图片的商店。 在这种情况下,需要建立两个单独的工厂来帮助进行类型-预制件的匹配。 在一个视图的ConnectModel方法中,将在另一个视图中使用一个视图。 如果您需要在一个地方显示完全相同的信息卡,则情况完全不同。 有时,在这种情况下,元素模型具有一个附加字段,用于指示特定元素的喜庆背景,有时,只是元素模型具有一个没有任何字段的继承人,只需要用另一个预制件绘制即可。 原则上,没有矛盾。
这似乎是一个显而易见的解决方案,但在这种情况下,我在奇怪的舞蹈中用铃鼓围绕着这种手鼓看到了足够多的代码,并认为有必要对此进行撰写。
特殊情况:带有大量依赖关系的控件
我想单独谈论一种非常特殊的情况。 这些是监视大量元素的控件。 例如,一个控件可以监视模型列表并汇总位于每个元素内部的字段的内容。 例如,如果列表中有一个大的上管,并用数据填充,则这样的控件可能会捕获与更改有关的事件,而该事件与元素列表中的事件一样多。 如此多次重新计算聚合函数当然不是一个好主意。 特别是对于此类情况,我们制作了一个控件,该控件订阅了从GameState伸出的onTransactionFinished事件,并且在任何模型中都可以使用指向GameState的链接。 并且随着输入的任何更改,此控件将简单地在其自身上标记原始数据已更改,并且仅当它收到有关事务结束的消息时,或者当它从输入事件流中收到消息时发现事务已经完成时,才重新计数。 。 显然,如果流处理链中有两个这样的控件,则可能无法保护此类控件免受不必要的消息的侵害。 第一个将积累一堆变更,等待事务结束,进一步启动变更流,还有另一个已经捕获了一大堆变更,接收到有关事务结束的事件(他很不幸在较早时已订阅该事件的函数列表中),计算了所有内容,然后他bam和另一个更改事件,然后第二次重述所有内容。 可能是但很少见,更重要的是,如果您的控件在一次计算中多次进行了如此怪异的计算,那么您做错了,您需要将所有这些推理操作转移到模型和规则中, ,实际上是那个地方。
UniRX Ready库
并且可以将自己局限于上述所有内容,并冷静地开始编写您的杰作,尤其是与模型和控制团队相比,这非常简单,并且如果您发明自行车的想法没有引起人们的注意,则不到一周的时间便可以完成编写,并且一切都已经经过深思熟虑,然后写在我面前,免费分发给所有人。
在发现UniRX的过程中,我们发现了一个美观且符合标准的设计,该设计可以从总体上创建线程,巧妙地将它们合并,将它们从主线程过滤到非主线程或将控制权返回给主线程,该主线程具有一堆现成的工具可以发送到不同的地方,等等。进一步。 我们那里没有两件事:调试的简便性。 您是否曾经尝试在调试器中按步骤在Linq上调试一些多层建筑物? 因此,这里的情况仍然严重得多。 同时,我们完全缺乏所有这些先进机械的用途。 为了简化调试和重现状态,我们完全缺少各种信号源,所有事情都在主流中发生,因为在元游戏中与多线程调情是完全多余的,所有命令处理的异步都隐藏在命令发送引擎内部,并且异步本身在其中占据了很大的份额。空间不大,因此需要更多地关注各种检查,自检以及记录和回放的可能性。
通常,如果您已经知道如何使用UniRX,那么我将为您提供专门用于IObservable模型的工具,您可以在需要的地方使用喜欢的库的王牌功能,但是对于其余部分,我建议不要尝试从高速汽车或仅从地面上的坦克制造坦克两者都有轮子。
在文章的最后,亲爱的读者们,我有对我来说非常重要的传统问题,关于美丽的观点以及科学技术工作的发展前景。