仓库:https://gitee.com/mrxiao_com/2d_game
回顾
目前正在处理的是关于游戏结构的核心部分,主要包括如何存储、更新以及追踪世界中的实体,以及实体之间如何交互等内容。这是开发中一个非常重要的环节,直接影响到后续的游戏功能和性能表现。
昨天的进度主要是整理代码,将部分内容拆分到game_world.h
和game_world.cpp
文件中。当前正在逐步完善实体更新的逻辑,尤其是如何在“活动实体集”中高效地添加和移除实体,以优化运行时性能。
接下来计划:
- 开始创建一个更大的世界,测试代码在较高负载下的运行情况。
- 验证实体是否正确地在“活动资产集”中进出,而不是简单地通过全局处理完成所有任务。
- 确保代码逻辑在更复杂的场景中也能正常工作。
目前的一些注意点包括:
- 实体渲染部分暂未处理,因此当前代码中实体(如玩家和墙壁)的顺序可能会显得不合适。
- 接下来将专注于优化代码逻辑,并尝试在代码中引入更加智能的空间存储和排序机制。
此外,还存在一些未决问题需要后续解决,例如未来如何更高效地管理大型世界的分区与实体更新等。但这些问题的探索和解决预计会在后续开发阶段逐步展开。
总结:当前工作的重点是提升代码在大规模场景下的性能,同时为后续优化空间分区和实体管理打下基础。
game.cpp:创建更多的房间
目前的任务是创建更多的房间,以扩展世界的规模。当前有一个功能可以按照设定的数量创建多个屏幕并生成对应的房间。接下来的目标是将这一数字大幅提高,例如生成 2000 个房间,而不是之前创建的较少数量,以观察系统在大规模生成时的表现并确保生成过程正确。
以下是具体的操作与考量:
-
调整房间数量
- 当前系统已经不再生成不必要的“无用实体”,因此理论上可以支持生成更多的房间而不会出现内存或性能问题。
- 将目标房间数量设置为更大的数值,验证生成逻辑的正确性和稳定性。
-
角色移动与测试
- 尝试通过测试玩家在这些房间中的移动情况,观察房间生成后角色的表现。
- 目前还没有用于加速角色移动的快捷键,因此测试中可能需要花费较多时间步行来回穿越这些房间。
-
地图的显示与缩放需求
- 当前开发中尚未实现渲染功能,因此调试时无法清晰地查看世界的全貌。
- 考虑未来需要实现地图缩放功能,或者提供其他方式来展示世界的结构,以便更直观地进行调试和优化。
-
调试优化
- 创建大规模房间后,将进一步测试和完善调试工具,以便更高效地验证生成逻辑以及系统在高负载下的表现。
- 虽然目前可视化手段有限,但可以通过适当的线索或标记简化调试流程。
总结:通过大幅增加房间数量,可以检验系统的扩展能力,同时为后续实现更高级的渲染和地图显示功能打下基础。此外,还需要提升调试工具的直观性与效率,以适应更复杂的场景测试。
调试器:在遍历世界时检查游戏状态(GameState)
当前的系统状态被分析和测试,以确保核心功能运行正常。以下是详细的总结:
-
采样和检查密度
- 通过手动方式进行简单的检查,以评估渲染前游戏状态的具体情况。
- 关注假设数量、区域数量,以及这些状态变化是否一致。
- 验证对象在世界中的移动和集合的正确性,确保它们被适当添加或移除。
-
设置断点与调试
- 在渲染前设置断点,快速查看游戏状态中的对象数量和其在集合中的表现。
- 通过对比数字,确认对象进出集合的行为是否符合预期。
- 如果集合中的对象持续增长,可能表明驱逐机制存在问题,需要进一步排查。
-
驱逐机制验证
- 观察集合的大小变化,确保驱逐机制能够正常移除不再需要的对象。
- 检查是否存在重复添加的对象,以及是否有其他潜在问题导致集合无法保持有限规模。
- 临时通过代码中的检查点,验证集合的数量是否稳定在合理范围内。
-
性能与帧率问题
- 注意到当前帧率较低,尤其在调试模式中。
- 对渲染中的不稳定性进行了观察,确认其主要与性能限制有关,而非系统逻辑错误。
-
结果确认
- 驱逐机制运作良好,对象集合数量始终保持有限和可控。
- 实体集合在玩家附近范围内得到了合理维护,数量维持在一个稳定且可处理的水平。
- 当前的实体管理逻辑可以确保在高密度情况下,系统能够正常运行。
-
下一步改进计划
- 在未来可能添加更多渲染和调试工具,例如直接打印集合数量以便实时监控。
- 考虑在调试期间,通过头显界面显示集合状态信息,提高开发效率。
最终确认了当前实体驱逐和管理机制的有效性,实体数量合理且性能可以接受,后续将优化调试工具以便更直观地分析数据。
考虑创建空间分区
我们正在考虑实现空间分区,以避免在处理每个实体时出现昂贵的循环操作。以下是详细总结:
-
避免昂贵的循环
- 为了避免每次相机移动时都要遍历系统中的所有实体,开始考虑使用空间分区。这是为了避免过多的性能开销。
- 在初步测试中,系统能够处理高达一百万个实体,但当数量达到五百万时,系统开始崩溃,表明需要优化处理方法。
-
空间分区的实施
- 空间分区允许将实体分布在不同的区域(如块)中,而不是在整个系统范围内进行循环操作。
- 每个“块”负责存储那些在其覆盖范围内的实体,这样可以有效地减少对不相关实体的查询和计算。
- 在需要将实体从低频集移到高频集时,可以快速从相应的空间块中提取相关实体。
-
空间操作的优化
- 通过空间分区,循环操作的范围将大大缩小,避免了对每个实体进行不必要的操作,尤其是当实体数量大幅增加时。
- 如果没有空间分区,查询将会变得极为低效,因为每个实体都会进行大量的空间查询,导致性能瓶颈。
-
性能与可扩展性
- 如果不采用空间分区,系统将面临查询和计算的急剧增加,特别是当处理大量自主实体时,这些实体在世界中移动并进行查询。随着实体数量增加,查询次数也会随之暴增,可能很快就会造成系统负担。
- 空间分区可以有效地解决这一问题,确保在处理百万级实体时,系统仍能保持较高的性能。
总结来说,空间分区是一种有效的优化手段,它能够减少不必要的循环操作,并大幅提高系统在处理大量实体时的效率和可扩展性。
黑板:讲解空间分区
我们讨论了如何通过空间分区来优化查询和减少计算开销。以下是详细总结:
-
空间分区的必要性
- 目标是通过空间分区限制查询的范围,避免在世界中的每个实体都进行大量的查询。如果没有空间分区,每个实体在进行查询时必须检查所有其他实体,导致计算量大增。
- 在没有空间分区的情况下,查询数量会呈现平方增长(O(n^2)),每个实体都需要检查所有其他实体,这会迅速变得无法承受,尤其是当实体数量达到十万个或更多时。
-
空间分区的作用
- 通过空间分区,每个实体只需要检查它附近的少量实体,而不是世界中的所有实体。这样就能大大减少查询的数量,从而提高效率。
- 空间分区确保了每个实体只关注与自己有可能互动的少量邻近实体,使得查询的成本变得更低。
-
空间查询的优化
- 如果没有空间分区,查询将会变得极为低效,每个实体都需要检查大量的其他实体。这种操作的复杂度为O(n^2),随着实体数量的增加,计算量会迅速增长。
- 空间分区通过限制查询范围,将查询的规模降低到一个可控的范围,避免了随着实体数量增加而导致的性能瓶颈。
-
实现与挑战
- 目前已经对空间分区进行了初步结构化,但还没有完全实现。虽然大部分工作已经完成,但我们还没有完全解决如何高效地完成这些查询操作的实现问题。
总结来说,空间分区的核心目的是通过限制查询的范围来降低查询成本,从而有效避免大规模实体时出现的性能瓶颈。这是处理大规模实体系统时的关键优化手段。
game_world.cpp:引入 ChangeEntityLocation
我们正在讨论如何将实体插入到世界中的空间分区,并处理实体的位置更新。以下是详细的总结:
-
插入实体的操作
- 首先,我们想要通过一个函数来插入一个特定的实体到世界中。这个插入过程涉及设置实体的空间位置,并确保它被插入到正确的区域或城市街区。
- 插入时,我们需要检查实体的位置。如果旧的位置和新位置不同,我们需要进行位置更新操作。具体来说,我们会检查旧的位置是否为空,如果是空的,这意味着该实体尚未存在于世界中,我们将其视为新的插入。
-
处理旧位置与新位置的比较
- 在检查实体的旧位置时,如果旧的位置与新位置相同,我们无需进行任何操作,因为实体已经位于正确的空间分区。
- 如果旧位置与新位置不同,我们需要将实体从其旧位置移除,并将其插入到新位置。
-
空位置和指针问题
- 如果旧的位置为空(即该位置没有有效的实体指针),我们就允许将实体插入到新的位置,这相当于在世界中第一次插入该实体。
- 如果旧的位置存在但位置发生了变化,则需要将实体从原来的位置删除,并将其移动到新的位置。
-
实体的插入与更新流程
- 实体首先会从其旧位置被移除,然后根据新位置插入到空间分区中。这个过程确保了实体始终处于正确的空间位置。
- 这个流程还包括了对实体在空间中的正确位置进行调试,确保每个实体都能正确地更新其位置。
-
位置更新的具体实现
- 当旧位置和新位置不同时,首先会执行一个操作,拉出实体从旧位置,然后将其插入到新的块或区域中。这是通过空间分区来实现的,确保每个实体都能被正确地重新定位。
-
后续操作
- 在位置更新之后,实体的位置将被记录在新的位置,确保后续查询和操作能够正确访问到该实体的最新位置。所有的插入和更新操作都会按照空间分区的规则进行,以优化性能。
总之,这个流程通过空间分区和位置更新机制,确保每个实体能被高效地插入和移动,避免了不必要的操作,并优化了整个系统的性能。
game_world.h:将 AbsTile
重命名为 Chunk
,并将 ChunkSideInMeters
添加到世界中
我们不再关心瓷砖的索引或偏移量,因为我们现在使用的是块(chunks)来表示世界的位置。以前,我们需要追踪瓷砖的索引以及其偏移量,但现在这种需求已经不再存在。瓷砖的概念仅仅是一个构建世界的手段,并不影响存储世界的方式。因此,我们将世界的位置完全基于块来表示,块的大小和位置不再受到瓷砖位置的限制。
我们不再需要考虑瓷砖的移动或位移掩码等细节,而是使用一个统一的尺寸单位,即每个块的尺寸,以米为单位。在这种新的方法中,我们不再关心具体的“米”是什么,而是将其视为一个可以由世界构建的抽象。块的大小会在内部存储,并以块的中心为基准来计算偏移量。这样,所有的实体和世界位置都会被存储为这些精细的小块,简化了世界管理和存储的复杂度。
在实际实现中,我们会直接存储块的x、y和z坐标,偏移量会基于块的中心进行调整。这种方法使得我们能够更高效地管理世界的位置,而不再需要依赖复杂的瓷砖索引和位置计算。
game_world.cpp:继续编写 ChangeEntityLocation
我们现在不再关心瓷砖索引了,因为之前我们追踪的是瓷砖索引和偏移量,但现在我们只关注块的位置。我们不再需要显式地处理瓷砖的概念,因为存储世界时,我们只关心块的定位。世界的位置现在完全基于这些块的坐标,而不再使用瓷砖索引。为了优化内存管理,我们通过一个memory_arena来存储数据,确保总是有能力分配更多的空间。
每当我们需要存储新的实体时,就通过memory_arena进行分配,并将新实体插入到块中。如果当前块已满(即块内的实体数达到上限),我们就需要创建一个新的块并将实体插入其中。具体来说,如果一个块的实体数量已经等于最大容量(例如16个实体),我们就会为其分配一个新的块并将剩余的实体移动到新块中。
通过这种方法,我们可以动态地管理实体的存储,确保不会因为块满而导致问题。每个实体都会被推送到相应的块中,块内的空间得到合理利用。
讨论将 FirstBlock
改为指针,而不是直接复制块
在考虑是否使用指针时,考虑了如果每个实体块填满后进行拷贝的问题。最初的想法是避免进行块的拷贝,因为拷贝操作可能带来额外的开销。但是在进一步分析后,发现实际上块拷贝并不昂贵,因为只有在每16个实体插入时才会发生这种拷贝。因此,尽管有这个初步的顾虑,最终认为在填满时进行拷贝可能并不是一个大问题,尤其是考虑到这种操作在游戏运行时的频率并不高。
考虑到这个情况,一个可能的解决方案是使用指针。通过将实体块转换为指针,可以避免频繁的拷贝操作,从而实现动态创建新的实体块并连接到已有块的结构。这种方式通过指针链表的形式逐步链接每个新的块,而不是在每次填满时都复制整个块。
然而,最终决定是否使用指针还需要进一步评估,特别是在实际的游戏环境中,这种操作的性能表现可能会有所不同。决定会基于更多的测试和分析,可能最终会选择保持现有的方式,或者在后期做出调整。
game_world.cpp:继续编写 ChangeEntityLocation
当获取一个新的实体块时,需要将其PushStruct并分配更多空间。这会消耗现有的空间,并且需要将新的实体块标记为空。接下来,将旧的实体块的指针指向新的块,并将新块的能量计数设为零,标记为空。通过这种方式,新的实体块可以继续接收新的数据。
为了避免频繁的复制,必须将旧块和新块之间的关系建立为指针链,这样就可以高效地管理这些块。通过指针,数据可以继续流动,不需要频繁地进行复杂的拷贝操作。当需要处理这些数据时,会验证当前块是否包含目标实体,并且如果找到了目标实体,采用交换位置的方式将其从列表中删除。
如果在过程中的任何步骤中遇到异常,必须立即进行断言检查。这样可以确保系统的稳定性,避免由于错误的内存操作而导致崩溃。在性能调优上,考虑到每次操作不是每帧都发生,因此并不需要对所有实体进行深度优化,但要考虑未来可能对频繁移动的实体做更进一步的加速优化。
总的来说,以上操作主要关注如何高效管理实体块,避免无谓的复制,通过指针操作来维护数据流动,确保在系统运行中的数据一致性和高效性。
黑板:将空闲空间保持在列表的头部
在这个系统中,有一系列的块,每个块内部都有很多实体。每个块可以容纳多个实体,例如每个块可能包含16个实体。当我们想要移除一个实体时,目标是保持空白的空间在列表的开头。
这样,当我们移除一个实体时,可以将一个实体从列表头部取出并放入空白的位置,这样空白空间就始终保留在列表的顶部。这个方法的好处是,不需要遍历整个列表寻找空白空间,而是能够直接将新的实体插入到列表的开头,避免了不必要的搜索。
另一种方法是从列表的末尾开始,但这种方式的效率较低,因为在查找空白位置时需要遍历整个列表。在这种情况下,先通过寻找实体的方式来解决问题,避免了两次查找,从而让整个过程更高效。
理解这个系统的概念可以通过一个实际的例子来说明:
系统概述:
系统中的“块”就像是一个容器,每个块里可以容纳多个“实体”。例如,每个块有16个位置(就像16个格子)。当我们需要移除一个实体时,目标是将空白空间保持在块的开头,以便下次可以快速插入新实体。
举个例子:
假设我们有一个块,里面已经放了16个实体,编号为1到16:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
现在,我们想要移除编号为5的实体。
方法1:将空白空间放在列表的开头
-
我们移除编号为5的实体后,空白空间就出现在位置5上。
-
然后我们从列表头部拿走一个实体(例如编号16的实体),并把它放到位置5。
-
结果是:
[1, 2, 3, 4, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 , _]
这样,空白位置始终在列表的末尾,并且新的实体可以快速放入开头的位置,不需要查找空白位置。这个过程避免了遍历整个列表,效率更高。
方法2:从列表末尾开始寻找空白位置
- 如果我们选择从列表末尾开始查找空白位置,假设我们会遍历整个列表直到找到一个空白的位置。这时我们可能会需要通过多次遍历才能找到合适的位置。
- 这种方法需要更多的查找和交换操作,导致效率较低。
比较:
- 方法1:通过始终保持空白空间在头部,每次插入新实体时不需要再次遍历整个列表,效率更高。
- 方法2:如果我们从末尾开始查找空白位置,可能会导致不必要的多次查找,因此效率较低。
总结:
这个系统设计的关键在于始终保持空白位置在列表的开头,以确保每次插入新实体时都能快速找到合适的位置,而不必遍历整个列表寻找空白空间。
game_world.cpp:继续编写 ChangeEntityLocation
黑板:将最后一个条目移到新释放的空间中
在这个过程中,我们的目标是操作实体的数量和位置。具体来说,我们想通过减少实体计数并移动实体的位置来更新实体列表。
- 减少实体计数:我们希望在某个位置减少一个实体,更新该位置的计数。通过调整指针或计数,使得该位置不再指向原来的实体。
- 移动实体:当某个位置的实体被移除时,我们将把其他位置的实体移到该空缺的位置。这涉及到将原本位于其他位置的实体移到新的位置,并更新相应的计数和指针。
- 更新实体位置:新实体的存储空间会被用来存放原本在其他位置的实体。我们要确保尾端的实体能够正确地搬迁并且更新位置。
总结来说,核心的操作是减少一个实体的计数,移动其他实体来填补空缺,并且动态地更新实体的位置和计数,以确保实体列表的正确性。
game_world.cpp:继续编写 ChangeEntityLocation
在这个过程中,核心目标是处理实体块的移动、计数和内存管理。
-
处理实体块:如果我们发现当前操作涉及的是第一个块,我们需要对其进行特殊处理。如果第一个块的实体计数为1,则需要进行调整,并确保如果当前块没有空间,可以移动或复制实体到其他位置。如果我们不是在第一个块,需要确保处理方式相同,确保在移动实体时更新相关的计数和位置。
-
复制与移动实体:在处理时,我们要确保实体从一个位置移动到另一个位置。这包括复制数据,减少原位置的实体计数,并确保目标位置正确地接收新的实体。如果在过程中某些块的计数降为0,意味着该块已不再有实体,应该释放内存。
-
内存管理:当我们释放某个块时,我们需要确保不会浪费时间在已经处理完的块上。通过适当的搜索停止和内存释放,优化性能,避免不必要的检查。
-
整体流畅性:通过处理块和实体的移动、复制及释放,保持程序的流畅和正确性,确保每个块的实体数和内存管理都能保持一致,不出现错误或资源浪费。
关于在数据结构和内存管理上花时间思考
人们通常不会花太多时间思考内存管理问题,这似乎大多数时候是自动处理的。我们更常花时间思考如何设计和优化数据结构,因为我们喜欢研究事物是如何存储的,并希望在这些数据结构上进行实验和创造,而不仅仅是使用通用的数组,像是直接调用搜索或映射之类的功能。
虽然有时内存管理的问题会浮现出来,但相较于其他方面,它似乎显得不那么重要。通常在系统设计中,内存管理的问题会不断出现,尤其是在涉及到实体块和内存使用时,我们需要确保对内存进行合理的分配和保留。尽管这可能是一个常见的讨论话题,实际上在许多情况下,处理内存管理并不复杂,尤其当我们确保内存得到有效使用时。
game_world.h 和 game_world.cpp:管理内存
内存管理的核心思路是尽量简化和自动化。基本的内存管理只是将空闲的内存块链表起来,并在需要时分配或者释放。具体来说,释放内存时,将空闲块指针指向刚释放的块,并更新相应的链接。此过程不涉及复杂的操作,内存管理系统非常简洁,只需要四行代码。
这种方法避免了在运行时进行频繁的内存分配,简化了内存管理的复杂性。在开发过程中,内存管理只是作为整体系统的一部分,几乎不需要特别关注。尽管处理空间查询等功能时,内存管理可能会涉及一些细节,但总体来说,它对开发者来说几乎是无关紧要的。其核心思想就是通过简单的操作,确保系统的内存得到有效管理,避免不必要的内存分配和序列化操作。
game_world.cpp:更新 InitializeWorld
并移除 GetChunkPositionFor
在此实现中,移除了与块移动和掩码相关的内容,如计算块偏移(chunk shift)和掩码(mask)的逻辑。现在仅保留块的边长(chunk side of meters)和整体单位米数(meters),使整个结构更直观。
主要优化点:
-
初始化块的第一个空闲指针为零。
- 确保内存管理的空闲块列表正确地初始化,这样在后续分配中可以顺利进行内存管理。
-
初始化块的敌意计数(enmity count)为零。
- 为了处理未初始化的内存,这在游戏运行多次时可能发生,保证每次运行时相关内存都被正确初始化。
通过这些操作,消除了可能由于未初始化内存引发的逻辑错误。
下一步工作:
-
空间分区逻辑的改进:
采用了一个哈希表加链式存储的方式实现空间分区。这种设计简化了原有的地板转换逻辑(transposition floor),并提高了整体的可读性和效率。 -
块结构与世界存储的关系:
将这些块结构与世界存储系统紧密结合。这种结合意味着这些数据不再适合放置在几何文件中,而是作为世界存储的一部分进行管理和操作。
当前优化的结果使系统更简单、直观,同时避免了潜在的初始化问题。后续可以继续完善哈希表的空间分区方案,以适应不同的游戏运行场景和性能需求。
更新 AreOnSameTile
的逻辑
我们认为这也是一个适合的地方。因此,现在需要开始讨论我们手头的其他事情。我们有一个变量m
,并引入了一种新的理解方式。接下来,我们希望将其转化,使其能够在同一块区域中操作。
我们继续查看代码,确认调用的位置是否正确,并检查变量是否在同一个块内。我们计划将这些异常情况修改为基于块的正常行为,以便流程更加统一和规范。对于“同一块”的定义,主要是对两个元素进行比较,判断它们是否位于同一个区域。这一逻辑在代码中被明确体现。
在具体实现时,我们会在检查过程中确保相关的偏移量均在合理范围内。若偏移量超出范围,则可能导致测试失败。因此,我们需要假设偏移量是正确的,以保证逻辑的正常运行。通过对这些变量的替换和优化,我们能够确保整个系统运行更加可靠和高效。
引入 IsCanonical
我们需要定义一个函数来检查某些内容是否是规范化的(canonical)。这个函数的主要目的是通过一个相对的偏移量来判断对应的状态是否符合预期。这一过程的重点在于提供一种高效且准确的方法来进行验证。
规范化性函数的核心逻辑是利用传入的相对偏移量,通过某种方式计算位置,并判断其是否处于合理范围内。如果范围超出界限,则测试可能会失败,因此我们需要在逻辑中确保这一点。此外,函数还会根据位置返回结果,确保其符合既定规范。
在实现过程中,我们通过建立单独的测试用例来验证不同情形。每个测试都将针对具体情况,比如判断某些元素是否在有效范围内。通过这种方式,我们能够清晰地界定功能的边界,并为调用者提供可靠的结果。
函数的另一个特点是确保调用者能够获得经过验证的结果。无论输入的初始状态如何,该函数都会在返回前执行必要的检查,保证结果符合规范要求。我们还计划在多个场景中重复使用该函数,以进一步验证其功能的通用性。
这一功能的引入使我们能够在任何时候快速检查某些对象是否为规范化状态,从而提高了代码的灵活性和鲁棒性。此外,我们还会对代码的其余部分进行检查,确保逻辑在各个模块中保持一致性。
通过这一系列调整,我们能够更好地管理和维护系统行为,使其在实际运行中更加稳定可靠。
继续代码清理
引入 ChunkPositionFromTilePosition
在代码实现中,提出了一个解决问题的思路,即创建一个名为 ChunkPositionFromTilePosition
的函数,用于处理从瓦片位置到其他数据的转换或调整。这种方法可能会简化问题,并有效解决当前的需求。
编写 ChunkPositionFromTilePosition
在代码中,我们需要实现一个功能,使得从瓦片位置转换到世界位置的操作变得简单,同时能够确保旧代码可以正常运行。即使这种转换可能在实际应用中很少使用,但我们需要确保它能够正常工作,以便测试和验证代码的逻辑与功能。
可以再讲一遍处理链表并移动实体的“lasagne code”吗?并且能移除 RecanonicalizeCoord
中关于环形拓扑的注释吗?
“The lasagne code” 是一个编程术语,指的是代码结构混乱、缺乏层次分明的组织或清晰的逻辑。这种代码通常由许多嵌套的层或依赖关系组成,类似于千层面(Lasagne)的叠加结构,因此得名。
我们分析了一段代码,该代码涉及链表操作和空间分区,旨在通过高效管理实体来实现动态位置更改。
核心流程与逻辑:
-
位置变更检查
- 首先检查实体在空间分区中的旧位置与新位置是否属于同一分区。
- 如果旧位置和新位置位于同一个分区,则无需进一步操作,直接退出流程。
-
实体插入与初次添加
- 如果实体是首次添加到世界分区,则直接插入至分区数据结构中,无需考虑移除操作。
-
移除旧位置的实体
- 如果实体已有旧位置,需要从其所在的分区中移除:
- 通过哈希表快速定位实体所在的分区链表。
- 遍历链表中的块,寻找目标实体所在的具体块。
- 找到实体后,从块中移除它,并将块中的最后一个实体填补至被移除的位置,以保持数据结构紧凑性。
- 如果某个块的所有实体被移除,将该块释放到空闲列表中以供后续复用。
- 如果实体已有旧位置,需要从其所在的分区中移除:
-
添加新位置的实体
- 在插入实体至新位置时,检查分区链表中的第一个块是否还有空间:
- 如果有空间,直接插入实体。
- 如果没有空间,检查空闲列表是否存在可用的块。如果存在,则从空闲列表获取块;如果不存在,则通过内存池分配新块。
- 将新块链接到分区链表末尾,并将实体存储至新块。
- 在插入实体至新位置时,检查分区链表中的第一个块是否还有空间:
-
空闲列表管理
- 移除过程中,空闲列表用于存储释放的块。
- 当需要新块时,优先从空闲列表获取,若空闲列表为空,则分配新的内存块。
代码逻辑特点:
-
断言检查
- 在多个关键点使用断言验证数据结构的完整性,例如:检查哈希表中是否存在分区块、验证分区块是否包含实体等。
-
数据复用
- 通过复用空闲块减少内存分配和释放的开销。
-
链表操作简洁高效
- 使用链表管理分区块,每次操作都确保结构紧凑,减少内存碎片。
整体结构优势:
- 高效性
- 利用哈希表快速定位分区,通过链表管理分区块,实现高效的实体增删操作。
- 动态性
- 支持动态添加、移除和更新实体,同时保证内存资源的高效利用。
- 数据一致性
- 使用断言确保数据结构操作的正确性,有助于快速发现潜在问题。
此代码体现了对于动态空间管理问题的深入优化,尤其适用于需要频繁更新实体位置的场景,例如游戏开发中的实时物理计算和场景更新。
你是否掉帧了?画面有点卡顿
根据观察,掉帧现象确实存在,虽然通常情况下掉帧率为0%,但目前掉帧率达到17%,这并不是理想的情况。对此问题已经尝试了多种解决办法,但仍然未能完全避免这种现象的发生。
你是否使用 Bink 来处理视频?
我们从未处理过涉及视频播放的内容,因此没有发布过任何与播放视频相关的项目。此外,未曾使用过粉红色或其他颜色用于与视频相关的功能设计。
你怎么看像“兔子跳跃”这样的移动机制?虽然是代码中的漏洞,但却增加了游戏性。如果你的游戏中有一种获得速度的技巧,虽然难以操作,你会保留吗?
尽管遇到一些困难,流的完整性问题并没有完全顺利,但过程仍然充满刺激。对于未来如何避免类似问题,似乎没有太多能做的,因为流本身就有其固有的特性。关于“兔子跳跃”这种游戏中的机制,尽管它是代码中的错误,但可能会影响游戏玩法,是否保留这种机制是个设计问题,而并非技术问题,自己对这一点没有明确意见。尽管如此,对于一些有趣的玩法,可能会考虑保留,但游戏设计师应该是最终决策者。此外,自己非常喜欢跳台滑雪,曾经在“部落”和“星种部落”中体验过类似的机制。
你对像 Entity_GetLocation
这种函数命名有何看法,相较于 GetEntityLocation
?
对于函数命名,倾向于使用接近英语的方式。例如,喜欢使用“GetEntityLocation”而不是“Entity_GetLocation”。虽然对于某些特定的库风格,可能会选择使用下划线命名,但通常不太倾向于这种命名方式。尽管如此,并不完全反对这种方式,只是觉得使用更为自然和简洁的命名方式会更好。
关于 PushStruct
,如果内存预分配用尽,它实际上可能会失败,对吧?所以当世界变得太大时,你是否仍然需要检查自定义块分配是否失败?
即使分配的内存不足,检查仍然是必要的。为了避免分配失败,采取了一些措施来确保内存不会超出预期。通过为世界生成设置限制,确保分配的内存始终在需要的范围内。未来可能会采取预先占据内存的策略,或者允许在特定情况下进行内存分配,如世界生成期间。如果分配失败,相关的内存块会被清除,这样程序就不会出现错误行为。整体而言,这些措施易于实现且能确保程序的稳定性,不会让分配失败影响到实际运行。
为什么在 Assert(Chunk)
后仍然需要 if(Chunk)
?
由于无法确保游戏完全无bug,决定避免程序崩溃。即使有时可能在错误地认为实体被插入到某个地方时,实际上并非如此,仍然会采取措施确保游戏能够尽量正常运行。这种小心谨慎的做法是为了减少因潜在错误而导致的不稳定性。
当你提到今天的代码性能不是关键时,我立刻想到了一场跨区块边界的机枪-投石机战斗
关于代码注释,尽量避免过多使用,除非是符号意义上的解释,如某些操作的目的。注释通常容易过时,尤其是当代码发生变化时,注释可能会与实际情况不符,导致理解上的困惑。因此,倾向于将代码写得尽可能简洁明了,让人可以从代码本身理解其功能。注释的使用主要集中在解释设计原则或代码结构,确保读者理解某些决定背后的理由,而不是一行一行地解释代码如何工作,因为这样通常没有帮助,反而可能产生误导。只有在某段代码需要特别说明时,才会添加注释。如果不能通过阅读代码理解其含义,可能就不该编辑这段代码。