仓库: https://gitee.com/mrxiao_com/2d_game
介绍
今天的主题是让世界存储真正实现稀疏化,即便当前效率可能并不高。我们计划花一些时间处理这个问题,并探讨相关的成本。稀疏化世界存储是接下来的重要步骤,为此需要逐步实施。
修复 SetCamera
中的拼写错误
具体问题在于逻辑判断中,代码包含了“大于或小于”的条件,其中一个逻辑被错误地翻转了。比如,X
的判断是正确的,但 Y
的条件顺序却存在问题。这种错误在实现稀疏化存储后可能会导致代码无法正常运行,因此需要及时修正。
此外,我们决定不再支持某些以前的功能,比如“坐标环绕”(wrapping),因为当前的存储设计还没有实现稀疏化。这一决定的背景是为了简化问题,并专注于让稀疏化存储工作起来。
回顾世界坐标环绕问题及稀疏世界存储设计
我们早期决定要做的只是设计要求。这确实有一些好处,使得我们能够灵活处理系统的不同方面。我们决定使用世界坐标来允许四十亿瓦的四十亿瓦,这样可以避免数字在超出范围时出现问题,例如在跨越负或四十亿瓦时。这种设计避免了环绕错误的问题,使得系统更稳定高效。我们选用了一种方法,并试验它,以确保系统的功能性。在最终的世界系统设计时,我们可能会更加深入地考虑如何优化和处理这些问题。
介绍哈希表
以下是上文简化的总结:
在游戏开发中,为了支持一个非常大的世界(如 40 亿 x 40 亿块瓦片),开发者遇到了以下问题:
- 存储限制:直接存储如此巨大的世界会占用过多内存,这是不可行的。
- 数字包装问题:当坐标超出某个范围(例如从零减到负数或从最大值“绕回”到零),可能导致逻辑错误,尤其是在边界条件计算中。
解决方法:
- 使用稀疏数据结构,如哈希表,将只需要的区域动态存储,而非整个世界。
- 这样可以避免分配过多的内存,同时让世界生成器自由生成局部区域,而无需关心整个世界的“全局”填充。
哈希表的工作方式:
- 它将大范围的坐标映射到更小的存储空间中,只存储实际需要的瓦片数据。
- 使用哈希函数将
(x, y, z)
坐标映射到数组索引位置。
最终目的是:
- 创建一个高效的世界坐标系统,避免大规模存储和边界逻辑错误,同时允许动态生成世界中所需的内容。
以下是简化的示例,说明如何使用哈希表来管理大规模世界中的瓦片数据:
示例:哈希表在大世界管理中的应用
-
问题背景:
- 假设我们有一个游戏世界,宽和高都为 40 亿(
40亿 x 40亿
)瓦片。 - 每个瓦片需要存储一些数据,如纹理、物理属性等。
- 直接存储整个世界将占用巨大的内存,无法接受。
- 假设我们有一个游戏世界,宽和高都为 40 亿(
-
使用哈希表的解决方案:
- 哈希表将世界坐标
(x, y)
转换为一个唯一的数组索引。 - 通过一个哈希函数将
(x, y)
坐标映射到数组中,比如:index = x * WORLD_WIDTH + y
- 这样,只有实际需要的瓦片数据才被存储。例如,当玩家接近某个区域时,我们只需要加载该区域的瓦片数据,而不是整个世界。
- 存在于哈希表中的瓦片数据可以动态生成和销毁,以节省内存。例如,玩家离开某区域,相关的瓦片数据可以被从哈希表中移除。
- 哈希表将世界坐标
-
哈希表的优势:
- 高效的存储管理:只存储实际使用的瓦片数据,大大减少了内存使用。
- 处理边界条件问题:当坐标超出正常范围时,哈希表会自动映射到数组中的合理位置,不会导致错误。
- 动态世界生成:世界生成器可以自由生成局部区域,不需要管理整个世界的“全局”填充。
这个哈希表方法在大规模游戏世界中,尤其是在需要处理动态生成和管理的大世界时非常有效。它不仅解决了存储和边界问题,还提高了游戏的性能和可扩展性。
哈希函数
我们可以通过从较大空间映射到更小的空间来使用哈希函数。这样可以使得存储和查找更加高效。哈希函数通常会将大量数据压缩成一个更小的表示,这样在查找时可以快速定位。尽管这并不是唯一的方法,但它利用了稀疏性和冗余,使得系统更高效。不同于直接存储所有数据,这种方法只需存储相关信息,这样可以节省内存。
哈希冲突
当使用哈希函数时,可能会发生所谓的“碰撞”。这意味着不同的输入数据(例如,x、y、z或a、b、c)会映射到相同的哈希槽。这种情况需要碰撞处理机制来解决。例如,可以使用链表法或开放地址法来处理这些冲突。链表法将相同哈希槽的所有碰撞数据链接在一起,而开放地址法则使用探测技术来寻找下一个可用位置。这些方法都能确保哈希表的完整性和高效性,即使发生了碰撞。
处理哈希冲突:外部链表法
当处理碰撞时,哈希表需要能够储存多个映射到相同槽的元素。通常,这种情况通过链表法来处理,其中哈希表的每个槽可以容纳多个元素的链表。这些元素通过指针链接在一起,以处理可能发生的冲突。例如,如果元素a和b都映射到同一个槽,但它们的实际坐标不同,当查找或存储时,哈希表需要能够区分这些冲突。链表法允许这些冲突的元素共享同一个槽,而不是覆盖掉另一个,这样可以有效地管理冲突并保持哈希表的完整性。
另一种方法是探测法,其中相同哈希槽的元素是直接放置在哈希表中的,而不是通过链表链接。这种方法通常称为“内部链式法”或“探测法”。在这个方法中,当新元素遇到冲突时,会查找下一个可用的位置。这种方法减少了链表的深度,减少了指针追踪的复杂性,但也可能导致更多的冲突处理。总的来说,处理碰撞是确保哈希表高效存储和查找的一个关键步骤。
处理哈希冲突:内部链表法
另一种处理哈希表碰撞的方法是使用内部链接法。在这种方法中,当发生碰撞时,不是将指针放在槽里,而是创建一个链来存储重复的数据。在查找时,系统会从第一个槽开始查找,依次检查下一个槽,直到找到匹配的项或者遇到一个空槽。在处理哈希表填满时,内部链接法能够有效减少碰撞的可能性,因为所有存储空间都用于哈希,而不像外部链接法一部分用于链条。这种方法的优点是碰撞后的查找效率较高,但随着哈希表接近填满,链条性能会更稳定。这使得在相同大小的存储空间中,内部链接法比外部链接法有更好的最坏情况性能。然而,这种方法也有一些缺点,比如在哈希表接近填满时,可能会有更多的探查次数,但整体上,它能提供更好的哈希表性能。
一种常见的例子是使用开链法(Chaining)来处理哈希表的碰撞。假设我们有一个哈希表大小为512的数组,每个位置代表一个槽(slot)。当插入一个新元素时,我们首先计算它的哈希值,并确定该值应该插入的槽位置。例如,假设我们计算一个元素的哈希值为h
,它应该放在哈希表的第h % 512
个槽内。
如果这个槽已经被其他元素占用了(发生碰撞),而不是覆盖该槽的值,开链法会在该槽处创建一个链表,将发生碰撞的元素链接到这个链表上。例如,如果在槽1中已经存在了一个元素x
,插入一个新的元素y
,它会被添加到这个链表的尾部。
查找操作也遵循类似的流程。假设我们要查找元素y
,首先计算它的哈希值并找到相应的槽。如果这个槽处没有元素(即为空),查找结束;如果该槽有元素,系统会遍历链表,依次检查链中的每个元素,直到找到y
或达到链表的尾部。
这种方法的优点在于:随着哈希表的填满,所有的存储空间都用于处理碰撞,而不是浪费于链条。虽然查找时可能会需要更多的探查次数,但整体上,在相同大小的存储空间下,开链法比开放地址法(Open Addressing)有更好的最坏情况性能。缺点是如果哈希表接近填满,链条的性能可能会减弱,因为需要遍历的元素会越来越多。
用哈希表替换瓦片块计数
我们想要使用外部链表法来处理哈希表中的碰撞。每当发生碰撞时,我们会把冲突的项存储在该槽的链表末尾。这种方法使得查找操作的时间复杂度稳定在 O(1) 的平均时间,但不影响存储空间利用率。
在实现过程中,我们假设有一个指针的结构,每个槽存储一个指向该区域所有相关块的指针。当需要扩展时,我们可以添加更多的块到同一个槽的链表中。通过这种方式,可以避免因哈希表接近填满导致的性能下降问题。
这种方法的优点在于查找操作效率高,不会受存储空间填满的影响,但同时存储空间的利用率可能较低,因为每个槽都可能包含多个元素的链表。
总的来说,外部链表法通过在碰撞时扩展链表来处理冲突,避免了填满导致性能下降的问题,同时查找和插入操作的时间复杂度始终稳定。
创建哈希函数
我们设计了一个哈希表来管理子块。首先,我们计算一个哈希值来确定子块的位置,然后用这个哈希值作为索引存储子块。为了确保这些存储的子块都在合理的范围内,我们通过减去1,使得索引值变成2的幂,从而确保存储的子块在数组大小的合理范围内。我们还断言这些子块不能位于世界的边界,以避免误操作。最终,通过遍历所有子块,找到匹配的子块,以确保找到符合条件的子块。
这段代码的设计意图是通过合理的索引和边界条件来确保子块管理的有效性,同时避免不必要的边界处理和错误的哈希碰撞。
// 计算哈希值,用于确定子块的位置
uint32 HashValue = 19 * TileChunkX + 7 * TileChunkY + 3 * TileChunkZ;
在这一行中,哈希值是通过将 TileChunkX
, TileChunkY
, 和 TileChunkZ
这三个坐标乘以不同的系数并相加得到的。这些系数(19, 7, 3)用于混合这些坐标,生成一个唯一的哈希值,这有助于在哈希表中找到对应的子块。
1. uint32 HashSlot = HashValue & (ArrayCount(World->TileChunkHash) - 1);
- 作用:将
HashValue
的值与(ArrayCount(World->TileChunkHash) - 1)
进行位与运算,确定子块的索引在哈希表中的位置。 - 原因:由于
ArrayCount(World->TileChunkHash)
的值通常是一个2的幂,这使得-1
是一个掩码,可以快速地将HashValue
限制到数组的有效范围内。这种方法避免了对HashValue
进行复杂的取模运算,提升了性能。 - 示例:如果
ArrayCount(World->TileChunkHash) == 4096
, 那么(ArrayCount(World->TileChunkHash) - 1) == 4095
, 在二进制形式上它相当于一个全1的掩码。HashValue & 4095
可以将HashValue
限制到一个小于 4096 的有效索引。
2. Assert(HashSlot < ArrayCount(World->TileChunkHash));
- 作用:确保计算得到的
HashSlot
索引是有效的,即不超出哈希表的大小。 - 原因:这是为了保证程序不会试图访问不存在的索引,从而避免可能导致崩溃的行为。
3. tile_chunk *Chunk = TileChunkHash + HashSlot;
- 作用:通过
HashSlot
索引从哈希表TileChunkHash
中获取子块指针。 - 过程:这一步从
TileChunkHash
数组的首元素开始,跳跃HashSlot
个位置,找到对应的tile_chunk
。这个操作将子块定位到其可能存在的位置。
在哈希表中查找条目并创建新条目
我们从开始,我们首先通过计算子块的哈希值来确定它在哈希表中的位置。我们计算哈希值并通过位与运算将其限制在哈希表的有效索引范围内。接下来,我们检查是否存在匹配的子块。如果找到了匹配的子块,我们就返回该子块;否则,我们需要创建一个新的子块并将其插入到哈希表中。我们还考虑了内存分配器的传递,如果它被传递,那么将使用它来创建新的子块。这种方式使得即使我们找不到匹配的子块,我们也可以在需要时创建一个新的子块。这样一来,即使在重新定位时也能高效地管理和使用这些子块。
回顾已编写的代码
这段文本描述了一个复杂的编程逻辑,可能是为游戏世界或类似系统实现自定义内存管理或网格管理。以下是一些关键点总结和优化建议:
核心逻辑
-
边界检查与非法区域防护:
- 通过边界值判断防止计算超出合法区域范围。
- 可设置较大安全边界(如16或更高)以避免越界。
-
哈希表操作:
- 使用混合计算生成索引并映射到哈希表。
- 确保无效或未初始化的槽位得到正确处理。
-
链表分配与扩展:
- 在链表末尾动态分配新块(chunk),并初始化其状态。
- 确保每个块的指针正确指向下一块或结束。
-
初始化与清除:
- 对整个数据结构进行清理(如将所有槽位设置为0)。
- 提供独立初始化函数以确保状态一致性。
-
条件逻辑与分支:
- 根据当前块的状态决定是跳到下一块还是创建新块。
- 确保分支条件处理所有可能情况。
改进建议
-
代码简化与结构优化:
- 提取重复逻辑到单独函数(如处理未初始化槽位或链表扩展)。
- 使用更加清晰的变量命名,减少上下文切换造成的理解障碍。
-
日志与调试支持:
- 添加日志记录或断点以验证分支逻辑正确性。
- 在复杂逻辑前后打印调试信息,帮助分析行为。
-
边界与异常处理:
- 针对可能出现的边界条件(如空链表、无效索引等)增加断言或异常抛出。
-
内存管理:
- 如果使用动态分配,确保正确释放内存,避免泄漏。
-
性能优化:
- 针对哈希表冲突或链表扩展的高频操作,评估性能瓶颈。
- 使用更高效的数据结构(如跳表或平衡树)代替简单链表。
若需要进一步的代码实现优化或详细解读,请提供更具体的上下文信息。
移动世界原点
我们目前的任务是通过从世界的中心开始构建世界来恢复结构化的平铺系统。断言的触发是预期的结果,因为到目前为止,我们还没有尝试将世界移动到指定的位置。世界的原点虽然已经通过初始化,但目前仍然是零。
为了从中心开始构建,我们需要明确中心的位置。假设我们的世界维度是固定的,例如30到最大值范围,我们通过将其除以2来确定中心坐标。这些中心坐标不仅用来初始化世界的构建,还用于设置相机的起始位置。这样,相机会与世界的中心保持一致,而不是默认从零开始。
调试丢失的实体
我们目前正在解决一个显示实体的问题。目标是确保屏幕上正确显示游戏中的实体,而目前的系统并未按预期工作。接下来的工作主要集中在调试和修复一系列计算和逻辑上的问题。
为什么不将世界中心设在 (0,0,0)
并使用有符号整数表示瓦片位置?【代码修改】
确实是没看懂为什么要这样改
哈希表是否本质上只是一个创意形状的瓦片块?如果是这样,是否可以通过从 Z 坐标贡献更多位而不是 X 和 Y 来提前优化哈希函数,例如在tile地图的情况下?我的思路对吗
我们在这里深入讨论了哈希映射的本质及其应用,内容总结如下:
1. 哈希映射的基本概念:
- 哈希映射的核心是一个函数,该函数接收任意数量的输入位,经过一定的处理后,输出固定数量的位。
- 输出的固定位可用于索引一个线性数组,这种方式极大地加速了数据查找的过程。
2. 哈希映射的工作原理:
- 输入数据(例如字符串)会通过哈希函数映射到一个固定范围内的值(例如 0 到 27),这个值作为数组的索引。
- 每次查询时,只需直接访问对应槽位即可,大多数情况下这是准确的。
- 如果槽位中存在冲突(多个元素映射到同一索引),则通过链式存储或其他方式处理冲突。
3. 哈希映射的优势:
- 高效性:相比于直接遍历所有数据,哈希映射可以显著减少查找时间复杂度,从线性时间 (O(n)) 降低到接近常数时间 (O(1))。
- 扩展性:适用于各种数据类型,例如字符串、数值等,只需设计合适的哈希函数即可。
4. 具体应用案例:
- 存储词典:假设需要判断一个单词是否存在于词典中,单词可以是任意长度的字符串。通过哈希函数将这些字符串映射到固定范围内的索引,就能快速判断其存在性。
- 映射逻辑:例如,可以将字符串中的字符加总后乘以某个常数,得到一个固定长度的输出位,用于查询哈希表。
5. 注意事项:
- 哈希函数需要尽可能均匀地分布输入数据,避免过多的冲突。
- 冲突处理方法如链式存储虽然会引入额外的操作步骤,但大多数情况下,哈希映射的效率仍然优于直接遍历。
6. 总结:
- 哈希映射不是固定的几何形状,而是一种高效的数据查找和映射方式。
- 它的目标是将任意输入映射到固定范围内,从而快速定位到目标数据。
- 无论是用于词典查找,还是其他数据管理,哈希映射都是一种灵活且强大的工具。
对于“free”优化,在早期或晚期开发中,例如尝试尽早失败的优化,你怎么看?最理想的情况下只进行一次比较而不是四次。
在讨论程序开发中的优化问题时,提到了在 if
语句 或类似逻辑判断的优化中,如何进行早期失败(early fail)以减少计算步骤的思考。以下是总结的要点:
1. 逻辑运算符的短路规则
- 在大多数编程语言(例如 C 语言)中,**逻辑运算符(
&&
和||
)**已经内置了短路计算的规则:&&
(逻辑与):当一个条件为false
时,后续条件无需计算,直接返回结果。||
(逻辑或):当一个条件为true
时,后续条件无需计算,直接返回结果。
- 这种短路特性确保了无需显式优化条件的顺序,编译器会根据逻辑运算符的性质自动实现早期失败。
- 如果按照原本的顺序直接书写条件,编译器在生成代码时会根据逻辑优化路径,不会产生额外开销。
2. 编写顺序与性能差异
- 当考虑对逻辑条件进行显式优化(如调整判断顺序以减少计算量)时,通常并不需要在代码编写阶段手动处理,因为编译器已经在内部完成了相关优化。
- 举例:
一个判断是否在矩形内部的逻辑:
编译器会自动根据逻辑短路规则,避免不必要的条件判断。if (x >= left && x <= right && y >= top && y <= bottom) { ... }
3. 是否在开发早期优化逻辑条件
-
开发早期:
不建议在编写代码时过早关注这些逻辑条件的性能优化,原因包括:- 复杂性:逻辑判断的性能受多种因素影响,例如 CPU 的分支预测、缓存命中率等,难以仅通过代码分析得出最优方案。
- 误判风险:开发阶段优化可能导致代码更复杂,但最终性能未必提升,甚至可能恶化。
- 编译器优化:现代编译器已经具备足够智能的优化能力,能够根据运行环境决定最优的计算顺序。
-
性能优化阶段:
只有在需要对代码性能进行细致分析(如通过性能剖析工具)时,才需要关注条件的排列顺序。- 优化的重点在于:
- 使用更高效的数据结构。
- 减少分支跳转。
- 提升分支预测成功率。
- 优化的重点在于:
4. 性能优化的原则
- 以测量为准:在优化前,先通过性能分析工具(Profiling)确定代码的性能瓶颈。
- 专注于关键路径:仅优化对性能影响最大的代码路径,而非所有逻辑判断。
- 动态调整:逻辑优化可能因运行环境的不同(如 CPU 架构、缓存大小等)而需要调整。
5. 总结
开发早期的代码编写不应过度关注逻辑判断的顺序优化,这部分工作应留到性能优化阶段。在优化时,应以性能分析数据为依据,结合具体场景和编译器的特性进行合理调整。优化的重点应放在整体算法的改进和数据结构的优化,而非单一的逻辑判断顺序。
你怎么看把哈希表代码从瓦片代码中分离出来,这样可以以后将哈希表用于其他内容?
在编程中,有时会考虑将代码分离,比如将哈希表的相关代码从某个具体上下文中抽取出来,以便在其他地方复用。这种做法虽然具有一定的吸引力,但也可能带来很多限制和问题。
首先,哈希表的核心逻辑通常是高度定制化的。例如,比较函数可能因具体需求而有所不同,有时甚至非常复杂。这意味着即使抽取出某些通用逻辑,也需要针对每种具体情况单独编写比较函数,而这部分代码无法省略。
其次,哈希表的其他设计细节,比如内部或外部链接的选择,以及数据结构如何标记空闲或已占用状态,往往与具体实现密切相关。这些都是哈希表结构的内在要求,很难抽象成通用模块。试图将这些逻辑统一抽象,可能反而增加复杂性,降低代码的效率。
此外,从效率的角度来看,与其花费额外精力去设计抽象系统,不如直接为每种特定需求编写相应的哈希表代码。这样既可以完全控制代码逻辑,又能根据需求进行优化,避免因抽象而带来的性能损失。
总结来说,与其试图将所有的逻辑强行抽象到一个通用框架中,不如根据具体需求直接实现定制化代码。这样不仅可以更好地满足特定场景的要求,还能提高代码的可维护性和效率。
世界是即时生成还是在游戏启动时生成?
我们讨论了关于游戏世界的生成和模拟问题。在游戏开始时,世界可能会被重新生成。这种生成过程可以在游戏运行期间动态进行,也可以在游戏重新启动时完成。目的是为了创建一个更加复杂和动态的世界,使得游戏中的角色和物体能够自由地从一个远离玩家的位置移动到另一个位置,并传递信息,进而影响世界的变化。
这种复杂的世界结构需要在整个游戏世界范围内存在,而不仅仅是局限于玩家附近的区域。这意味着世界的生成不再是局部化的,而是需要考虑到整个游戏场景的动态和变化。世界应当在高频率更新的地方存在,例如玩家经常活动的区域,而在低频率更新的地方存在,例如较为远离玩家的区域。这种方式能够确保整个游戏世界的动态性,即使在玩家不在的地方,世界也会继续发生变化。
因此,在玩家开始游戏之前,整个世界必须预先生成。这是为了确保游戏能够在任何时候都能流畅地运行,不会因为玩家的移动而导致世界生成和模拟的延迟。这样的设计使得游戏中的所有元素都能够在游戏启动时便准备就绪,从而提供更加沉浸的游戏体验。
是否考虑过使用空间哈希代替瓦片块?我在考虑为我的游戏使用它,以支持开放的生成世界而不是固定网格对齐的瓦片或固定大小的对象。
我们讨论了游戏世界生成方式的变化。传统的生成方式主要依赖于分块(chunks)这种方法,即将世界划分为多个固定大小的区域来管理。然而,我们正在探索一种新的方式,不再仅限于这种分块方法。我们希望能够让玩家和固定大小的物体不受网格对齐的限制,这意味着这些元素可以在不严格的网格对齐的情况下自由地放置。
这代表了一个新的空间散列的概念。与传统的分块方法不同,新方式将实体按需排序,而不是依赖于固定的块状或瓦片开始的智能。这种做法意味着我们不再只处理特定的区域,而是更灵活地对整个世界进行管理,动态地调整物体的位置和行为。这是我们的下一步计划。虽然我们已经开始实现这种新的方式,但还没有完全实现所需的效果和面积,这意味着仍需进一步优化和调整来达到理想的状态。
对于稀疏存储,为什么不用八叉树代替哈希表?
在讨论使用不同存储结构时,我们比较了传统的散列映射和一种基于树的稀疏样式存储。散列映射通常更高效,因为它允许直接访问,并通过少量的比较找到所需的信息。这种结构非常适合于快速检索和数据管理,因为它能够在很短的时间内找到对应的元素。
另一方面,基于树的稀疏样式存储(如一棵树)虽然可以有效地分割物理空间,并存储细致的低层次信息,但可能会浪费计算资源。原因在于树的结构使得低层次的检索需要进行多次分支,增加了查找的复杂性和时间开销。例如,一棵不完全填充的树可能会将信息存储在更高的细节层次上,而这些信息并不经常需要进行检索。使用这种结构可能不太适合于低频访问的数据检索。
因此,尽管树可以提供更高层次的信息管理,散列映射在处理频繁检索的情况下可能会更具优势,因为它直接访问,并进行少量的比较来查找答案。这种方式更高效,适用于需要快速数据检索的场景。
既然所有内存分配都使用了内存分配区域,如何处理动态数量的对象,比如敌人、粒子等?
我们讨论到,由于使用的是所有分配的Arena(内存池),因此如何处理动态数量的元素,例如敌人、粒子等。通常,我会使用循环缓冲区来管理这些元素。这种方法使得我们能够在内存内动态分配和释放这些对象,避免频繁的内存分配和释放操作,从而提升性能。这对于那些具有显著动态数量的游戏元素来说,是一种非常有效的管理方式。
字母链表的解释是不是一个字母指向下一个字母,直到单词结束?
我们讨论了处理字符串的方法。这里的处理方式并不是像字典那样使用指向下一个字母的指针,而是通过将每个字母的 ASCII 值加起来,然后把它们合并成一个唯一的二进制数字。这个过程生成了一个唯一的哈希值。随后,这个哈希值会被截断到八位,进入一个包含二百五十六项的表格中进行查找。这种方式不会保留字母之间的指向关系,也不像树结构那样存储指针,它仅仅通过哈希表来进行字符串的存储和查找。如果有多个字符串碰撞,这些碰撞的字符串会通过一个相对的指针指向下一个条目,而不是保留指向它们的字母指针。这种方法与传统的树结构不同,因为它不依赖于保留指向关系,而是通过哈希来进行高效的查找。
你提到过哈希表只是存储稀疏数据的多种方法之一,能否简要提及其他方法?
我们讨论了存储CRF数据的一些方法,除了传统的哈希表外,还有很多其他有效的存储方式。
-
(Archetype):这种方法将整个数据空间分成越来越小的立方体,并在每个层级中存储数据。这种递归的分割方式使得查询时只需要处理那些实际填充数据的部分,而不是遍历整个世界。这种方式非常适合存储稀疏数据,优化了内存使用。
-
(Quad Trees):它将整个数据区域分成四个等分,适用于二维数据结构的存储。例如,在地图生成或碰撞检测中,四棵树可以有效地管理区域间的联系和碰撞检测。每个子区域可以进一步分割,直到所有区域包含实际数据。
-
kd树(K-d Trees):这种方法通过按维度进行分割,每次将数据分成两部分,适用于高维数据的存储。每次分割都根据数据的某个维度进行,形成一个多维的树结构。kd树可以高效地支持多维查询,尤其是在空间检索中表现优越。
-
运行长度编码(Run Length Encoding):这种方法只存储那些有数据的区域,而忽略整个世界中的空区域。这在处理大量连续空白数据时非常有效,可以节省内存。它记录了数据的实际填充和非填充区域,不会浪费内存。
从零开始是否会对瓦片块造成问题,因为现在我们使用零作为未初始化的变量?【代码修改】
我们遇到的问题是,当从零开始初始化变量时,会导致TileChunk问题。在这样的情况下,我们应该将初始化变量定义为一个超出安全边际的值,而不是零。这种方式可以避免在检查过程中出现零的问题,确保数据的正确性和系统的稳定性。这种调整后,我们的初始化将不会受到匆忙操作的影响,从而避免潜在的调试问题。