游戏引擎学习第280天:精简化的流式实体sim

发布于:2025-05-16 ⋅ 阅读:(18) ⋅ 点赞:(0)

回顾并为今天的内容做铺垫

今天的任务是让之前关于实体存储方式的改动真正运行起来。我们现在希望让实体系统变得更加真实和实用,能够支撑我们游戏实际所需的功能。这就要求我们对它进行更合理的实现和调试。

昨天我们基本让代码编译通过了,但实际上还没有进行调试。尽管如此,我们还是忍不住修复了一些小问题,使得程序至少能正常运行,不会崩溃。虽然目前程序运行起来了,但并没有按照预期的逻辑工作。

接下来的任务,就是系统地调试和验证这些更改是否生效,确认实体的管理系统能够按照我们设想的方式处理数据。因为我们正在从一个简化版本向更真实、可扩展的架构过渡,所以现在这个过程不仅仅是修 bug,更是确保整个系统框架能够支持未来游戏中的复杂需求。我们需要做大量工作来一步步推进这个目标。

game_world.cpp 中查看 PackEntityIntoChunk 函数里 *Source 的大小

这个问题听起来正是我所预期的那种问题,尤其是在我们进行数据打包时。这很可能是由于我们在打包时使用了错误的大小,具体来说是在计算 size of source 时出现了错误。我们应该计算的是实际打包的实体大小,而不是指针的大小。所以正确的写法应该是这样。

目前我们并没有真正进行打包操作,而只是将实体直接以块的形式复制到世界的区域块中。这种方式只是暂时使用的,我们打算在以后真正关心这些实体的时候,再来进行压缩操作,因为最终这些区域块会变得非常大,直接复制的方式就不再合适了。

总的来说,这里确实存在一个 bug,需要修正来确保我们计算的是正确的实体大小,而不是指针的大小。

在这里插入图片描述

在这里插入图片描述

如果是指针会有警告提示

在这里插入图片描述

game_sim_region.h 中将 StorageIndex 重命名为 ID

另一个问题是关于“stored index”(存储索引)。目前的存储索引有些混乱,实际上我们不再需要称其为“存储索引”,而应该称之为“ID”。之前有个概念是关于存储索引的,但这个概念已经不再适用。因此,存储索引这个名称本身是错误的,我们还没有完全修正它。

为了避免调试一个不太正确的东西,决定再做一次修改,将其更正为“ID”。从现在开始,我们将会明确使用“实体ID”,而不是之前的存储索引。在代码中,所有涉及到“存储索引”的地方,都应该被替换为“ID”。例如,在存储实体引用时,我们实际上要获取的是实体的ID,而不是存储索引。同样,当我们传递这些数据时,处理的也是实体的ID。

接下来,我们将检查代码中所有使用“存储索引”的地方,并将其更改为ID。这个过程包括修改与获取ID相关的函数,比如“get hash from ID”,这个函数也需要重新命名,确保符合我们新的命名规范。
在这里插入图片描述

在这里插入图片描述

其他地方依次修改

game_sim_region.cpp 中将 GetHashFromStorage 重命名为 GetHashFromID

现在,我们在添加实体时,不再需要传递存储索引了。在添加实体的过程中,我们可以直接传入实体的ID,而不再使用存储索引。这样,我们可以简化代码,避免使用不必要的参数。

实际上,考虑到未来的解包操作,我们可以在这个过程中直接传递实体的位置或其他相关信息。这意味着我们在处理实体时,将不再依赖存储索引,而是直接使用实体的ID。这样做有助于让代码更简洁,避免冗余的操作。

game_sim_region.h.cpp 中移除 EntityFlag_Nonspacial 并继续清理相关代码

我们决定完全去掉“非空间”标志,并将相关的逻辑进行简化。以前,我们可能会检查一些关于是否是“非空间”实体的条件,但现在这些都不再需要了,因为所有的实体默认都将是空间实体。因此,之前涉及到“非空间”标志的代码将被移除,我们不再需要检查或处理这些情况。

同时,实体ID的处理也变得更加直接和简洁。之前我们可能会传递冗余的信息(例如ID和源对象),但现在只需要从源对象中提取ID即可。

在处理碰撞检测时,我们将不再关心“非空间”标志,因为所有实体现在都是空间实体。因此,碰撞规则中相关的检查和逻辑也会有所简化。以前需要的那些条件判断(例如“非空间”是否设置)将被移除,所有的逻辑将直接基于实体的ID进行处理。

最终,存储索引也会被替换为ID,这意味着所有涉及存储索引的地方都会改成使用ID。这些改动使得代码更加简洁和一致,减少了不必要的冗余和复杂性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_world_mode.cppgame_entity.h 中移除“剑”(Sword)实体

我们决定彻底移除“剑”(Sword)这个实体类型。之前我们通过设置“非空间”属性来处理它,但现在既然“非空间”已经被移除,而且这个实体类型也不再需要,所以它的所有相关逻辑都会被清理掉。包括在实体类型定义中的“剑”也会被删除,在逻辑判断中唯一排除“剑”的地方也会随之简化,比如“是否阻挡碰撞”的判断,原先除了剑其他实体都返回 true,现在可以统一为始终返回 true。

同时清理了很多早期用来测试和临时搭建系统的旧代码,那些冗余字段和判断已经不再需要,因为我们已经不再保留那些特殊处理。之前判断是否设置了“非空间”标志的代码将被简化为只检查“是否可移动”,因为所有实体现在都是空间实体。

为了避免逻辑错误,我们还仔细检查了实体之间的碰撞规则系统,确保移除“非空间”标志后,默认的碰撞处理仍然合理。碰撞默认视为允许,除非有明确的规则指明不可碰撞,这种处理逻辑保持不变。

之后我们尝试运行程序,发现红屏,表示没有活跃的英雄实体。进入调试后确认,英雄实体确实已经创建出来,只是没有被正确地显示。进一步调查发现实体在创建和初始化时 ID 被设置后又被清零,原因是初始化结构体和设置 ID 的代码顺序不对。于是我们把设置 ID 的语句放到结构体清零操作之后,确保 ID 能正确保留。

尽管现在英雄实体已经被系统识别并创建,但仍然没有显示。我们推测问题出在实体的打包和显示逻辑上。于是进入游戏世界模式模块,查看实体的创建和打包流程,确认实体 ID 设置是否及时、打包是否正确执行。同时也验证是否是摄像机视角出错或显示坐标偏移的问题,例如检查 debug 摄像机是否能看到周围场景。结果显示附近完全没有实体,说明问题比简单偏移更严重。

此外,我们保留了一个断言用于标记删除功能尚未实现。接下来计划在 bug 修复后,第一时间实现实体的删除机制。这个机制将通过“流出”操作实现:被标记为删除的实体在打包导出时不再被写入,借此完成实体的生命周期终结。

总之,目前我们已经完成了一轮清理工作,包括移除无效实体类型、简化标志判断逻辑、修正实体初始化顺序,系统结构更加清晰。接下来将聚焦在调试实体显示相关问题,并进一步实现删除机制以完善整个实体系统的生命周期管理。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_world.cppPackEntityIntoWorld 中添加储存实体位置的逻辑

我们开始检查空间相关的位置逻辑,准备调试整个仿真流程中实体的创建与初始化,弄清楚我们到底创建了什么,以及在模拟区域开始运作时实际发生了什么。

BeginLowEntityEndEntity 这类实体构造函数中,我们会传入一个 world position P,它表示实体的初始位置。随后我们调用 PackEntityIntoWorld 函数,这个函数会将实体注册到游戏世界中。

PackEntityIntoWorld 首先会通过传入的位置 P 从世界中获取一个 WorldChunk,如果该位置对应的 chunk 不存在,就会新创建一个。我们可以确认这点:函数调用 GetWorldChunk 并传入 P,这个逻辑保证我们一定会得到一个 chunk。在获取到 chunk 后,调用 PackEntityIntoChunk 把实体实际打包进这个 chunk 的数据结构中。

但是这里存在一个明显的问题:在打包实体的过程中,我们并没有把实体的位置信息(例如 chunkP 或者其偏移量)记录下来。也就是说,我们把实体放进了世界的某个块里,但实体本身并没有任何字段反映出它是在这个位置创建的。

要解决这个问题,我们需要在打包时将实体的空间位置信息同步写入其自身结构中。当前我们还没有正式实现实体内部的完整数据结构(比如各种字段如何组织),但作为临时修复措施,我们应该在 PackEntityIntoWorld 中为实体设置 chunkP 值,让实体知道自己所在的世界块位置。

偏移量 p(实体在 chunk 中的具体偏移)暂时可以忽略,因为未来在解包时会重新计算。而我们也确认了解包过程确实会读取 chunkP:在模拟区域代码中调用 AddEntity 时,会执行 GetSimSpaceP,它会读取实体的 chunkP,从而获取实际的世界坐标。

这说明,如果我们不在打包时设置 chunkP,那么解包阶段将无法获得实体的正确位置,也就无法在正确位置显示实体,这正是我们当前无法看到任何实体的原因之一。

总结:

  • 实体在创建并打包进世界中时,缺少关键的空间位置字段更新,导致后续模拟与渲染失败;
  • 应立即在打包函数中记录实体的 chunkP,以确保后续能够通过该信息正确反解出实体的世界位置;
  • 偏移部分暂时可以由解包阶段自动处理;
  • 这一修复是当前调试工作的关键节点,有望解决实体“存在但不可见”的问题。

接下来将继续深入调试与修复,完善实体系统的空间一致性管理。
在这里插入图片描述

使用调试器步入 BeginSim,发现最初打包阶段没有实体被输出

我们当前依然无法在画面上看到任何实体,为了进一步调试问题,我们准备从模拟的首次执行入手,观察实体解包是否正常发生。

首先设置断点,查看 BeginSim 函数第一次运行时是否能成功获取到任何实体。运行结果显示 没有任何实体被解包成功,这表明我们摄像机所观察的区域可能并不包含任何实体,或者实体的打包过程出现问题,导致其根本没被放置到摄像机视野范围内。

这引发我们去验证摄像机的位置初始化是否正确。根据初始化代码逻辑,在创建世界模式时,会将摄像机位置设为某个新建点 newCameraP,理论上该位置应指向实体所在位置。但现实是摄像机视角下仍为空,暗示可能有两个潜在问题:

  1. 摄像机初始化逻辑未能将其定位在实体所在区域
  2. 实体被打包进世界时的位置和期望位置不一致,导致其与摄像机视角不匹配

为此,我们进一步确认了摄像机的初始化代码位置确实存在,并在创建世界模式时被调用。同时,打包实体时所用的 PackEntityIntoWorld 代码也看上去是正确的——它通过映射世界坐标到 chunk 空间,并在需要时创建 chunk,确保实体被加入。

接下来决定进行更细粒度的调试:

  • 进入 PackEntityIntoWorld 的具体过程,逐步检查是否实体实际被加入到世界中
  • 检查 MapIntoChunkSpace 函数,确认从世界坐标到 chunk 坐标的转换是否如预期;
  • 确认打包时实体的 chunkP 位置确实被设置;
  • 检查模拟区域 BeginSim 时是否正确选择了对应 chunk 区域进行解包;
  • 检查摄像机位置是否真的覆盖了实体所在 chunk 块。

当前判断是:实体可能确实被打包了,但摄像机没有落在它们所在的 chunk 上,或模拟系统没有从正确的位置开始检索实体,导致模拟区域为空。这将直接造成主画面无实体显示(即红屏空白场景)。

总结当前发现的问题与计划:

  • 模拟开始时未能解包任何实体,说明位置或检索逻辑存在问题
  • 摄像机理论上是对准初始实体的,但实际视角下并没有任何实体
  • 需要对打包、chunk 映射、摄像机设置及解包流程进行单步跟踪,定位是实体未被正确放置,还是未被正确检索;
  • 一旦找出实体未被处理的根因,就可以修复并推进后续逻辑,如实体可视化与交互逻辑的验证

在这里插入图片描述

在这里插入图片描述

还是没有

在这里插入图片描述

遇到几个bug

在这里插入图片描述

在这里插入图片描述

使用调试器步入 PackEntityIntoWorld,并追踪代码直到 RemoveWorldChunk

这段内容描述的是调试一个游戏引擎或仿真系统中的实体打包(pack entity)到世界(world)逻辑的过程,尤其关注实体在空间中的放置、内存块管理以及与摄像机视图的匹配关系。下面是详细中文总结:


实体打包(Pack Entity)流程验证

我们从初始化开始,首先执行了一个 add_standard_room 的过程,它会遍历一些 ABSTile 的 X 和 Y 坐标,看起来这些坐标都离 (0, 0, 0) 很近,因此我们传入的实体坐标也是 (0, 0, 0),这本应是一个处于摄像机视野范围内的点,因此在理论上应该能看到这些实体。


创建 Chunk 并打包实体

接着,我们查看了打包到世界(pack_entity_into_world)时的细节流程:

  1. 请求 Chunk 坐标 (0,0,0)

    • 这是一个合理的位置,应该可以找到或创建对应的 chunk。
  2. 初始化实体并进入 pack_entity_into_chunk:

    • 初次创建时,没有任何已存在的内存块(block),因此创建一个空的 block,并将其附加到该 chunk。
    • 清空 block 中残留内容(可能是调试垃圾),以准备使用。
  3. 检查 block 空间是否足够放实体数据:

    • 此时由于我们刚刚新建了 block,因此空间是足够的。
    • 设置目标指针(dest),并将其转换为实体指针结构(类型转换),写入实体信息(例如 ID 为 1,位置为 (0,0),类型为地板 Floor),这些都符合预期。

验证已存在 chunk 中的打包逻辑

我们继续查看打包第二个实体时的流程,即往已存在 chunk 中追加实体:

  1. 命中已有的 chunk:

    • chunk 中已存在一个实体,此时尝试往同一个 chunk 中插入第二个实体。
    • 通过类型转换观察内存块中已有的实体数据,验证插入不会覆盖已有数据,而是追加在末尾。
  2. 打包完成后验证:

    • 最终 block 中成功拥有两个实体,一个原始实体、一个新插入的实体。打包过程成功,数据也没有错乱,内存管理正常。

检查 BeginSim 中读取实体失败问题

虽然实体已经成功打包进 chunk,接下来进入 BeginSim 流程想查看是否能读取这些实体:

  1. 断点检查遍历到 (0,0,0) chunk:

    • 我们知道之前实体打包在 (0,0,0) 中,因此在模拟区域遍历时,应该访问到这个 chunk。
    • 确认 get_world_chunk_internal 正确返回了我们要的 chunk,指针也匹配,并未进入 fallback 分支,说明匹配正常。
  2. 问题是未能正确读取或识别 chunk 中实体:

    • 表明问题可能不是实体没打包进去,而是后续模拟器(Sim Region)未能正确从 chunk 中解包这些实体,或实体未能满足某种条件导致被跳过。

总体流程逻辑总结:

模块 说明
add_standard_room 添加标准房间实体,位置接近原点 (0,0,0),摄像机应该能看到
pack_entity_into_world 负责将实体打包进世界的某个 chunk,位置正确,chunk 会被创建或复用
pack_entity_into_chunk 实际完成实体数据写入,内存块分配、地址偏移、数据拷贝都执行正常
begin_sim 启动仿真区域模拟,尝试加载周围 chunk 中的实体,但未正确读取出来

初步怀疑问题原因:

  1. Sim Region 中未正确解包 chunk 中实体

    • 可能实体没有被加入模拟区域,或是被某些筛选逻辑跳过。
  2. 摄像机位置或 Sim Region 定义不正确

    • 虽然摄像机理论上应处于原点附近,但可能计算或对齐逻辑有偏差。
  3. 实体缺少必要标志/类型信息,导致未识别为有效实体

    • 例如没有设置位置或标志位错误。

后续建议

  • 查看 begin_sim 中实际提取 chunk 内部实体的逻辑:

    • 是否正确从 chunk 的 block 中提取所有实体。
    • 是否因为某些判断(如位置、可见性、类型)被忽略。
  • 核查摄像机实际视野范围与 sim region 的构建逻辑:

    • 确保摄像机视野确实覆盖到我们打包实体所在 chunk。
  • 打印 debug 日志或断点观察:

    • unpack_entity_from_chunk() 是否被调用,chunk 中是否有实际数据被读出。

如果你需要,我可以继续帮助你分析 begin_sim 中的遍历逻辑或 sim region 创建是否有偏差。欢迎贴代码片段进一步分析。

game_world.cppPackEntityIntoChunk 中增加 Chunk->EntityCount 的计数逻辑

这段内容详细讲述了在进行实体打包(pack entity)和仿真区域(Sim Region)处理时,发现了一个关键性的问题,并对问题的原因及其后果进行了分析与说明。下面是中文详细总结:


问题背景

在调试过程中,发现仿真代码没有正确识别出世界中已有的实体,导致逻辑仿真部分一直认为没有任何实体存在。经过逐步追踪,最终定位到了问题的根源。


问题发现过程

  1. 观察实体数量的残留值:

    • 在世界数据中发现了一个“残留值”,这个值表示某个 chunk 中包含的实体数量(chunk 内实体计数器)。
    • 当前系统逻辑中仍然依赖这个值来判断某个 chunk 是否包含实体。
  2. 打包实体时未更新实体计数器:

    • 每次将实体打包进 chunk 的过程中,虽然实体本身打包成功,但并没有相应地更新 chunk 的实体数量计数器。
    • 这就导致后续在遍历世界或构建 Sim Region 时,判断该 chunk 是否为空时得到错误结论(因为计数为 0,会认为没有实体)。
  3. 这个计数器目前仍被依赖使用:

    • 虽然将来可能会改成流式处理(streaming),不再依赖计数器,但目前系统中相关逻辑仍然依赖它来判断一个 chunk 是否有实体。
    • 如果这个值不正确,那么即使实体存在,也会被错误地认为不存在,从而跳过处理。

问题根因总结

问题点 说明
chunk 的实体计数器未被正确维护 在 pack_entity_into_chunk 时没有递增该值,导致系统认为 chunk 是空的
当前逻辑依赖该值判断 chunk 是否有效 用于决定是否进入仿真区域或是否需要进一步处理
打包逻辑本身成功,实体也写入了内存 但因为缺少对计数器的维护,仿真系统逻辑层无法“看见”这些实体

对后续系统影响

  • 仿真失败或缺失:
    系统在 BeginSim 或实体遍历时会跳过这些 chunk,造成逻辑漏洞。

  • 摄像机视野可能误判为空:
    因为系统误以为这些 chunk 中没有实体,即使摄像机位置正确,也不会渲染或处理这些数据。

  • Debug 较难察觉:
    内存中实体确实存在,但因为状态变量未更新,导致问题非常隐蔽。


解决方案

  • 每次打包实体时,必须显式递增 chunk 的实体计数器
  • 检查所有修改 chunk 内容的路径,确保在更新实体数据时,同时更新 chunk 的元数据(如实体数量、状态标志等)。
  • 后续如果系统迁移到不依赖计数器的设计(如纯粹基于流式遍历),则可删除该依赖。

可能的后续验证方法

  • 在每次 pack 之后打印或断点查看该 chunk 的实体计数值,确保正确递增。
  • 在构建 Sim Region 或 BeginSim 前后,打印系统实际获取到的实体数量以核对。
  • 使用自动化测试验证 chunk 中实体是否被系统逻辑正确“看见”。

这个问题虽然隐蔽,但定位清晰,一旦修复能有效恢复实体仿真逻辑的正确性。后续优化中建议尽量减少对显式状态变量的依赖,改为实际遍历或统一管理机制。

运行游戏,发现有所进展,但身体不再跟随头部

这一段内容描述了在进行实体引用处理、仿真区域(Sim Region)加载和实体指针关联的过程中,系统出现了部分不一致性和引用错误的问题,并详细分析了错误的根源与可能的解决方法。下面是完整的中文总结:


当前问题状态

  • 实体的主体(body)和头部(head)之间的跟踪关系出现异常,表现为在仿真中头部和身体的行为不同步。
  • 整体系统逻辑已经恢复到正常运行状态,但仍存在细节错误。

问题初步定位

  • 实体引用(Entity Reference)未能正确解析导致的同步错误。
  • 某些实体的引用未能在合适的时机完成加载和绑定,造成引用为空或不正确。

详细分析

1. 实体引用的解析时机错误
  • 引用的加载过早:当前 get_entity_by_id 函数中会直接通过 ID 获取实体的哈希值(hash bucket)。
  • 哈希表尚未填充完毕:在 Sim Region 初始化并解包实体时,所有实体尚未全部加载完毕,也就是说,有些实体还未被插入哈希表。
  • 解包顺序问题导致引用失败:如果头部先加载、而身体尚未注册至哈希表,则引用失败;反之亦然。此时实体引用将无法成功解析。
2. 实体加载流程中的风险函数
  • add_entity_raw 中调用了 load_entity_reference
  • load_entity_reference 中使用了 get_hash_from_id,这是当前错误的核心所在。
  • 实体还未被放入哈希表,就尝试在哈希表中查找引用对象,导致失败。

系统设计上的约束逻辑

模块 要点
Sim Region 解包所有实体之后,才允许进行引用解析
实体哈希映射 必须在所有实体加入哈希表后,才能使用 ID 查找引用
实体引用加载 load_entity_reference 不能在实体解包时立即调用

可能的修复策略

  1. 调整引用解析时机
    推迟对实体引用的加载与解析,直到所有实体都被解包并成功加入哈希表之后再进行引用绑定。

  2. 将引用缓存下来,延后处理

    • 在加载过程中,暂时保存原始引用数据(如 ID 等);
    • 完成所有实体的加载后,再统一处理这些待解析引用。
  3. 限制引用行为

    • 明确规定实体引用解析只能发生在仿真区完全初始化之后;
    • 禁止在构造过程中即发起引用查找操作。

系统结构未来方向建议

  • 使用 流式加载机制(streaming) 管理实体进入与退出 Sim Region;
  • 实体的所有引用操作必须显式依赖一个“引用有效性阶段”,不可自动提前执行;
  • 可能重构 add_entity_rawload_entity_reference 的职责划分,使其在逻辑上与仿真阶段解耦。

验证建议

  • 设置断点或日志打印,确认实体加入哈希表的时序;
  • 在引用解析前检查哈希表中是否包含对应实体;
  • 手动测试加载顺序(例如先加载 body 再加载 head,或反之)验证引用是否正常。

总结

本质上是实体系统引用解析时机过早,引用关系尚未建立即尝试访问,从而导致头部和身体之间失去同步。在系统运行初期阶段特别容易出现该类问题,必须通过延迟引用解析或二次处理机制来规避。修复此问题后,实体同步行为将更稳定可靠。
在这里插入图片描述

game_sim_region.cpp 中合并 AddEntityRawAddEntity 函数

这段内容主要是对实体处理逻辑中的函数结构进行重构和简化,尤其集中在实体添加(add_entity)和解包(unpack)阶段的一些冗余流程进行合并与清理。以下是详细的中文总结:


代码结构简化目标

  • 原先存在两个函数:add_entityadd_entity_raw,分别用于通过 ID 添加实体与通过实体指针直接添加实体;
  • 实际运行过程中,现在所有实体的添加只通过 add_entity,因此决定将 add_entity_raw 的逻辑合并简化;
  • 旧有的某些处理逻辑已不再适用,属于历史遗留代码,现阶段可以安全移除或简化。

函数重构流程详解

1. 确认当前调用路径
  • 当前系统中,只剩下 add_entity 被调用;
  • 其内部逻辑是判断实体是否落在仿真区域(sim bounds)内,如果在范围内则执行添加;
  • 确认 updatable 标志仅用于标记实体是否在仿真区域中心,这部分逻辑仍然保留。
2. 合并逻辑
  • add_entity_raw 的功能逻辑整体移动进 add_entity,避免重复定义和维护;
  • 将获取实体哈希位置(get_hash_from_id)的部分提前执行,作为公共流程处理;
  • 加入断言逻辑:如果某个 ID 对应的哈希槽已经存在实体指针,应抛出异常,防止 ID 冲突或数据覆盖。
3. 简化仿真区域处理
  • 不再需要对每个实体进行额外的锁保护操作,因为锁机制已经转移到以 chunk 为粒度,不再针对单个实体;
  • siming 标志与来源实体(source)的一些额外状态标志不再使用,统一清除;
  • 实体的位置设置只在目标副本(dest)上进行,无需再去操作源对象;
  • 处理流程更直接,减少冗余判断。

进一步清理逻辑

  • 将返回值类型从结构体或实体指针改为 void,因为现在添加实体后不再需要返回任何内容;
  • 所有处理都已经在内部完成,外部不再需要操控解包完成后的实体数据;
  • 处理完实体指针与引用的更新后,无需再继续其他工作。

最终成效与好处

优化内容 效果
合并重复函数 减少维护负担,逻辑集中
移除历史字段与标志 清除不再使用的代码路径
保持引用一致性 保证实体引用不会冲突或丢失
提高代码可读性 结构清晰,行为明确
确保线程安全 所有同步机制由更高层次管理

验证建议

  • 测试同一仿真区域内添加多个实体时是否正常;
  • 确认所有实体的哈希表项都正确设置;
  • 验证是否能避免 ID 冲突、引用缺失等错误;
  • 观察实体头部与身体是否重新正确绑定并同步移动。

总结

此次优化主要是将旧有的 add_entity_rawadd_entity 合并为统一流程,移除了无用的状态标志和锁机制,改为以 Chunk 为单位进行实体管理与同步。通过简化逻辑结构,提升系统可维护性,并为后续进一步的实体流式加载与仿真区域优化打下基础。整体架构更简洁,运行更高效,逻辑更清晰。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

再次运行游戏,发现结果依然相同

我们现在的代码逻辑虽然已经完成了清理和重构,但并没有真正修复之前存在的 bug。也就是说,我们目前只是对相关函数和流程做了结构上的整理,把冗余的部分合并,去掉了历史遗留的逻辑,但核心问题仍然存在。


当前状态总结:

  • bug 仍然存在:我们尚未对 bug 的根本原因做任何修改,原有的错误行为依旧会发生;
  • 清理的是代码结构:我们只是把添加实体的流程变得更简洁清晰,例如将 add_entity_raw 的逻辑合并到了 add_entity 中;
  • 预期行为未改变:仿真流程、实体引用处理、哈希表插入等行为逻辑并未被实质性改动,所以 bug 仍然可以复现;
  • 目的是打好基础:虽然 bug 没修,但通过简化代码结构,我们现在更容易追踪问题、理解流程,也为接下来的修复工作打下了坚实基础。

下一步该怎么做?

  • 聚焦 bug 的触发机制和时间点,例如实体引用被解析的时机;
  • 检查是否在所有实体加载完成之前就尝试引用其他实体,导致引用失败或未绑定;
  • 优化 unpack 流程,让哈希插入在引用解析之前全部完成,确保引用永远能找到目标。

结论

我们这次的重构主要是为了清理和简化代码逻辑,并未触及 bug 的根本原因。因此,bug 依旧会复现。接下来需要专注于实体引用初始化与哈希表构建顺序的问题,从根本上解决实体之间引用失败的问题。

game_sim_region.cpp 中引入 ConnectEntityPointers

我们目前的任务是解决一个实体在加载过程中,头部和身体之间无法正确连接的问题。为了解决这个问题,我们首先需要确保所有实体都完成了解压缩操作,然后再进行实体指针的连接处理。虽然这种做法不是最高效的,但目前我们选择最简单直接的方式,优先让系统能够正常运作。

具体来说,我们需要引入一个阶段,例如叫做“connect entity pointers”。在这个阶段中,我们会传入一个 sim region(模拟区域),它包含了一系列的实体。我们将对其中的每一个实体进行遍历,并对其中的指针进行重映射。

因为 sim region 已经包含了解压后的所有实体,所以我们可以在解压完成之后,再集中进行一次性连接操作。之前在实体加载过程中用到的 load entity reference 函数,将被移到这个新的连接阶段之外执行。它不再需要在加载过程中调用,也不再需要依赖世界模式(world mode),所以这部分逻辑也可以被简化或者废除。

在执行之前,如果直接运行程序,预期会看到两个问题:第一,我们的头部和身体之间仍然没有连接;第二,由于尚未调用连接函数,实体内部的指针仍然是无效的垃圾数据。因此,为了解决这些问题,我们在所有数据解包完成之后,立即调用一次 connect entity pointers 函数。这一函数会遍历所有实体,并通过哈希表查找,把需要建立关系的指针连接起来。

完成上述流程后,我们的系统就可以正确地在加载完成后,把头部和身体等关联实体连接起来,从而恢复其在逻辑上的整体结构。
在这里插入图片描述

没身体

在这里插入图片描述

断点没进来head是空

在这里插入图片描述

修复一下问题

在这里插入图片描述

在这里插入图片描述

运行游戏,发现头部和身体保持连接

我们目前已经完成了实体指针连接的逻辑处理,现在理论上,这些实体之间的连接关系应该已经稳定,不能再轻易被断开了。具体来说,在所有实体完成解压之后,我们统一执行了连接实体指针的阶段,这一步确保了例如头部与身体等之间的引用关系已经被正确地设置。

通过这种集中式的指针连接流程,我们避免了在解压过程中尝试进行实体引用绑定的问题,也降低了逻辑复杂性。这样一来,在模拟区域中的每个实体,其内部的所有引用都会被一次性正确重映射到实际存在的其他实体对象上。

完成连接后,实体之间形成了稳定的结构关联,指针不再指向空值或无效内存,也就意味着像身体与头部这类相互依赖的部位将无法在逻辑上被错误地断开。这是我们实现系统完整性与实体行为一致性的关键一步。系统结构现在更加健壮,也为后续进一步优化处理逻辑打下了基础。

将与实体相关的结构体从 game_sim_region.h 移至 game_entity.h

我们接着继续进行了代码的整理与清理工作,目标是尽可能移除一些多余的、遗留的代码结构,以便后续能更专注地处理实体相关的功能逻辑,并逐步扩展可以用于游戏系统的实体内容。

首先,我们确认了之前标记为“待处理”的 sumit identity 已经完成处理,可以标记为已解决。接着,我们对原先定义的实体标志(flag)进行了清理,比如 ENTITY_FLAG_SWIMMING 已经不再使用,经过检查,确实没有其他地方再引用它,于是将其彻底删除。

接下来对实体的定义代码进行结构性整理。我们注意到原本散落在不同位置的实体相关代码可以进一步集中,于是尝试将这些内容抽取到更合适的文件中,例如 entity.h 文件。这包括实体结构体 entity 本身、实体 ID 类型定义等内容。

在移动的过程中,我们发现之前一些结构体的声明顺序存在些许混乱,比如 game_entity.h 被放在 sim_region.h 之后引用,但逻辑上它应该更早加载。我们重新整理了这些文件包含顺序,使逻辑上更加清晰。

完成代码拆分之后,entity.h 现在独立管理所有与实体直接相关的定义和结构,使代码结构更加模块化,方便管理和扩展。而在 sim_region 中,清理了不再需要的前向声明,现在的 sim_region 文件保留的内容也更加专注于模拟区域本身的逻辑,不再与实体实现细节交叉。

最终的结构表现出一种更清晰的模块划分:模拟系统处理模拟逻辑,实体模块管理实体数据与逻辑,世界状态存储与加载在专用部分中管理。整个系统的架构正在逐步走向一种清晰、有序、可扩展的方向,也为今后添加更多游戏逻辑和实体交互打下了良好基础。下一步我们将进一步展开实体的具体功能和逻辑结构的构建。

把game_sim_region.h的内容先全部剪切到game_entity.h中

在这里插入图片描述

在这里插入图片描述

不要让game_entity.h 引用game.h头文件

在这里插入图片描述

之后把game_entity.h 中关于sim_region 的移动到game_sim_region.h中

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_world_mode.cpp 中禁用 Z 轴移动,测试地图遍历效果

接下来我们将工作重心重新放回到实体的物理属性上,特别是它们在世界中的位置、体积感(Box感)以及移动方式等方面。我们的目标是使实体在游戏世界中拥有更合理的物理表现,并进一步构建可探索的世界环境。

我们准备进入 game world mode 来审查当前是如何生成世界结构的,包括地图区域的划分和实体初始位置的设定。例如,我们决定移除 Z 轴的上下移动支持,也就是暂时不处理垂直方向的位移。这可以通过简单修改 Z 轴的坐标设置来实现,从而简化当前的空间结构,让我们专注于二维平面的探索与碰撞。

接着,我们尝试生成多个屏幕区域,例如八个屏幕大小的世界区域,以模拟实际游戏中可供玩家自由移动的大地图。目的是测试当前系统在多区域连接、实体在区域之间移动时的表现,并观察是否可以实现基本的探索体验。

在测试过程中,我们立即发现了一些问题,例如跨越边缘时会出现异常现象。这类问题可能与实体的空间定位、区域边界处理、实体之间的碰撞检测等有关。因此我们下一步的任务将是定位并修复这些 bug,以确保基础的探索行为逻辑正确。

此外,我们注意到当前系统中还存在一个断言错误(assertion),这提示我们有某个逻辑前提没有被满足。虽然世界探索和移动 bug 的修复可以稍后再进行,但这个断言错误则需要优先处理,以确保系统在运行过程中不会因逻辑不一致而崩溃。我们将首先着手修复这个问题,作为当前开发阶段的首要任务之一。
在这里插入图片描述

game_sim_region.cpp 中引入 DeleteEntity 函数

我们现在要实现的是实体的删除功能。这个功能的逻辑非常简单:我们通过给实体添加一个删除标志位(ENTITY_FLAG_DELETED)来表示某个实体需要被删除。

一旦某个实体设置了这个删除标志,我们在模拟结束阶段打包世界(例如在 package_world 函数中)时,就会跳过这些被标记为“已删除”的实体。具体来说,在遍历所有实体并准备打包之前,首先检查该实体是否设置了删除标志,如果有,就不进行打包。这样,该实体不会被写入下一帧的模拟数据中,从而在后续更新中自然消失,完成删除操作。

在这个流程中,我们可以非常轻松地实现一个 delete_entity 函数。这个函数的功能就是接收一个模拟区域 sim_region 和要删除的实体 ID,然后在对应的实体上设置 ENTITY_FLAG_DELETED 标志。由于删除行为只在模拟区域内部起效,所以函数也强制要求提供 sim_region,确保操作范围明确。

这一设计的好处在于实体的删除是“延迟处理”的:控制器代码仅设置“退出”请求,而不直接处理实体结构本身。实际的实体删除逻辑会在之后模拟流程中自动执行。这种方式能避免对运行时数据直接操作造成错误或不一致,同时保持了系统的简洁性与稳定性。

在具体实现中,例如在玩家退出场景的逻辑中,我们只需要设置一个变量,比如 controller_hero_exited = true,表示玩家触发了退出事件。接下来,在处理主角控制逻辑的位置,我们会判断该标志是否为真,如果为真,并且成功加载到主角实体对象,则调用 delete_entity 对其进行标记。

这个过程非常清晰且简洁,不需要对系统做出过多干预,仅靠标志控制流转逻辑,就可以在下一帧自然完成实体的清理。实现这一机制后,系统将具备更稳定、更清晰的生命周期管理能力。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,测试进出区域功能

我们目前重新回到了之前提到的一个关键问题 —— 世界与移动系统相关的 bug。在进入或退出游戏之后,某些实体的行为表现异常。虽然当前剩余时间不多(大约还有10到15分钟开发时间),我们决定先着手定位这个 bug,为明天重构世界移动逻辑打好基础。

首先我们注意到,原本用于地面缓存(ground buffers)的机制已经被移除,因此相关的一些旧数据和流程也失去了作用。这使我们更有必要检查当前实体的位置和移动系统实现是否存在遗漏。

通过查看实体结构体的定义,我们发现里面包含了一系列与移动相关的位置变量,例如 movement_frommovement_to。这些变量在被序列化(打包)和反序列化(解包)时存在问题。因为实体的位置在打包时应转换为相对于当前区块(chunk)的局部坐标,但这些移动变量却仍保留了旧的、全局坐标系下的位置数据。

这是导致 bug 的根源之一。打包和解包过程中,如果只处理了实体的主 position 值(例如 p),而忽略了 movement_frommovement_to 等辅助位移参数,就会导致实体在模拟区域切换或重载后出现位置错乱、移动方向错误等问题。

因此,我们意识到,所有的与位置相关的数据都必须统一处理。也就是说,在打包实体时,所有位置数据都要转换为相对于所属区块的局部坐标;而在解包时,也要将这些局部坐标转换回目标模拟区域的全局坐标。这些步骤需要在 packunpack 过程中一并实现,不能只处理主位置值。

我们已经可以开始着手实现这个改进,第一步就是修改打包逻辑,使 movement_frommovement_to 也使用与 p 相同的坐标变换规则进行处理。这样就能确保实体在不同模拟区域或世界区块之间移动时,所有相关坐标值都保持一致性,避免出现物理表现错乱的问题。

这将是我们接下来工作的重点之一,也是重构世界移动逻辑和提升系统稳定性的关键步骤。

game_entity.h 中解释为何不再需要 world_position,并考虑移除它

我们现在开始对世界位置系统进行进一步的简化与优化。首先明确一点:WorldPosition 中的 chunk_p(区块位置)字段已经不再是必须的。这是因为其所承载的信息在新的架构中已经冗余,完全可以被移除。

我们已经重构了系统,使得每个实体的世界位置不再需要单独记录其所处的世界区块坐标(chunk_x, chunk_y, chunk_z),因为这些信息已经由包含该实体的区块本身维护了。实体只需要保存相对于所在区块的偏移量(offset),不再需要存储其完整的世界位置。

我们可以放心地删除旧的 chunk_p 字段,也可以移除 old_chunks,因为这些已经不再被使用。重新编译后发现,chunk_p 的唯一用途是通过 world_position_at 被设置,但这个函数其实只是为了转换信息而存在的。

接着我们来看 GetSimSpaceP 这个函数的用途。现在它唯一的用途是在实体被解包(unpack)时调用。换句话说,实体在被加载到模拟区域时,只需要知道相对摄像机所在区块的位置偏移即可完成位置计算,因此 GetSimSpaceP 实质上只是执行了一个偏移运算。因此,这个函数不再需要处理复杂的世界坐标逻辑,它的实现可以大幅简化。

进一步说,这一系列简化和重构是系统架构趋于完善的直接体现:当我们移除原本复杂而冗余的设计,系统的各种机制反而变得更自然、更一致,同时也更容易扩展和维护。现在,世界位置系统彻底变成了相对局部坐标系的结构,所有实体仅关心与其所属区块之间的偏移值即可。

这一改变不仅让打包/解包逻辑更清晰,同时也避免了频繁的坐标转换带来的错误或重复运算,为后续更复杂的功能提供了更坚实、简洁的基础。我们将继续在此架构基础上推进下一步的开发。
在这里插入图片描述

在这里插入图片描述

发现不能移动

在这里插入图片描述

game_sim_region.cpp 中让 BeginSim 计算 world_position 并将 ChunkDelta 传入 AddEntity

我们现在继续对实体的解包(Unpack)流程进行优化,核心目标是简化世界坐标的处理方式,彻底消除之前那套繁琐的、基于绝对世界坐标的系统。

首先明确当前需求:在解包时,我们需要知道模拟区域的原点(sim region origin)以及当前正在处理的存储区块(storage chunk)的位置,目的是获取两者之间的位移差值,也就是“区块偏移”(chunk delta)。

既然我们已经掌握了存储区块的 chunk_xchunk_ychunk_z 等信息,我们可以直接重建其世界位置。然后,通过将模拟区域的原点位置与该区块的位置相减,得到二者之间的偏移量 chunk_delta。之后,在解包每个实体的位置时,只需将其偏移值与 chunk_delta 相加,就可以获得其在模拟区域中的实际位置。

这意味着我们不再需要使用任何绝对坐标或复杂转换逻辑,所有位置运算都可以在局部坐标系统内完成,只需要处理偏移量。

接下来,我们将原来传入 GetSimSpaceP 的部分重构为简单地传入 chunk_delta。在这个新模型下:

  • 所有与实体位置相关的值(如 positionmovement_frommovement_to 等)都变成了相对于区块的偏移;
  • 在解包过程中,只需要一个统一的处理方式:加上 chunk_delta
  • 这样,不管是主位置 P,还是运动起点 movement_from、终点 movement_to,都可以直接在解包阶段一并修正为模拟区域下的本地坐标。

在代码层面,我们遍历实体数据并将各项需要修正的位置都加上 chunk_delta。逻辑变得极为简洁,不再需要特殊判断或不同类型坐标的区分。

最终结果是:实体结构体中的世界坐标相关字段全部实现了解耦,解包逻辑统一且清晰,位置数据全部基于区块偏移进行运算。这种设计大大提升了可维护性、可扩展性,也为后续更复杂的逻辑(如多区块加载、动态移动区块等)打下了坚实基础。

系统的模块化和简化已经逐步显现出巨大的优势,我们将继续在这个方向上推进架构优化。
在这里插入图片描述

game_sim_region.cpp 中让 EndSim 中的 EntityP 相对于 ChunkP

我们目前正在处理的是实体在“打包进世界”(Pack Entity into World)时的坐标系统问题。之前实现中,所有实体的位置值都是相对于模拟区域(Sim Region)的局部坐标,而不是相对于其所在区块(Chunk)的坐标,这会在序列化和反序列化(打包与解包)时导致问题。

为了修正这一点,我们采用了以下策略:


坐标偏移的处理逻辑

我们知道实体需要相对于其所在区块进行定位,因此:

  1. 在打包时,我们要把实体的坐标(如 Pmovement_frommovement_to 等)从模拟区域坐标转换为区块坐标;
  2. 在解包时,我们执行相反的操作,把这些坐标从区块坐标恢复成相对于模拟区域的本地坐标。

核心操作就是:计算并应用“区块偏移量”(Chunk Delta),实现坐标的双向转换。


修正打包逻辑中的问题

PackEntityIntoWorld 的过程中,原本的代码是错误地直接使用了相对于 SimRegion 的坐标。为了修复这个问题,我们:

  • 先计算了实体的 全局世界位置(World Position)
  • 然后创建了一个与该位置匹配、偏移为零 的 Chunk 结构;
  • 最后从该位置中减去 Chunk 基点,得到相对于 Chunk 的局部偏移值,作为实体的最终坐标。

举例:

WorldPosition ChunkP = EntityWorldPosition;
ChunkP.Offset = {0, 0, 0};

这段代码的目的是:保留 Chunk 的位置标识(X、Y、Z),但将其偏移部分清零,便于后续计算 Chunk Delta。


Bug 调试及原因分析

在调试过程中,我们遇到了一些问题:

  • 一开始修改了打包流程的代码,但导致了意料之外的行为;
  • 追踪后发现原因是:我们将位置的设定从 SimRegion 内移到了其他位置,破坏了原本的调用路径;
  • 更严重的是,我们误用了一些变量,比如错误地使用了不正确的 ChunkP 值,导致坐标变换不正确。

最终明确的修复策略是:

  • 在初始化实体位置时,必须根据实体的实际世界坐标生成 Chunk 信息
  • 同时要确保偏移为零,这样在打包和解包阶段应用 Delta 时不会出现逻辑偏差;
  • 所有涉及实体坐标的逻辑都应统一成“以 Chunk 为基准”的相对系统。

总结与架构成果

通过以上一系列改进,我们实现了:

  1. 实体坐标系统彻底本地化:再无全局世界坐标,统一使用 Chunk + Offset;
  2. 打包与解包对称性增强:操作逻辑统一,便于维护与扩展;
  3. 代码清晰度提高:简化了繁琐的转换逻辑,减少出错点;
  4. 为未来系统优化打下基础:尤其是在支持大世界、动态加载、跨 Chunk 移动等特性时。

之后我们还会对这些代码做进一步清理,消除冗余变量和不一致调用,进一步提升系统的整洁性和运行效率。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,发现角色可以离开屏幕

现在系统在视觉上已经有了一定的改进,比如可以成功地将实体移出屏幕边界,但是相机的跟踪行为出现了异常:相机没有随着实体的移动进行更新。为了解决这个问题,我们接下来要进行排查和修复。


相机未跟踪问题分析

首先确认目前的表现是:

  • 实体在屏幕上可以正常移动,甚至可以超出可见范围;
  • 但是相机没有跟随实体移动,导致画面无法持续聚焦在关键目标上。

这个问题很可能出现在以下几个方面:


检查流程和位置

我们需要检查“相机位置更新”的逻辑链条:

  1. 相机目标的确定逻辑:我们需要确认当前系统是否正确设置了相机的跟踪目标,通常是主角或特定实体;

  2. 模拟区域(Sim Region)更新时的相机位置逻辑

    • 是否在每一帧都重新计算了模拟区域的原点;
    • 是否以被跟踪实体的世界坐标来更新模拟区域;
    • 是否将模拟区域的偏移同步到了相机绘制使用的位置中。
  3. 渲染位置换算逻辑

    • 所有世界坐标是否正确转换为相对于相机原点的位置;
    • 是否遗漏了将 chunk delta 应用在视图变换中。

推测当前可能的问题

根据前文,我们很可能已经从实体数据中剥离了冗余的世界位置信息,改为通过 chunk 进行位置还原。在这个架构中:

  • 如果相机位置仍然依赖旧的世界坐标(而非 chunk + offset 的新体系),就可能导致未正确更新;
  • 或者,相机目标实体的 offset 在打包时被错误处理(没有正确加上 chunk delta),从而使得相机跟踪失效;
  • 也有可能是在实际更新相机位置函数中,使用的是错误的坐标源(比如用到了某个未更新的缓存值)。

后续修复思路

为了修复这个问题,我们接下来应该:

  1. 确认相机跟踪目标是否设置为正确的实体 ID
  2. 查看更新相机位置的代码段,确保使用的是实体经过 chunk delta 解包后的位置;
  3. 核实模拟区域的中心是否正确设置,并被用于转换坐标系
  4. 添加调试信息,输出相机当前位置与被跟踪实体的位置差值,确认同步逻辑是否正确;
  5. 若存在滞后问题,可能还需检查是否在帧更新流程中顺序有误(如相机更新在实体更新前触发)。

阶段总结

目前系统已经进入了一个重要的结构性转折点:坐标完全本地化,相机、实体、chunk 之间的相对关系清晰。剩下的相机跟踪 bug 只是数据流或者更新顺序的小瑕疵,只要理清逻辑链,很快就能解决。

接下来处理完这个相机问题后,整个坐标系统和渲染框架就会趋于稳定,为后续更多功能(如地图分页加载、动态场景构建、区域裁剪优化等)打下基础。

game_sim_region.cppEndSim 末尾设置 Entity->P 和分块移动

当前在处理实体位置(entity P)时,发现代码中有部分操作是基于实体的世界坐标(world P)进行的,但实际上这些操作应该是在转换到区块空间(chunk space)后才更合理。

因此,针对相机的跟踪代码,需要重新编写和调整执行顺序。具体来说,相机相关的计算必须在后续的实体位置转换和处理之前完成,也就是说,相机代码需要提前执行,放到整个流程的更前面部分。

这样可以确保相机能够基于正确的、已转换的实体位置来进行追踪和更新,避免因为执行顺序错误而导致相机位置不正确的问题。调整执行顺序后,实体的位置映射和相机跟踪逻辑将更清晰且准确。
在这里插入图片描述

再次运行游戏,发现摄像机现在能跟随主角

目前的位置打包和解包机制已经比较稳定,整体进展有所提升,但效果还不是完全理想,画面仍然显得有些不流畅或者说“有点毛糙”。观察时发现相机的位置偶尔会出现短暂的错误,导致画面瞬间偏离正确位置。

这个问题尚未彻底解决,需要放慢速度仔细观察以定位具体原因。目前时间紧迫,未能完全搞清楚错误的细节,但整体方向是向好的,稳定的打包解包机制为后续优化打下了基础。下一步重点是解决相机位置偶尔不正确的问题,使得画面切换和移动更加平滑自然。

问答环节

目前代码工作已经推进到一定阶段,接下来会继续处理和完善相关部分,因此暂时先放下,明天再继续。现在进入问答环节,如果有关于当前进展或细节方面的问题,可以随时提出,进行解答和讨论。

#ifndef#if !defined 有什么区别吗?

之间其实没什么实质区别,未定义主要是为了能在更复杂的表达式中使用它。至于是否觉得有时会把事情倒着做,这个问题表达了对开发流程或思路的反思,暗示可能在某些步骤上存在逆向或非线性处理,但并没有具体展开。

每次回到一段久未修改的代码时,会不会觉得像是在倒退?

虽然有一段时间没进入代码,但屏幕移动的问题其实不大。相反,现在感觉进展很大,代码比之前好多了。以前代码很糟糕,现在变得相当不错。通常是给代码一些时间沉淀,然后再回过头来看,才能真正理解它的运作方式,现在正是这种情况。

你平常会用很多头文件吗?

我们实际上把所有代码都编译在同一个翻译单元里,技术上虽然有个平台相关的单元,但基本上就是把代码按想要看到的内容或者方便管理的方式拆分成不同的文件而已。拆分文件主要是为了方便管理,把相关的内容归到一起,这就是拆分头文件的主要目的。

为什么不用 #pragma once

我们发现使用 #pragma once 实际上比传统的 include guards(#ifndef#define)还慢,虽然理论上 #pragma once 应该更快更简单,但在实际编译中,尤其是用 Visual Studio 编译器时,速度却没那么理想。历史上做过很多性能测试,尤其是在1999年,当时测试显示用重复的 include guards 是最快的做法。传统的写法是先用 #ifndef FOO_H 包裹头文件内容,然后在 CPP 文件中用 #ifndef FOO_H#include "foo.h" 的方式引用。经过多次验证,#pragma once 直到很晚依然比传统的 include guards 慢。

不过,现在我们用的构建方式是 Unity Build(将所有代码合并成一个翻译单元整体编译),这种方式速度远远快于任何单独的 include guard 机制,因此不必太担心 #pragma once#ifndef 这些细节。

但如果还在单独处理头文件重复包含的问题,建议不要盲目用 #pragma once,而是先验证编译器的性能表现,因为在某些编译器上,手写的 include guards 反而会更快,这虽然很反常,但事实如此。

你觉得实体系统架构更多是基于经验还是探索?

我们的实体系统完全是基于探索和尝试,完全没有现成的经验可以依赖。这个系统和之前见过的任何实体系统都不一样,甚至没有听说过有人用类似的方法来实现。可以说这是全新的设计,完全是零经验的状态下进行的开发。目前所有的方案和结构设计都是在摸索中逐步形成的,没有现成的计划或参考,一切都是在不断试验和学习中推进的。

你对实体的“组件”有什么计划?还是继续用现在的标志位方式?

实体的组成部分只是使用标志(flags),没有引入组件的概念。所有实体理论上都可以拥有所有的功能和属性,这就是系统设计的目标。系统并不通过组件来区分不同实体的功能,而是通过标志来标识和控制不同的行为和特性。这样简化了结构,使得每个实体都能灵活地拥有多种能力。

代码库增长后,有很多函数可能不再被调用,有什么好方法可以识别这些无用代码?

要找出哪些函数从未被调用,除了自己发现之外,实际上没有特别好用的办法。技术上可以通过开启链接器的警告功能来实现,比如让链接器提示“某个函数未被调用,已被移除”等信息。通过把每个函数编译成单独的编译单元,并开启相应的警告,可能让编译器或链接器告诉我们哪些函数未被调用。

另外,开启全优化选项(如优化引用和相关设置)时,编译器会做调用图分析,自动剔除那些没人调用的函数,也能间接告诉我们哪些函数未被使用。

不过其实不用特别在意这些未使用的函数,因为它们不会影响编译时间或运行效率。如果对某个函数有疑问,可以直接删掉然后尝试编译,如果能编译通过说明没人调用,函数可以安全移除。这样是最简单直接的方法。

能否运行完整个游戏流程以找出未被使用的代码?

要想完全运行游戏并找出哪些代码未被使用以便清理,实际上没有简单直接的方法。可以使用代码覆盖率分析(coverage profiling),这是一种代码分析操作,需要借助性能分析器来完成。覆盖率分析会告诉我们哪些代码路径被执行过,哪些没有被执行。

但是,问题在于游戏的复杂性和交互性:某些代码没被执行,可能是因为测试时玩家没进入那部分内容或者没触发那些代码。覆盖率分析只能告诉我们哪些代码“没被运行过”,但不能判断这些代码是否“永远不会运行”。

因此,覆盖率分析虽然有用,但必须结合自身的思考和测试设计,比如安排多种测试路径,确保覆盖更多游戏场景。不能仅凭某次测试没调用某代码就断定它永远不会被调用,因为这种假设通常不成立。需要通过反复测试和设计更全面的用例来辅助判断哪些代码是真的多余的。

Dynamite Jack 的实体系统类似(但没有压缩)

有人提到Dynamite Jack的实体系统和我们的类似,但没有使用压缩。实际上,压缩才是这个系统的关键部分,是让系统独特的核心。虽然很多游戏之前也有单一实体类型的设计,但真正不同的是,我们的实体类型包含非常多的数据,如果不压缩是无法直接存储的。我们通过动态压缩,把这些庞大的数据实时压缩处理,这才实现了系统的可行性。

至于锁机制,我们最近确实有检查和调整,确保多线程或者资源访问的安全性和效率。

说到代码覆盖率,当年 Farbrausch 为了压缩《.kkrieger》体积开发了一个名为 Lekktor 的工具,因为某个菜单按键没人用,对应代码被省略了

代码覆盖率问题非常复杂,主要有两种“未使用代码”的情况:第一种是完全无法被调用的代码,这种代码比较容易识别和移除,链接器通常能自动处理这类代码;第二种是理论上可能被调用但实际运行时永远达不到的代码,这种情况很难判断。

实际上,判断一段代码是否永远不会被调用是一个不可解的问题,类似于著名的“停机问题”。如果能写程序判断某代码是否可达,那么就能解决停机问题,但停机问题已知是不可解的,因此这种判断本质上是不可能做到完全准确的。

在实际项目中,比如为了赶比赛或者有紧迫的开发周期,经常很难对所有代码路径做全面测试。虽然可以尝试在设计时避免出现极其罕见的代码路径,尽量让大部分流程走相同代码路径以保证基本覆盖,但这并不能完全确保代码覆盖的充分性,特别是代码中的极端或特殊情况可能根本没被触发。

另外,代码覆盖率工具虽然能告诉你哪些代码被执行了,哪些没有执行,但它们无法判断未执行的代码是否根本无法被执行。游戏开发中特别难,因为游戏状态多变,用户行为不可预测,某些功能可能只有在非常特殊条件下才会触发。

因此,在实际开发中,只能通过近似和经验去设计测试和代码,依赖链接器和编译器的死代码消除来剔除明显不可达的代码,但不可能有一种神奇的工具能完美告诉你所有活跃和死代码的情况。

最后,即使代码被发现无法达到,也要判断它是不是故意留下的,或者是设计上的失误导致不应存在的代码残留,这本身也是一个复杂的判断问题。

停机问题(Halting Problem)是计算机科学中的一个经典不可判定问题。它的核心意思是:

给定一个任意的计算机程序和它的输入,是否存在一个通用的算法,可以判断这个程序在这个输入下是否会最终停止运行(停机),还是会永远运行下去(死循环)?

**答案是不存在这样的通用算法。**换句话说,没有一个万能的方法能够准确判断所有程序是否会停机。

这个结论是由艾伦·图灵在1936年证明的,成为计算理论中的一个重要里程碑。停机问题说明有些问题本质上是无法被计算机程序解决的,是不可判定的。

通俗点说,就是你不可能写一个程序,它能对所有其他程序说“这个程序一定会停”或“这个程序一定不会停”,而且这个判断永远是正确的。

所以,停机问题是计算机科学里证明某些问题根本无解的经典例子。


网站公告

今日签到

点亮在社区的每一天
去签到