如何最好地构造/管理数百个“游戏中”角色


10

我一直在制作一个简单的RTS游戏,其中包含数百个角色,例如Unity中的Crusader Kings 2。要存储它们,最简单的选择是使用可编写脚本的对象,但这不是一个好的解决方案,因为您无法在运行时创建新的对象。

因此,我创建了一个名为“ Character”的C#类,其中包含所有数据。一切正常,但随着游戏的模拟,它不断创建新角色并杀死一些角色(随着游戏中事件的发生)。随着游戏不断进行模拟,它会创建1000个角色。我添加了一个简单的检查,以确保在处理字符时该字符处于“活动”状态。因此,这有助于提高性能,但是如果“角色”已死,我将无法删除它,因为在创建家谱时我需要他/她的信息。

列表是保存游戏数据的最佳方法吗?否则,一旦创建了10000个字符,就会出现问题?一种可行的解决方案是在列表达到一定数量后再制作一个列表,然后将其中所有无效字符移动。


9
过去,当角色消亡后需要角色数据的子集时,我所做的一件事就是为该角色创建一个“墓碑”对象。墓碑可以承载我以后需要查找的信息,但是它可以更小且迭代次数更少,因为它不需要像活着的角色一样不断进行模拟。
DMGregory


2
是像CK2这样的游戏,还是仅仅是拥有大量角色的一部分?我认为整个游戏就像CK2。在这种情况下,这里的许多答案都是不正确的,并且包含良好的专业知识,但它们却遗漏了问题的重点。当CK2 实际上是一个大型策略游戏时,您称它为实时策略游戏并没有帮助。这看似挑剔,但这与您面临的问题非常相关。
拉斐尔·施米兹

1
例如,当您提到“ 1000个字符”时,人们会同时想到屏幕上的 1000个3D模型或子画面 -因此,在Unity中是1000个GameObjects。在CK2中,我同时看到的最大字符数是当我在球场上看到10到15个人时(尽管我玩得并不远)。同样,一支拥有3000名士兵的军队只是一支GameObject,显示的数字为“ 3000”。
拉斐尔·施米兹

1
@ R.Schmitz是的,我应该把每个角色都没有附加游戏对象的部分弄清楚。只要有必要,就可以将角色从一个点移动到另一点。创建一个单独的实体,其中包含具有Ai逻辑的该角色的所有信息。
paul p

Answers:


24

您应该考虑三件事:

  1. 实际上会导致性能问题吗?好吧,实际上不是1000。现代计算机的速度非常快,可以处理很多东西。监视字符处理需要花费多少时间,并在担心太多之前查看它是否真的会引起问题。

  2. 当前最少活跃字符的保真度。初学者游戏程序员经常犯的一个错误是,痴迷于精确更新屏幕外角色的方式与屏幕上角色相同。这是一个错误,没人在乎。相反,您需要设法营造一种印象,即屏幕外的角色仍在起作用。通过减少屏幕外接收的更新字符的数量,您可以大大减少处理时间。

  3. 考虑面向数据的设计。不是拥有1000个字符对象并为每个对象调用相同的函数,而是为1000个字符提供数据数组,并在1000个字符上循环一个函数,依次更新每个字符。这种优化可以大大提高性能。


3
实体/组件/系统对此非常有效。根据需要构建每个“系统”,让它保留成千上万个字符(它们的组件),并为系统提供“字符ID”。这使您可以将不同的数据模型分开且保持较小,并且还可以从不需要它们的系统中删除无效字符。(如果您目前不在使用系统,则也可以完全卸载它。)
Der Kommissar

1
通过减少屏幕外接收的更新字符的数量,您可以大大增加处理时间。你不是说减少吗?
Tejas Kale

1
@TejasKale:是的,已更正。
杰克·艾德利

2
一千个字符并不多,但是当每个字符不断检查它们是否可以
cast割

1
总是最好进行实际检查,但这通常是一个安全
可行的

11

在这种情况下,我建议使用Composition

类应通过其组成实现多态行为和代码重用的原理(通过包含实现所需功能的其他类的实例)


在这种情况下,听起来您的Character班级已变得神似,并包含了角色在其生命周期的各个阶段如何运作的所有详细信息。

例如,您注意到仍然需要使用死字符-因为它们在家谱中使用。但是,仅将它们显示在家谱中,仍然不太可能需要您活着的角色的所有信息和功能。例如,他们可能只需要姓名,出生日期和肖像图标。


解决方案是将您的各个部分拆分CharacterCharacter拥有其实例的子类。例如:

  • CharacterInfo 可能是一个简单的数据结构,其中包含名称,出生日期,死亡日期和派别,

  • Equipment可能拥有您角色拥有的所有物品或它们的现有资产。它还可能具有在功能上管理这些功能的逻辑。

  • CharacterAICharacterController可能具有有关角色的当前目标,其效用功能等所需的所有信息。它也可能具有协调其各个部分之间的决策/交互的实际更新逻辑。

拆分完字符后,您不再需要在更新循环中检查“活动/停止”标志。

相反,您只需制作一个AliveCharacterObject带有CharacterControllerCharacterEquipmentCharacterInfo脚本的附件。要“杀死”角色,您只需删除不再相关的部分(例如CharacterController),现在就不会浪费内存或处理时间。

请注意,CharacterInfo族谱树实际所需的唯一数据可能是多少。通过将类分解为更小的功能-您可以在死后更轻松地保留该小数据对象,而无需保留整个AI驱动的角色。


值得一提的是,这种范例是Unity所使用的一种范例-这就是为什么它使用许多单独的脚本来处理事情的原因。在Unity中,构建大型上帝对象很少是处理数据的最佳方法。


8

当您要处理大量数据并且并非每个数据点都由一个实际的游戏对象表示时,那么放弃特定于Unity的类并只使用普通的旧C#对象通常不是一个坏主意。这样,您可以最大程度地减少开销。因此,您似乎在这里走对了。

将所有活动或死亡字符存储在一个List(或array)中可能很有用,因为该列表中的索引可以用作规范的字符ID。通过索引访问列表位置是非常快速的操作。但是保留所有活动字符的ID的单独列表可能很有用,因为与死字符相比,您可能需要更频繁地迭代这些ID。

随着游戏机制的实现不断进步,您可能还想看看您执行得最多的其他类型的搜索。例如“特定位置上的所有活着的角色”或“特定角色上的所有活着或死去的祖先”。创建一些针对此类查询而优化的辅助数据结构可能是有益的。请记住,它们每个都必须保持最新。这需要额外的编程,并且会成为其他错误的来源。因此,只有在您期望性能显着提高时才这样做。

CKII 认为字符对于节省资源不重要时,会从其数据库中“ 修剪 ”字符。如果您的一堆死角字符在长时间运行的游戏中消耗了太多资源,那么您可能想要做类似的事情(我不想将其称为“垃圾收集”。也许是“尊敬的增量者”?)。

如果您实际上对游戏中的每个角色都有一个游戏对象,那么新的Unity ECS和Jobs系统可能对您有用。它经过优化,以一种高性能的方式处理大量非常相似的游戏对象。但这会迫使您的软件体系结构陷入一些非常僵化的模式。

顺便说一句,我真的很喜欢CKII及其模拟具有成千上万由AI控制的独特角色的世界的方式,因此我很期待您能尝试这种类型的游戏。


您好,感谢您的回覆。所有计算均由一个GameObject Manager进行。我只是在必要时才将游戏对象分配给单个演员(例如显示角色军团从一个位置移动到另一个位置)。
paul p

1
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.根据我对CK2模式的经验,这与CK2处理数据的方式很接近。CK2似乎使用了本质上是基本数据库索引的索引,这使查找特定情况下的字符更快。它没有一个字符列表,而是具有一个内部字符数据库,具有所有缺点和好处。
Morfildur

1

当只有几个靠近玩家时,您不需要模拟/更新数千个字符。您只需要更新播放器在当前时间点实际可以看到的内容,因此应该暂停距离播放器较远的角色,直到播放器离它们更近为止。

如果由于您的游戏机制要求遥远的角色来显示时间而不能解决问题,则可以在玩家靠近时以一次“大”更新来更新它们。如果您的游戏机制要求每个角色在发生事件时实际对游戏中的事件做出响应,无论角色与玩家或事件之间的关系如何,那么这都可能会降低与玩家距离较远的角色出现的频率已更新(即它们仍与游戏的其余部分保持同步更新,但更新频率不高,因此在遥远的角色响应事件之前会稍有延迟,但这不太可能引起问题,甚至不会被玩家注意到)。另外,您可能想使用混合方法,


这是RTS。我们应该假设在任何给定的时间,实际上有相当数量的单位在屏幕上。
汤姆(Tom),

在RTS中,玩家需要在不注视的情况下继续游戏。一次大的更新将花费同样的时间,但是当您移动相机时会突然爆发。
PStag

1

澄清问题

我一直在制作一个简单的RTS游戏,其中包含数百个角色,例如Unity中的Crusader Kings 2。

在这个答案中,我假设整个游戏应该像CK2一样,而不是仅仅包含很多角色。您在CK2中在屏幕上看到的所有内容都很容易实现,不会危及您的性能,也不会在Unity中实现复杂。它背后的数据变得很复杂。

无功能Character类别

因此,我创建了一个名为“ Character”的C#类,其中包含所有数据。

好,因为在你的游戏中的角色单纯的数据。您在屏幕上看到的只是这些数据的表示。这些Character类是游戏的核心,因此有成为“ 上帝对象 ”的危险。因此,我建议采取以下极端措施:从这些类中删除所有功能。一种GetFullName()将名字和姓氏结合起来的方法,可以,但是没有实际“做某事”的代码。把代码放到专用类,做一个动作; 例如Birther,带有方法的Character CreateCharacter(Character father, Character mother)类比在Character类中具有该功能要干净得多。

不要将数据存储在代码中

要存储它们,最简单的选择是使用可编写脚本的对象

否。使用Unity的JsonUtility以JSON格式存储它们。对于那些没有功能的Character类,这应该是微不足道的。这将适用于游戏的初始设置以及将其存储在savegames中。但是,这仍然是一件无聊的事情,因此,我只是根据您的情况给出了最简单的选择。您也可以使用XML或YAML或其他任何格式,只要将其存储在文本文件中仍可以被人类读取。CK2可以做到,实际上大多数游戏都可以。这也是允许人们修改您的游戏的绝佳设置,但这是以后的考虑。

认为抽象

我添加了一个简单的检查,以确保处理过程中角色为“活着”,但是如果他死了,则无法删除“角色”,因为在创建家谱时我需要他的信息。

说起来容易做起来难,因为它常常与自然思维方式相冲突。您正在以“自然”方式思考“字符”。但是,就您的游戏而言,似乎至少有2种不同类型的数据是“角色”:我将其称为ActingCharacterFamilyTreeEntry。死字符FamilyTreeEntry不需要更新,可能需要的数据比活动字符少得多ActingCharacter


0

我将基于一些经验,从严格的OO设计到实体组件系统(ECS)设计进行演讲。

前一阵子我就像你一样,我有一堆具有相似属性的不同类型的东西,我建立了各种对象并尝试使用继承来解决它。一个非常聪明的人告诉我要这样做,而应使用Entity-Component-System。

现在,ECS是一个很大的概念,很难做到正确。要做很多工作,正确地构建实体,组件和系统。但是,在执行此操作之前,我们需要定义术语。

  1. 实体:这是事物,玩家,动物,NPC,无论如何。这是需要附加组件的东西。
  2. 组件:这是属性属性,例如您的情况下的“名称”或“年龄”或“父母”。
  3. 系统:这是组件或行为背后的逻辑。通常,您为每个组件构建一个系统,但这并不总是可能的。此外,有时系统需要影响其他系统。

所以,这就是我要去的地方:

首先,ID为您的角色创建一个。An intGuid无论您喜欢什么。这是“实体”。

其次,开始考虑您正在进行的不同行为。像“家谱”之类的东西-这是一种行为。建立一个包含所有信息的系统,而不是将其建模为实体上的属性。然后,系统可以决定如何处理。

同样,我们要为“角色是活着还是死了?”构建一个系统。这是设计中最重要的系统之一,因为它会影响所有其他系统。一些系统可以删除“死”字符(例如“ sprite”系统),其他系统可以在内部重新排列事物以更好地支持新状态。

例如,您将构建一个“ Sprite”或“ Drawing”或“ Rendering”系统。该系统将负责确定需要与哪个角色一起显示角色,以及如何显示角色。然后,当角色死亡时,将其删除。

另外,一个“ AI”系统可以告诉角色该做什么,该去哪里,等等。这应该与许多其他系统交互,并根据它们做出决策。同样,死角字符可能可以从该系统中删除,因为它们实际上不再做任何事情了。

您的“名称”系统和“家谱”系统应该将字符(有效或无效)保留在内存中。无论角色的状态如何,该系统都需要重新调用该信息。(即使我们将他埋葬了,吉姆仍然是吉姆。)

这还为您带来了改变系统更有效响应时间的好处:系统具有自己的计时器。有些系统需要快速启动,而有些则不需要。这是我们开始研究使游戏有效运行的原因。我们不需要每毫秒重新计算一次天气,我们大概每5秒钟可以进行一次。

它还可以为您提供更多的创造力:您可以构建一个“路径查找器”系统,该系统可以处理从A到B的路径的计算,并且可以根据需要进行更新,从而使Movement系统可以说“我需要在哪里下一个?” 现在,我们可以将这些问题完全分开,并更有效地进行推理。运动不需要找到道路,只需要让您到达那里即可。

您将需要将系统的某些部分暴露给外部。在您的Pathfinder系统中,您可能需要一个Vector2 NextPosition(int entity)。这样,您可以将这些元素保留在严格控制的数组或列表中。您可以使用更小,struct类型,它可以帮助你保持部件更小,连续的内存块,它可以使系统更新快。(特别是如果对系统的外部影响很小,那么现在只需关心它的内部状态,例如Name。)

但是,我对此并不足够强调,现在一个Entity仅仅是一个ID,包括图块,对象等。如果实体不属于系统,则系统将不会对其进行跟踪。这意味着我们可以创建“树”对象,将它们存储在SpriteMovement系统中(树不会移动,但是它们具有“位置”组件),并将其与其他系统隔离。我们不再需要特殊的树列表,因为除了纸质文字外,渲染树与字符没有什么不同。(这是Sprite系统可以控制的,还是Paperdoll系统可以控制的。)现在NextPosition可以稍微重写Vector2? NextPosition(int entity)一下:,它可以null为无关的实体返回位置。我们也将其应用于我们NameSystem.GetName(int entity),返回null树木和岩石。


我将对此进行总结,但是这里的想法是为您提供有关ECS的背景知识,以及如何真正利用它来为您的游戏提供更好的设计。您可以提高性能,分离不相关的元素,并使事情保持井井有条。(这也与功能语言/设置(例如F#和LINQ )配合很好,如果您还没有的话,我强烈建议您检查F #;当您结合使用它们时,它与C#会很好地搭配。)


您好,感谢您的详细回复。我仅使用一个GameObject Manager,其中包含对所有其他游戏角色的引用。
paul p

在Unity中进行开发围绕着被称为实体的GameObject事情,这些实体并没有做很多事情,但是有一系列Component可以完成实际工作的类。大约十年前关于ECS发生了范式转换,因为将代理代码放在单独的系统类中更加干净。Unity最近也实现了这样的系统,但是他们的GameObject系统过去一直都是ECS。OP 已在使用ECS。
拉斐尔·施米兹

-1

在Unity中进行此操作时,最简单的方法是:

  • 为每个角色或单位类型创建一个游戏对象
  • 将它们保存为预制件
  • 然后您可以在需要时实例化预制件
  • 当一个角色被杀死时,销毁游戏对象,它将不再占用任何CPU或内存

在您的代码中,您可以将对对象的引用保存在列表中,以免您一直使用Find()及其变体。您正在以CPU周期为内存进行交换,但是指针列表非常小,因此即使其中包含数千个对象也不会有太大问题。

随着游戏的进行,您会发现拥有单独的游戏对象会给您带来很多好处,包括导航和AI。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.