手机游戏的架构解决方案。 第1部分:模型

题词:
-如果您不知道该怎么办,我将如何评估?
-嗯,会有屏幕和按钮。
-Dima,您现在用三个字形容了我的一生!
(c)在游戏公司的一次集会中进行真正的对话



在我参与大约十二个大型项目的过程中,形成了一系列需求和满足这些需求的解决方案,这些项目首先是在Flash上​​,然后是在Unity上。 其中最大的项目拥有超过20万个DAU,并为我的存钱罐增加了新的原始挑战。 另一方面,证实了先前发现的相关性和必要性。

在我们严酷的现实中,至少一次在思想上设计过一个大型项目的每个人都有关于如何做的自己的想法,并且常常准备捍卫自己的想法,直到最后一滴血。 对于其他人来说,这让我微笑,而管理层通常将所有这些视为一个巨大的黑匣子,并未与任何人抗衡。 但是,如果我告诉您正确的解决方案将有助于将新功能的创建减少2-3倍,将旧功能的搜索次数减少5-10倍,并使您能够做许多以前无法访问的新的重要事情,那该怎么办? 足以让建筑融入您的心中!
手机游戏的架构解决方案。 第2部分:命令及其队列
手机游戏的架构解决方案。 第3部分:射流推力视图


型号


进入领域


大多数程序员都意识到使用MVC之类的重要性。 很少有人会使用四口之家中的纯MVC,但是普通办公室的所有决定在精神上都某种程度上类似于这种模式。 今天,我们将讨论该缩写中的第一个字母。 因为手机游戏中程序员的大部分工作是元游戏中的新功能,这些新功能是通过对模型的操纵来实现的,并将成千上万个接口拧入这些功能中。 模型的便利性在本课程中起着关键作用。

我没有提供完整的代码,因为它有点像张纸,而且通常与他无关。 我将通过一个简单的示例来说明我的推理:

public class PlayerModel { public int money; public InventoryModel inventory; /* Using */ public void SomeTestChanges() { money = 10; inventory.capacity++; } } 

此选项根本不适合我们,因为该模型不会发出有关其中发生的更改的事件。 如果有关哪些字段受更改影响,哪些字段不受影响,哪些字段需要重绘,哪些字段不受影响的信息,则程序员将以一种或另一种形式手动指定-这将成为错误和花费时间的主要来源。 在我工作过的大多数大型办公室中,程序员自己发送了各种InventoryUpdatedEvent,在某些情况下还手动填充了它们。 您认为,其中一些办事处还是赚了上百万?

我们将使用我们自己的类ReactiveProperty <T>,该类将隐藏所有用于发送所需消息的操作。 它看起来像这样:

 public class PlayerModel : Model { public ReactiveProperty<int> money = new ReactiveProperty<int>(); public ReactiveProperty<InventoryModel> inventory = new ReactiveProperty<InventoryModel>(); /* Using */ public void SomeTestChanges() { money.Value = 10; inventory.Value.capacity.Value++; } public void Subscription(Text text) { money.SubscribeWithState(text, (x, t) => t.text = x.ToString()); } } 

这是模型的第一个版本。 对于许多程序员来说,此选项已经是一个梦想,但我仍然不喜欢它。 我不喜欢的第一件事是访问值很复杂。 在编写此示例时,我感到困惑,在一个地方忘记了Value,而正是这些数据操作构成了模型所完成和混淆的所有事情的绝大部分。 如果您使用的是4.x语言版本,则可以执行以下操作:

 public ReactiveProperty<int> money { get; private set; } = new ReactiveProperty<int>(); 

但这并不能解决所有问题。 我只想简单地写:inventory.capacity ++;。 假设我们尝试获取每个模型字段; 设置 但是为了订阅事件,我们还需要访问ReactiveProperty本身。 明显的不便和混乱的根源。 尽管事实上我们只需要指出我们要监视的字段。 在这里,我想出了一个我喜欢的棘手动作。

让我们看看您是否喜欢它。

不是将ReactiveProperty插入程序员正在处理的具体模型中,而是插入其静态描述符PValue(更通用的Property的继承者)来标识字段,并且在Model构造函数的内部隐藏了所需类型的ReactiveProperty的创建和存储。 不是最好的名字,但是它发生了,然后被更名。

在代码中,它看起来像这样:

 public class PlayerModel : Model { public static PValue<int> MONEY = new PValue<int>(); public int money { get { return MONEY.Get(this); } set { MONEY.Set(this, value) } } public static PModel<InventoryModel> INVENTORY = new PModel<InventoryModel>(); public InventoryModel inventory { get { return INVENTORY.Get(this); } set { INVENTORY.Set(this, value) } } /* Using */ public void SomeTestChanges() { money = 10; inventory.capacity++; } public void Subscription(Text text) { this.Get(MONEY).SubscribeWithState(text, (x, t) => t.text = x.ToString()); } } 

这是第二种选择。 当然,该模型的一般祖先很复杂,但要根据其描述符创建和提取一个真实的ReactiveProperty,但这很快速,而且不需要反射,或者在类初始化阶段仅应用反射一次即可。 这是引擎的创建者完成的工作,然后将被所有人使用。 另外,这种设计避免了意外地操纵ReactiveProperty本身而不是操纵其中存储的值的尝试。 字段的创建很杂乱,但在所有情况下都完全相同,可以使用模板创建。

本文结尾处有一个民意调查,您最喜欢哪个选项。
下述所有内容均可在两个版本中实现。

交易次数


我希望程序员只有在引擎采用的限制(即在团队内部)允许的情况下才能够更改模型字段,并且以后再也不能更改。 为此,设置器必须走到某个地方,检查transaction命令当前是否打开,然后才允许在模型中编辑信息。 这是非常必要的,因为引擎用户经常尝试做一些奇怪的事情以绕过典型的过程,从而破坏引擎的逻辑并引起细微的错误。 我看到了不止一次或两次。

人们认为,如果您创建一个单独的接口来从模型中读取数据和进行编写,则将以某种方式提供帮助。 实际上,该模型被其他文件和繁琐的其他操作所淹没。 这些限制是最终的,首先,程序员被迫了解并不断思考:“每个特定的功能,模型或其接口应该提供什么”,其次,当必须规避这些限制时,也会出现这种情况。在出口处,我们有d'Artagnan,他的想法是白色的,他的引擎的许多用户是项目经理的后卫,尽管经常被滥用,但没有达到预期的效果。 因此,我宁愿紧紧阻止这种错误的可能性。 可以减少惯例的使用量。

ReactiveProperty设置器应具有指向检查事务当前状态的位置的链接。 假设这个地方是classCModelRoot。 最简单的选择是将其显式传递给模型构造函数。 调用RProperty时,该代码的第二个版本会显式接收到此链接,并可以从那里获取所有必要的信息。 对于代码的第一个版本,您将必须在构造函数中的ReactiveProperty类型的字段中进行反射,并为它们提供一个链接以进行进一步的操作。 不便之处是需要在每个模型中创建一个带有参数的显式构造函数,如下所示:

 public class PlayerModel : Model { public PlayerModel(ModelRoot gamestate) : base (gamestate) {} } 

但是对于模型的其他功能,模型具有指向父模型的链接,形成双向连接的构造非常有用。 在我们的示例中,这将是player.inventory.Parent == player。 然后可以避免使用此构造函数。 任何模型都将能够从父级那里获取并缓存到一个神奇的地方的链接,并从他的父辈那里得到一个链接,依此类推,直到下一个父辈变成那个神奇的地方。 结果,在声明级别,所有这些看起来像这样:

 public class ModelRoot : Model { public bool locked { get; private set; } } public partial class Model { public Model Parent { get; protected set; } public ModelRoot Root { get; } } 

当模型进入游戏状态树时,所有这些美丽都会自动填充。 是的,尚未创建的新模型尚无法了解交易并自行阻止操作,但是如果交易状态被禁止,此后将无法进入该状态,将来的父代的设置者将不允许它,因此游戏状态的完整性不会受到影响。 是的,这将需要在对引擎进行编程的阶段进行额外的工作,但是,另一方面,使用引擎的程序员将完全不需要了解和思考它,直到他尝试做错事并得到帮助为止。

由于有关事务性的对话已经开始,因此关于更改的消息不应在更改后立即处理,而应在当前命令中对模型的所有操作完成后再进行处理。 造成这种情况的原因有两个,第一是数据一致性。并非所有数据状态在内部都是一致的,也许您无法尝试呈现它。 或者,例如,如果您不耐烦,可以对数组进行排序或在循环中更改某些模型变量。 您不应收到数百条更改消息。

有两种方法可以做到这一点。 第一种是订阅对变量的更新,并使用棘手的函数,该函数将事务结束流添加到变量的更改流中,然后才跳过消息。 例如,如果使用UniRX,这很容易做到。 但是这种选择有很多缺点,特别是它引起了很多不必要的动作。 就个人而言,我喜欢其他选择。

每个ReactiveProperty都会在事务开始之前记住其状态和当前状态。 仅在交易结束时才会发出有关更改和更改确定的消息。 在更改的对象是某种集合的情况下,这将允许显式地包括有关已发送消息中发生的更改的信息,例如,在列表中添加了这两个元素,并删除了这两个元素。 不仅仅是说某些事情已经改变了,而且迫使接收者分析一千个元素的列表以寻找需要重绘的信息。

 public partial class Model { public void DispatchChanges(Command transaction); public void FixChanges(); public void RevertChanges(); } 

该选项在创建引擎的阶段比较耗时,但是使用成本较低。 最重要的是,它为下一步改进打开了可能性。

有关模型更改的信息


我希望从模型中得到更多。 在任何时候,我都想轻松方便地查看由于我的行为导致的模型状态发生了什么变化。 例如,以这种形式:

 {"player":{"money":10, "inventory":{"capacity":11}}} 

通常,对于程序员来说,查看命令开始之前和结束之后或命令内部某个时刻之间的模型状态之间的差异很有用。 一些为此在团队开始之前克隆整个游戏状态,然后进行比较。 这在调试阶段部分解决了问题,但是绝对不可能在产品中运行它。 这种状态克隆,即计算两个列表之间的微不足道的差异,对于任何打喷嚏来说都是一项极其昂贵的操作。

因此,ReactiveProperty必须不仅存储其当前状态,而且还必须存储前一个状态。 这带来了一整套极为有用的机会。 首先,在这种情况下,差异的提取速度很快,我们可以从容地将其全部倒入食品中。 其次,您可以获得的不是一个比较大的差异,而是来自变化的紧凑哈希,并将其与另一个相同游戏状态中的变化哈希进行比较。 如果不同意,那就有问题。 第三,如果命令的执行随执行而下降,那么您始终可以取消更改,并在事务开始时找出未损坏的状态。 与适用于该州的团队一起,此信息非常宝贵,因为您可以轻松准确地重现该情况。 当然,为此,您需要具有现成的功能以方便游戏状态的序列化和反序列化,但是无论如何您都将需要它。

模型变更的序列化


该引擎在json中提供序列化和二进制代码-这绝非偶然。 当然,二进制序列化占用的空间少得多,而且运行速度快得多,这一点很重要,尤其是在初始引导过程中。 但这不是人类可读的格式,在此我们为调试的方便而祈祷。 此外,还有另一个陷阱。 当您的游戏进入正式版时,您将需要不断地从一个版本切换到另一个版本。 如果您的程序员遵循一些简单的预防措施,并且没有不必要地从游戏状态中删除任何内容,您将不会感到这种过渡。 但是在二进制格式中,出于明显的原因没有字符串字段名称,如果版本不匹配,则必须读取状态为旧版本的二进制文件,将其导出到更有用的内容(例如,相同的json),然后将其导入新的状态,并将其导出到二进制文件,写下来,直到所有这些工作像往常一样进行。 结果,在某些项目中,考虑到它们的独眼巨人大小,将配置写入二进制文件,并且他们已经更喜欢以json的形式来回拖动状态。 评估开销并选择您。

 [Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2, //    ,    } /**    */ public partial class Model { public bool GetHashCode(ExportMode mode, out int code); public bool Import(BinaryReader binarySerialization); public bool Import(JSONReader json); public void ExportAll(ExportMode mode, BinaryWriter binarySerialization); public void ExportAll(ExportMode mode, JSONWriter json); public bool Export(ExportMode mode, out Dictionary<string, object> data); } 

Export方法(ExportMode模式,出字典<string,object>数据)的签名有些令人担忧。 事情是这样的:当序列化整个树时,您可以直接写入流,或者在我们的情况下,写入JSONWriter,这是StringWriter的简单附加组件。 但是,当您导出更改时,它并不是那么简单,因为当您深入一棵树并进入其中一个分支时,您仍然不知道是否从中导出任何内容。 因此,在此阶段,我提出了两种解决方案,一种更简单,第二种更复杂且经济。 一个更简单的方法是,当仅导出更改时,可以将所有更改从Dictionary <string,object>和List <object>变成树。 然后发生了什么,请输入您最喜欢的序列化程序。 这是一种简单的方法,不需要铃鼓跳舞。 但是它的缺点是,在将更改导出到堆的过程中,将为一次性集合分配一个位置。 实际上,空间不大,因为此完整的导出会生成一棵大树,并且典型命令在树中几乎没有留下任何变化。

但是,许多人认为,在没有极端需要的情况下,不需要为巨魔喂食垃圾收集器。 为了让他们安心,我准备了一个更复杂的解决方案:

 /**    */ public partial class Model { public void ExportAll(ExportMode mode, Type propertyType, JSONWriter writer, bool newModel = false); public bool DetectChanges(ExportMode mode, Stack<Model> ierarchyChanged = null); public void ExportChanges(ExportMode mode, Type propertyType, JSONWriter writer, Queue<Model> ierarchyChanges = null); } 

这种方法的本质是在树上走两次。 第一次,查看所有已更改的模型或子模型中的更改,并将所有模型完全按照它们在树中当前状态的显示顺序写入Queue <Model> ierarchyChanges中。 更改不多,队列不会很长。 另外,没有什么可以阻止在调用之间保留Stack <Model>和Queue <Model>,然后在调用期间几乎没有分配。

并且已经第二次经过树了,那么有可能每次都查看队列的顶部,并了解是否有必要进入树的这一分支或立即继续前进。 这允许JSONWriter立即编写而不会返回任何其他中间结果。

很有可能实际上不需要这种复杂性,因为稍后您会看到将更改导出到树中,仅在调试时或使用Exception崩溃时才需要。 在正常操作期间,所有内容都限于GetHashCode(ExportMode模式,输出为int代码),所有这些乐趣与之完全无关。

在继续使我们的模型复杂化之前,让我们先讨论一下。

为什么这么重要


所有程序员都说这非常重要,但通常没人相信他们。 怎么了

首先,因为所有程序员都说您需要扔掉旧代码并编写新代码。 仅此而已,无论资格如何。 没有管理方法可以确定这是否正确,并且实验通常过于昂贵。 经理将被迫选择一名程序员并相信他的判断。 问题在于,这样的顾问通常是管理层与之长期合作的顾问,并根据其是否能够实现其想法对其进行评估,而其所有最佳想法已经体现在现实中。 因此,这也不是找出其他人的想法和异类想法的理想方法。

其次,所有手机游戏中有80%的一生收入不到500美元。 因此,在项目开始时,管理还有其他问题,更重要的是体系结构。 但是,在项目一开始就做出的决定将人质作为人质,并且从六个月延长到三年。 在已经有客户的已经工作的项目中,重构和转换为其他想法的过程是非常困难,昂贵且有风险的业务。 如果对于一开始的项目而言,在正常的架构上投资三个工时似乎是不可接受的奢侈,那么对于将新功能推迟几个月的更新成本,您能怎么说呢?

第三,即使“应该怎样”的想法本身是好的和理想的,也不清楚其实施需要多长时间。 时间的长短取决于程序员的技巧,这是非线性的。 领军者将完成一项简单的任务,其速度不会比初级者快得多。 可能是一年半。 但是,每个程序员都有自己的“复杂性极限”,超过这个极限,其有效性就会大大下降。 在我的生活中,有一个案例需要解决一个相当复杂的建筑任务,甚至完全集中精力关掉家里的互联网并订购一个月的便餐也无济于事,但两年后,在阅读了有趣的书并解决了相关任务之后,我在三天内解决了这个问题。 我相信每个人都会在职业生涯中记住类似的事情。 而这里是要抓住的地方! 事实是,如果您想到的是一个巧妙的想法,那么这个新想法很可能就在您个人对复杂性的极限上,甚至可能落后于它。 管理层一再将其烧毁,然后开始抨击任何新想法。 如果您自己制作游戏,结果可能会更糟,因为不会有人阻止您。

但是,那么,有谁能设法使用好的解决方案呢? 有几种方法。

首先,每个公司都想聘请一个已经和以前的雇主一起做过的现成的人。 这是将实验负担转移给其他人的最常见方法。

其次,做出第一个成功游戏的公司或个人,s之以鼻,并开始下一个项目,已经为改变做好了准备。

第三,诚实地向自己承认,有时候您做某件事不是为了薪水,而是为了过程的乐趣。最主要的是为此花时间。

第四,这是一套经过验证的解决方案和图书馆,以及与人一起构成游戏公司主要资金的图书馆,当一些关键人物辞职并移居澳大利亚时,这将是唯一存在的东西。

最后,尽管不是最显而易见的原因:因为它非常有益。好的解决方案可以减少编写新功能,调试新功能和捕获错误的时间。让我举个例子:两天前,客户执行了一项新功能,这种执行的可能性是千分之一,也就是说,质量检查人员会很酷地重现它,如果提供的话,则每天有200条错误消息。在一切崩溃之前,您需要多少时间重现这种情况并在断点处抓住客户,直到发生崩溃?例如,我有10分钟。

型号


模型树


该模型包含许多对象。不同的程序员决定如何将它们连接在一起。第一种方法是通过模型所在的位置来识别模型。当模型的引用在ModelRoot中属于一个位置时,这非常方便且简单。也许它甚至可以从一个地方转移到另一个地方,但是来自不同地方的两个联系永远不会导致它。我们将通过引入新版本的ModelProperty描述符来实现此目的,该描述符将处理从一个模型到位于其中的其他模型的链接。在代码中,它将如下所示:

 public class PModel<T> : Property<T> where T:Model {} public partial class PlayerModel : Model { public PModel<InventoryModel> INVENTORY = new PModel<InventoryModel>(); public InventoryModel inventory { get { return INVENTORY.Value(this); } set { INVENTORY.Value(this, value); } } } 

有什么区别?将新模型添加到该字段时,将添加了新模型的模型写入其“父”字段,删除后,将重置“父”字段。从理论上讲,一切都很好,但是有很多陷阱。第一个-会使用它的程序员可能会误解。为了避免这种情况,我们从不同的角度对此过程进行了隐藏检查:

  1. 我们将修复PValue,以便它检查其值的类型,并在尝试在其中存储对模型的引用时由专家宣誓,这表明有必要为此使用其他结构,以免混淆。当然,这是运行时检查,但它会在第一次尝试启动时就发誓,因此可以。
  2. PModel Parent - , . . , .

这样做会产生副作用,如果您需要将这样的模型从一个地方转移到另一个地方,则必须首先从第一个地方删除它,然后再将其添加到第二个地方-否则支票会打扰您。但这实际上很少发生。

由于模型位于一个严格定义的位置,并且具有对其父代的引用,因此我们可以向其添加新方法-它可以确定模型在ModelRoot树中的位置。这对于调试非常方便,但是也需要它,以便可以对其进行唯一标识。例如,在另一个相同的游戏状态中找到另一个完全相同的模型,或者在传输到服务器的命令中指示该命令所包含的模型的链接。看起来像这样:

 public class ModelPath { public Property[] properties; public Object[] indexes; public override ToString(); public static ModelPath FromString(string path); } public partial class Model { public ModelPath Path(); } public partial class ModelRoot : Model { public Model GetByPath(ModelPath path); } 

为何实际上不可能将一个对象植根于一个位置并从另一个位置引用它呢?并且因为您想象您正在从JSON反序列化对象,所以在这里您将找到指向一个植根于完全不同位置的对象的链接。而且仍然没有地方,只能通过反序列化来创建它。哎呀请不要提供任何多遍反序列化。这是此方法的局限性。因此,我们将提出第二种方法:

通过第二种方法创建的所有模型都在一个魔术位置创建,并且在游戏状态的所有其他位置仅将链接插入到它们。反序列化期间,如果有多个对对象的引用,则在您首次访问魔术位置时,将创建该对象,并返回对该对象的所有后续引用。为了实现其他功能,我们假定游戏可以具有多个游戏状态,因此魔术场所不应是一个常见的游戏场所,而应位于例如游戏状态中。对于此类模型的引用,我们使用PPersistent描述符的另一个变体。持久性:模型将使模型本身更加特殊。在代码中,它将如下所示:

 public class Persistent : Model { public int id { get { return ID.Get(this); } set { ID.Set(this, value); } } public static RProperty<int> ID = new RProperty<int>(); } public partial class ModelRoot : Model { public int nextFreePersistentId { get { return NEXT_FREE_PERSISTENT_ID.Get(this); } set { NEXT_FREE_PERSISTENT_ID.Set(this, value); } } public static RProperty<int> NEXT_FREE_PERSISTENT_ID = new RProperty<int>(); public static PDictionaryModel<int, Persistent> PERSISTENT = new PDictionaryModel<int, Persistent>() { notServerVerified = true }; /// <summary>      Id-. </summary> public PersistentT Persistent<PersistentT>(int localId) where PersistentT : Persistent, new(); /// <summary> C    Id. </summary> public PersistentT Persistent<PersistentT>() where PersistentT : Persistent, new(); } 

有点麻烦,但是可以使用。持久地说,Persistent可以使用ModelRoot参数来固定构造函数,如果他们尝试不通过此ModelRoot方法创建此模型,则会发出警报。

我的代码中同时有两个选项,问题是,如果第二个选项完全涵盖所有可能的情况,那么为什么要使用第一个选项呢?

答案是,游戏的状态首先应该是人们可读的。如果可能,使用第一个选项会是什么样?

 { "persistents":{}, "player":{ "money":10, "inventory":{"capacity":11} } } 

现在,如果仅使用第二个选项,将会是什么样子:
 { "persistents":{ "1":{"money":10, "inventory":2}, "2":{"capacity":11} }, "player":1 } 

要亲自调试,我更喜欢第一种选择。

访问模型属性


最终,隐藏在模型的引擎盖之下的是对于属性的反应式存储设施的访问权。如何使它工作以使其快速工作并不太明显,最终模型中没有太多的代码,也没有太多的反思。让我们仔细看看。

了解字典的第一件事是,无论字典的大小如何,从字典中读取不需要花费太多的固定时间。我们将在Model中创建一个私有静态字典,在其中为每种类型的模型分配一个描述,其中包含其中的字段,并且在构建模型时将对其进行一次访问。在类型构造函数中,我们查看是否有对我们类型的描述,如果没有,我们创建它,如果是,我们将完成。因此,对于每个类仅创建一次描述。在创建描述时,我们在每个静态属性(字段描述)中放入通过反射提取的数据-字段名称,以及该字段的数据存储在数组中的索引。这样当通过字段描述访问时,将以先前已知的索引(即快速)将其存储从数组中取出。

在代码中,它将如下所示:

 public class Model : IModelInternals { #region Properties protected static Dictionary<Type, Property[]> propertiesDictionary = new Dictionary<Type, Property[]>(); protected static Dictionary<Type, Property[]> propertiesForBinarySerializationDictionary = new Dictionary<Type, Property[]>(); protected Property[] _properties, _propertiesForBinarySerialization; protected BaseStorage[] _storages; public Model() { Type targetType = GetType(); if (!propertiesDictionary.ContainsKey(targetType)) RegisterModelsProperties(targetType, new List<Property>(), new List<Property>()); _properties = propertiesDictionary[targetType]; _storages = new BaseStorage[_properties.Length]; for (var i = 0; i < _storages.Length; i++) _storages[i] = _properties[i].CreateStorage(); } private void RegisterModelsProperties(Type target, List<Property> registered, List<Property> registeredForBinary) { if (!propertiesDictionary.ContainsKey(target)) { if (target.BaseType != typeof(Model) && typeof(Model).IsAssignableFrom(target.BaseType)) RegisterModelsProperties(target.BaseType, registered, registeredForBinary); var fields = target.GetFields(BindingFlags.Public | BindingFlags.Static); // | BindingFlags.DeclaredOnly List<Property> alphabeticSorted = new List<Property>(); for (int i = 0; i < fields.Length; i++) { var field = fields[i]; if (typeof(Property).IsAssignableFrom(field.FieldType)) { var prop = field.GetValue(this) as Property; prop.Name = field.Name; prop.Parent = target; prop.storageIndex = registered.Count; registered.Add(prop); alphabeticSorted.Add(prop); } } alphabeticSorted.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); registeredForBinary.AddRange(alphabeticSorted); Property[] properties = new Property[registered.Count]; for (int i = 0; i < registered.Count; i++) properties[i] = registered[i]; propertiesDictionary.Add(target, properties); properties = new Property[registered.Count]; for (int i = 0; i < registeredForBinary.Count; i++) properties[i] = registeredForBinary[i]; propertiesForBinarySerializationDictionary.Add(target, properties); } else { registered.AddRange(propertiesDictionary[target]); registeredForBinary.AddRange(propertiesForBinarySerializationDictionary[target]); } } CastType IModelInternals.GetStorage<CastType>(Property property) { try { return (CastType)_storages[property.storageIndex]; } catch { UnityEngine.Debug.LogError(string.Format("{0}.GetStorage<{1}>({2})",GetType().Name, typeof(CastType).Name, property.ToString())); return null; } } #endregion } 

设计有点简单,因为在此模型的祖先中声明的静态属性描述符可能已经注册了存储索引,并且不能保证从Type.GetFields()返回属性的顺序。时代,您需要监控自己。

集合属性


在模型树的这一节中,可以注意到以前没有提到的构造:PDictionaryModel <int,Persistent>-包含集合的字段的描述符。显然,我们将必须为集合创建自己的存储库,该存储库存储有关在事务开始之前该集合的外观以及现在的外观的信息。这里的水下卵石是彼得一世统治下的雷石的大小。它的事实是,手头上有两个长长的词典,计算它们之间的差异是一项非常昂贵的工作。我认为此类模型应该用于与meta相关的所有任务,这意味着它们应该可以快速运行。我制作了一个棘手的钩子,而不是存储两个状态,克隆它们,然后进行昂贵的比较,而是制作了一个棘手的钩子-仅将字典的当前状态存储在存储中,另外两个字典是已删除的值,和替换元素的旧值。最后,存储添加到字典的一组新关键字。可以轻松,快速地填写此信息。很容易使用它生成所有必需的差异,并且必要时可以恢复以前的状态。在代码中,它看起来像这样:

 public class DictionaryStorage<TKey, TValues> : BaseStorage { public Dictionary<TKey, TValues> current = new Dictionary<TKey, TValues>(); public Dictionary<TKey, TValues> removed = new Dictionary<TKey, TValues>(); public Dictionary<TKey, TValues> changedValues = new Dictionary<TKey, TValues>(); public HashSet<TKey> newKeys = new HashSet<TKey>(); } 

我没有为清单创建一个同样漂亮的存储库,或者我没有足够的时间保存两个副本。需要额外的附件以尝试最小化差异的大小。

 public class ListStorage<TValue> : BaseStorage { public List<TValue> current = new List<TValue>(); public List<TValue> previouse = new List<TValue>(); //        public List<int> order = new List<int>(); //       . } 

合计


如果您清楚地知道要接收什么以及如何接收,则可以在几周内写下所有内容。同时,游戏的开发速度发生了巨大变化,以至于在尝试时,如果没有良好的引擎,我什至没有开始自己的游戏制作游戏。仅仅因为在第一个月对我的投资显然就得到了回报。当然,这仅适用于meta。游戏玩法必须以老式方式完成。

在本文的下一部分中,我将讨论命令,网络和预测服务器响应。我还有一些对您来说很重要的问题。如果您的答案与括号中的答案不同,我会很乐意在评论中阅读它们,甚至您也可以写一篇文章。预先感谢您的回答。

PS请在PM中提供有关众多语法错误的合作建议和说明。

Source: https://habr.com/ru/post/zh-CN434582/


All Articles