游戏引擎学习第56天

发布于:2024-12-23 ⋅ 阅读:(14) ⋅ 点赞:(0)

仓库: https://gitee.com/mrxiao_com/2d_game

回顾

我们正在设计一个从零开始开发的游戏引擎,目标是不使用任何现成的库或引擎。在当前阶段,我们正专注于如何管理游戏世界中的实体布局,优化系统以便能够处理不同频率的实体更新。

目前,我们将实体划分为两个类别:高频更新实体低频更新实体。高频更新实体包括屏幕上可见以及周围区域可能影响玩家体验的实体,例如声音或其他效果。低频更新实体则是屏幕外或远离玩家的实体,它们的更新频率较低,用于减少计算压力。

为了实现这一点,我们设计了一个逻辑区域,当摄像机移动时,会动态调整高频和低频实体的归属。具体来说,屏幕内以及周围一定范围内的实体会被归类为高频更新,而超出这个范围的实体会被移至低频更新集合中。这种设计确保了当前需要实时处理的实体数量保持在一个较低的范围内,从而提升系统性能。

此外,这种划分方式还支持游戏的可扩展性,即使游戏世界变得非常庞大或包含数十万甚至上百万的实体,也不会显著影响游戏性能。高频更新实体的数量始终受到控制,而低频实体的模拟可以以更粗略的方式进行。

目前,我们在代码中实现了这一逻辑。例如,摄像机移动时,会调用一个检查频率的函数,根据实体的位置将它们分类到高频或低频集合中。同时,我们将实体的状态完全存储在系统中,包括类似墙壁的静态元素,这样可以统一处理所有类型的实体。

在性能优化方面,这种设计适合现代快速的CPU环境。高频集合的实体数量保持较少,而低频集合则可以根据需要扩展。最终目标是实现一个既高效又灵活的实体管理系统,为后续的游戏开发提供坚实的基础。

目前的重点是继续完善这些功能,确保系统的稳健性和性能优化。同时,我们计划探索更多适配的绘图工具,以便在后续开发过程中更直观地展示设计思路。
在这里插入图片描述

缩放低频实体更新


能够达到这种规模

所以我在这里试图解释的是,这意味着那些对所有对象进行操作的调用,比如说,遍历所有高频实体并检查它们是否在矩形内。这个矩形是模拟区域,比屏幕大,但相机只捕捉到其中的一部分。实际上,我们可以称之为“高频边界”,因为这是高频实体的作用范围,而不是严格的相机边界。
在这里插入图片描述

遍历所有高频实体是可以接受的。虽然将来可能需要优化,但现在并没有紧迫的需求,因为高频实体的数量较少,不会严重影响帧率。

然而,另一个遍历低频实体的循环是个问题。如果我们将数组的大小扩展到一百万个实体,那么这样处理的性能显然不足。这会花费太长时间进行处理,严重拖慢游戏速度。我们可以合理地证明这一点。
在这里插入图片描述

修改发现的一个Bug

在这里插入图片描述

计算机的速度快得令人惊讶

即使我们没有进行优化,代码的运行仍然足够顺畅,没有特别的问题。如果我在初始化游戏时添加大量低频实体,比如在初始化时通过一个循环添加很多墙体实体,然后把它们随机放在远离相机的位置(在相机范围之外),我们可以测试性能。

增加这些低频实体后,如果我们检查游戏状态中实体的数量,可以看到游戏仍然可以正常运行。但当实体数量达到一定规模,比如几百万,我们的游戏帧率就会显著降低。

为了展示这个问题,我尝试将实体数量逐渐增加,从数十万到数百万。到500万个实体时,游戏运行明显变慢,表现得非常迟钝。这是因为每一帧都需要检查这些实体是否在矩形内,计算量变得非常大。

需要更高效的处理方法

通过这个测试,可以明显看出,当实体数量增长到一定程度时,现有的方法已经不再高效。这表明我们需要改进低频实体的处理方式,比如引入更优化的分区技术或空间索引结构,以减少需要遍历的实体数量。


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

修改瓦片地图

目前的帧时间不会因采取任何方式而显著变化,因此我们可以通过这种方式进行调试。更重要的是,现在的代码是调试代码,并且当前还没有针对优化进行编译。这让我们处于一个相当不错的起点。

接下来,我们计划进行一个架构上的调整,尽管这个调整并不是为了提高速度,而是为了更好地组织数据。之前我们为瓷砖地图创建了一个哈希表,使其可以以稀疏的方式存储数据。这种方法目前运行良好,但我们认为是时候改变一下这种设计了。

当前瓷砖地图的设计过于依赖于瓦片的组织方式,而我们希望将世界划分为可以按需加载的部分。这些部分可以稀疏存储并分页进出,以提高存储效率。我们倾向于将这些部分视为包含实体的容器,而不是固定在瓷砖地图上的概念。

进一步来说,我们希望以更自由的方式组织和渲染图形,而不是受限于传统的瓦片地图格式。瓦片地图更像是一个指导性工具,而不是最终的实现目标。现在,我们已经实现了墙体和实体的管理,因此可以考虑将瓦片地图的功能转变为仅仅是一个存储实体的容器。

即使这些实体按照瓦片的方式存储或生成,那也只是为了方便,并非强制要求。我们追求的是灵活性,不需要严格遵循瓦片化的概念。这种调整将更适合当前项目的发展需求,同时为未来的扩展提供更多可能性。
在这里插入图片描述

考虑将瓦片地图改为存储实体而不是瓦片

当前的重点是调整瓷砖地图的概念,以更加适合存储实体而不是存储带有数据值的瓦片。这一调整将从两种方法中选择:

  1. 直接存储实体:
    实体直接存储在对应的瓷砖块中。这种方法的优点包括:

    • 空间定位更加高效。当需要访问某一特定空间点的数据时,可以直接从相应的内存块中提取,无需额外查找。
    • 简化了实体的空间处理流程,适用于基于空间的频繁操作,例如碰撞检测或区域更新。

    但此方法也有缺点:

    • 引用某个特定实体会变得复杂。例如,如果需要跟踪某个特定实体的位置,没有全局索引的情况下很难快速定位该实体。
    • 需要一个额外机制来管理引用的有效性。若实体被删除或移动,其引用需要同步更新,否则可能导致数据不一致。
  2. 索引指向实体:
    瓷砖地图仅存储指向实体的索引,而实际的实体存储在一个独立的实体表中。此方法的优点包括:

    • 提供全局访问的灵活性。通过索引,可以快速定位任意实体,无论其空间位置。
    • 适合需要频繁引用特定实体的场景,例如实体间的交互或玩家控制的对象。

    但此方法的缺点在于:

    • 增加了访问开销。每次空间定位后需要额外的索引查找步骤。
    • 在高频空间处理任务中,可能导致性能下降。

综合考虑:
根据实际需求,空间处理的效率可能比引用的灵活性更为重要,因为大多数实体在多数时间并不需要被直接引用。仅少量实体会涉及复杂的引用需求,如被跟踪或作为其他实体的子对象。因此,优先将实体按空间存储在块中是合理的。

为支持特殊的引用需求,可以引入一个额外的机制,例如:

  • 为需要持久引用的实体提供一个唯一标识符,并通过全局表管理这些标识符与实体的映射关系。
  • 当实体被移动或删除时,确保同步更新映射关系。

最终的设计目标是平衡空间处理效率与引用灵活性。下一步将评估这些存储方式的具体实现细节,并确保整体系统能够兼顾性能与灵活性。

关于如何在内存中引用实体的选项

我们正在讨论如何管理游戏中实体的存储和引用问题,并针对一些技术细节展开深入分析。


存储实体的方式

在初始阶段,子块主要用于存储瓷砖数据。但现在,它们开始存储一组实体。这些实体数据可以被组织为一个“低频实体块”,这些块内部存储了多种实体。


空间迭代的简化

在当前设计下,当需要遍历空间中的数据时,可以轻松地获取所有存储在子块中的实体。这种存储方式设计简单,易于操作。


间接引用的实现方法

我们探讨了如何间接引用这些实体。主要有两个选择:

  1. 通用 ID 系统:为每个实体分配唯一的通用 ID,并通过一种树状结构或数组将 ID 与实体关联。通过这种方式,可以定位实体所在的子块及其索引。
  2. 仅存储指针:直接存储指向实体的指针。但在大多数情况下,还需要附加信息,因此需要更多的存储结构。

引用系统的优化

我们也考虑了一种更加动态的设计,即仅在需要时为某些实体创建引用。这可以通过以下方式实现:

  • 引入“引用计数”机制:仅为当前需要被引用的实体生成引用。
  • 这种设计减少了冗余数据存储,但同时引入了复杂性。例如,当实体需要更新其位置信息时,由于实体本身并未存储回溯引用信息,因此找到对应的数据变得困难。这可能导致性能问题。

空间与性能的权衡

我们探讨了一种妥协方案,即采用“低实体计数表”。这一方案包括:

  • 在子块中记录每个实体的索引或位置信息。
  • 虽然这会浪费一些内存,但查询效率相对较高,并且不会显著影响 CPU 和缓存性能。
  • 对于大规模的实体(例如 500 万个实体),这种方案可以有效解决查询效率低的问题。

未来的改进方向

当前的方案更多是基于直觉和实验的权衡,目的是快速实现功能并验证效果。未来可能会根据实际使用情况进一步优化设计,以在内存消耗与性能之间找到更好的平衡。


总结来说,我们通过逐步优化和权衡,为游戏中大量实体的管理设计了一个高效的存储与引用机制。这一机制兼顾了空间效率和性能,能够很好地适应复杂的游戏场景。

在这里插入图片描述

将实体存储在瓦片实体块中

我们可以考虑如何处理这些瓷砖块的问题。不确定如何存储这些实体,所以需要进行一些思考。初步想法是将实体存储在块中,因为每个屏幕上都会有一定数量的实体,至少包括墙壁和树木等。每个瓷砖块可能包含一些实体,因此我们可以将实体存储在所谓的“实体块”中。每个实体块将存储一定数量的实体,并且作为内存的中介,允许实体在块内“聚集”。

在设计中,每个块大致包含16个实体,而这些块可以链式存储。随着瓷砖块被实体填充,它们会继续使用更多的块。最终,我们不再使用瓷砖块,而是直接使用实体块,每个屏幕开始时都会有一个实体块。这个实体块将记录它的填充状态,以便了解当前块内有多少实体。如果不需要追踪实体数量,我们也可以让实体标记是否存在,然后每次处理16个实体。

此外,还有其他选择。比如,我们可以选择存储更多的瓷砖块,而不是通过块存储实体。这将意味着瓷砖块之间将有更多的实体引用,可以在处理时依据位置进行匹配。每个瓷砖块可能会分配更多的空间存储实体引用。虽然这种方式不确定是否有效,但值得考虑。

最终,效率方面,我们需要特别关注高优先级实体。它们需要能够快速写回到低优先级实体中,这可能是效率瓶颈所在。低优先级实体可能会受到更多的引用和操作,而这些引用可能是最频繁访问的,尤其是在实体之间进行交互时。
在这里插入图片描述

思考改进低频实体访问的技术

在设计中,考虑了高频实体与低频实体之间的交互。高频实体不断与低频实体交互,因此,处理这些交互的方式是一个关键问题。当前的结构可能不够理想,需要寻找一种更高效、更符合逻辑的方案来优化这种交互。

考虑到高频实体需要频繁回到低频实体,这意味着对瓷砖块的迭代可能仅用于移动物体到相机周围的视野,而不一定适用于所有的交互。这样,间接访问的结构可能应该放在另一侧。这个思路有可能帮助做出更合理的设计决策,因此需要进一步的思考和评估。

反向思考方向

这个方案可能是正确的做法。低频实体将保留在这里,且它们将直接存取。接下来,将在外部存储每个瓷砖块相关的低频实体,并且不再是一个瓷砖地图。其核心思想是将这些低频实体与空间分区存储在一起,这样可以根据需要扩展这些实体的规模。例如,当实体数目增加到500万时,系统可能会变得较慢,因此需要优化以避免性能问题,特别是在低端CPU上。
在这里插入图片描述

移除无效代码

首先,需要删除一些不再有用的内容,例如SetTileValue,这些函数已不再需要,因为它们对于当前的系统来说没有意义。原本用来设置和获取瓷砖值的部分现在已经不再需要,这些数据会被逐步移除,转而使用实体来替代。删除操作的目的就是去除所有不再需要的内容,使得系统更加简洁。

接下来,仍然需要保留一些重要的部分,比如获取瓷砖块的位置,因为这些信息对于后续的操作仍然有用。删除过程中将会保留一些与访问相关的数据,确保在清理无用数据的同时,仍能保留必要的信息。

经过这些调整,最终将会去除大部分不再使用的内容,并且确保仅保留当前实际需要的部分,例如获取瓷砖块和位置的功能。

在这里插入图片描述

在这里插入图片描述

考虑瓦片地图 TileChunkHash 的大小

首先,开始了关于如何存储和管理实体的调整。以前,tile不再是活跃成员,也就是说,不再使用传统的瓷砖值。之前存储的瓷砖块数据开始变得过于庞大,空槽占用了大量空间,因此必须对数据结构进行优化。

对于块缓存来说,一种方式是通过指针来存储,这样能够避免双重指针引用的问题,减少不必要的空间浪费。然而,如果数据结构继续保持较大的数组形式,并且出现许多空槽,那么会导致空间浪费。因此,考虑采用平面指针结构,这样可以通过初始的双重间接来解决问题,确保数据紧凑并减少内存浪费。

另外,对于低频实体的存储,需要进一步调整和优化存储方式,使其能高效地存储和访问。这个过程中,部分之前的内容会被清除,而新的存储方法会根据需要进行实施,确保在访问效率和内存使用方面达到平衡。

最终,虽然这些改变对当前的功能没有明显影响,但在未来可能会提高系统的性能,特别是在处理大量实体时。
在这里插入图片描述

在这里插入图片描述

将瓦片地图重命名为世界

重命名与重构

  • 该过程开始时,通过重命名和重新组织文件,特别是将与瓷砖地图相关的代码迁移到更通用的“世界”系统中。这一转变帮助简化了游戏世界实体的管理。
  • game_tile.cpp game_tile.h这样的文件被重命名和重构game_world.cpp game_world.h,以适应新的世界系统结构。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

调试重构

这里描述了一些程序调试和问题排查的过程。主要问题是世界(world)没有被初始化,导致出现了垃圾数据。
在这里插入图片描述

回顾总结

  1. 摒弃瓷砖概念:传统上,游戏世界的布局往往使用瓷砖系统,每个瓷砖代表一个固定的空间单元。然而,当前的思考是放弃这种固定的格局,改用更灵活的方法来表示空间和物体的位置。

  2. 物体位置的相对性:放弃瓷砖后,物体的位置不再依赖于固定的网格,而是相对于其他对象或“区块”(chunks)。区块可以看作是比瓷砖更大的空间单元,可能包含多个物体。因此,如何有效地映射和管理这些位置,确保它们能够与区块相对并保持精确性,是当前面临的挑战。

  3. 浮点精度问题:浮点数在计算机中的表示有精度限制,特别是当数值很大或很小的时候。开发者需要考虑如何在不牺牲精度的情况下,管理物体的位置,避免由于浮点数精度问题导致的错误或不一致。

  4. 房间中的位置设定:例如,开发者需要一种方式来设置房间的中心点,并确保物体可以精确放置在某个位置。这种方法应该直观易懂,同时避免由于浮动精度引起的问题。

  5. 世界的连续性与弹性:目标是创建一个既能维持空间的连续性,又能适应精度限制的系统。也就是说,世界的结构需要保持一致和流畅,但同时要具备一定的灵活性和适应性,以应对可能的技术限制。

“即使没有瓦片,你仍然期望游戏玩法大致基于网格吗?”

关于游戏是否会保持广泛的网格基础,即使没有瓷砖的情况下,答案是肯定的。尽管未来可能会摒弃瓷砖的概念,但仍然希望保持网格结构,这样玩家能更容易理解和思考游戏世界。尽管如此,目标是让引擎支持任意布局,使得物体可以不必依赖网格,但在某些情况下仍会在网格上创建物体,以帮助玩家更好地理解游戏空间。

即使不需要碰撞检测或移动,游戏中的一切都会是实体吗?

讨论的重点是是否可以将游戏中的所有事物都视为实体,即使它们不需要碰撞检测或移动功能。尽管某些事物不具备实体的任何特点,但依然可以将其视为实体。通过这种方式,游戏会变得更加动态,能够让更多的内容参与其中,增强游戏的丰富性。举个极端的例子,比如一根魔杖可以让物体变成实体,譬如挥动魔杖使墙壁长出腿来走动,这样的设定非常有趣。

整体上,目标是使所有事物都成为实体,并在此基础上探索如何让这些实体更具动态性,这样它们可以代表不同的物体,如墙壁或其他真实的东西,并在这些之间实现平滑的过渡。相比于硬编码物体之间的差异,采用这种动态方式处理会更为有趣。至于实际开发中,具体实现实体系统的难度还需要在编程过程中进一步探索。

你不认为大多数存储问题源于仅仅有高频和低频实体,而通过更细化的实体分组可以解决这些问题,从而创建更多更小的特定实体数组吗?

大部分存储问题源自于高频和低频实体的处理,而通过增加粒度、使用更多的小规模实体数组,可以有效解决这些问题。然而,目标是让游戏中的实体保持高度动态,因此不希望在设计中实现同质性。尽管同质化可以提升性能,但这种做法不适合游戏的设计目标,因为希望游戏能够支持多种不同类型和组合的事物,这样能够更灵活地应对各种情况。因此,虽然同质化在某些情况下有助于性能,但在当前设计下,仍然倾向于保持实体的多样性和灵活性。

你提到过考虑让世界块存储固定数量的实体,然后在必要时为哈希表添加多个块以存储更多实体。如果采取这种方法,你将如何比较一个块与另一个块?多个块会具有相同的 X,Y,Z。

在讨论中提到,如果采用将世界块存储固定实体数量并在哈希表中添加多个块的方式,查询时需要比较多个块,因为这些块可能具有相同的XYZ坐标。为了实现这一点,查询会遍历所有匹配的块,而不是找到一个匹配后就停止。这意味着所有匹配的块都会被处理,而不是在第一个匹配时退出。

因此,考虑到简便性,选择让哈希查找返回一个链表,并通过迭代器逐一遍历这些块,而不是使用更复杂的方式。虽然当前的实现还未完全决定,可能在后续开发中会改进或采用更简便的方式进行遍历。
式。虽然C++中有一些改进,但这种方式并不是语言的本意。

为什么 world_position 不能直接拥有指向 world_chunk 的指针,而不是所有这些 AbsTiles?然后 Offset_ 就相对于 world_chunk。

在讨论实体压缩和解压时,提到了一些关于如何存储和处理对象状态的问题。首先,提到可以将世界块存储为指向世界块的指针,而不是直接存储所有的详细信息。这样做可以节省一定的空间,因为指针通常比存储完整的坐标(如XYZ坐标)要小。然而,这种方法并不能大幅节省空间,因为指针本身也需要占用一定的内存(例如8字节),而直接存储坐标会占用12字节。

尽管如此,使用指针的方式并不完全理想,特别是当需要从哈希表中删除块时。由于仅存储指针,无法直接知道该块的位置,因此在需要删除块时会遇到困难。而存储完整的XYZ坐标可以确保能够准确地进行操作,包括删除或移动块。

最终的目标是尽可能减少存储空间,尤其是希望找到一种方法完全摆脱这种存储方式。如果能够做到完全优化存储,那将是理想的,但也承认可能无法完全实现。

在这里插入图片描述

在这里插入图片描述

我想说的是,我们似乎正朝着被称为实体组件系统的方向发展,其中现在称为‘高频’和‘低频’的部分只是组件,未来可以随意为实体添加其他组件。

讨论中提到,目标是朝着实现一个实体组件系统(Entity-Component System,ECS)发展。在这种系统中,所谓的“高频”实体和“低频”实体实际上只是组件的一部分。高频实体可能主要用于存储诸如动画状态等信息,而低频实体则包含一些永远不会被删除的组件。这些组件将始终存在,并且是低实体的一部分。

尽管如此,讨论者表示不希望过早做出假设,而是希望根据实际使用的模型逐步发展出合适的解决方案。因此,虽然存在组件的想法,但如何具体实现和管理这些组件,还需要在实践中逐步探索,而不是过度设计或制作通用的处理方式。

看到计算机能处理一百万个实体而不会卡顿,额外的指针间接访问带来的性能损失真的值得担忧吗?

讨论围绕性能优化展开,尤其是关于指针解引用对系统性能的影响。主要总结如下:

  1. 指针解引用与效率问题
    在系统设计中,指针解引用操作可能会显著降低性能。当前的循环处理方式是理想情况,不涉及任何指针解引用,完全流式处理,内存以顺序方式读取和写入,对处理器非常友好。然而,指针解引用带来的性能开销可能非常大,有时甚至会导致性能下降多达200倍。

  2. 性能考量的实际情况
    对于某些场景,例如瓷砖块数据的访问,频率较低,额外的指针解引用操作可能对整体性能影响不大。但如果系统需要扩展到更大规模,这种开销可能变得不可忽视,因此需要在设计阶段权衡这些细节。

  3. 教育和学习的目标
    开发过程不仅仅是实现能够运行的系统,更是要通过设计和实现高效、专业的系统,培养问题解决能力。即使当前某些性能问题不显著,也需要提前考虑未来潜在的扩展和优化需求。

  4. 硬件平台的差异性
    当前系统在高性能硬件上运行效果良好,但如果迁移到资源受限的硬件平台(如树莓派或移动设备),性能问题可能更加突出,因此需要为这些情况做好准备。

  5. 注重效率和系统设计的专业性
    系统设计需要综合考虑效率和专业性,即使某些情况下性能开销可以接受,也不能放弃对高效系统设计的追求。这种设计方法不仅提升系统性能,还为未来可能遇到的性能瓶颈提供了解决思路。

  6. 特定循环的性能分析
    当前的循环操作即使在大规模处理下也表现出色,不会成为性能瓶颈。但在迁移到更低性能的设备时,例如树莓派或移动端,可能需要重新评估这些操作的效率。

实体能够从一个块转移到另一个块吗?

讨论涉及敌人如何在不同的地图块(chunk)之间转移以及实体系统的使用,主要总结如下:

  1. 敌人的块间转移
    敌人能够在不同的块之间自由转移,这是一种默认行为。无论是普通的敌人还是其他移动的实体,只要具备移动能力,就可能频繁地在块之间切换。

  2. 特殊情况:静态场景
    如果某些敌人或物体属于纯粹的场景装饰(如静态的背景或不可交互的物品),则它们不会进行块之间的转移。这些对象通常是固定在某个块内的,不参与动态更新。

  3. 实体系统的未来使用
    系统可能会逐步开始采用实体系统对敌人及其他对象进行管理,这种方式能够更高效地处理块间的动态转移和状态更新。

总结:
在设计中,动态实体(如敌人)将具备在不同块之间转移的能力,以支持开放式或动态更新的场景。同时,静态场景物体则无需参与这种动态管理。未来将考虑使用实体系统以便更好地实现这一逻辑,从而提升系统的可扩展性和管理效率。

我们很快会将实体系统用于玩家和墙以外的其他东西,还是会先处理渲染器?

讨论涉及测试内容的添加顺序以及渲染器的实现计划,主要总结如下:

  1. 测试目的的内容添加
    在实现渲染器之前,为了测试目的,将会添加更多内容来进行初步验证。这些内容可能包括:

    • 可拾取物品(例如道具、奖励等)。
    • 宝箱等交互对象。
    • 敌人或其他动态对象。

    这些测试内容将帮助确保基本功能正常运行,并对整体效果进行验证。

  2. 渲染器的实现顺序
    当前计划是在添加了上述测试内容后再开始着手实现渲染器。通过这些测试元素,可以更清楚地看到系统的实际运行效果,然后再集中精力开发渲染器,以适配这些功能。

  3. 流程总结
    在测试内容加入后,将进行系统化的验证,确保功能与预期一致。随后,会进入渲染器的开发阶段,这是为了确保流程的清晰性和测试的完整性。

  4. 当前状态检查
    对队列的处理已接近尾声,整体工作进入了收尾阶段。当前任务的完成标志着阶段性目标的实现。

总结:
当前重点在于为测试目的添加基础功能和对象,确保其与系统运行的契合度。完成初步测试后,将转向渲染器的开发,为后续的展示和优化奠定基础。整个流程有条不紊地进行,以确保开发的每一步都在正确的方向上推进。