总结并为今天做铺垫
今天的工作内容是解决调试系统中的一个小问题。昨天我们已经完成了大部分的调试系统工作,但还有一个小部分没有完全处理,那就是关于如何层次化组织数据的问题。我们遇到的一个问题是,演示代码中仍有一个尚未解决的部分,这个问题阻碍了我们进一步的进展,因此我们必须解决这个问题,才能继续进行剩下的工作。
这个问题的根源在于,我们之前对调试系统的理解还不完全,尤其是在处理新帧数据、对帧进行关联时,如何有效管理和存储这些调试记录。因此,我们现在需要回过头来,重新审视这些代码,并认真编写这一部分,确保它能正常工作。
回顾一下昨天的情况,运行游戏时,调试输出大致是正确的,但输出重复了很多次。每一行调试信息其实出现了多次,这显然不是我们想要的行为。因此,我们希望修复这个问题。这个 bug 并不是源自我们昨天编写的代码,因为我们写的代码看起来是正常的。实际的问题出在我们尚未找到一个合理的方案来处理调试系统的状态。当新的一帧数据进入时,它会被处理和关联,但我们并没有明确的管理策略,导致了重复的问题。
我们需要的是一种方法,能够在处理调试记录时,有一个合理的计划来存储这些数据,并以一种高效、合适的方式来保证一切正常运行。为了实现这一点,我们需要详细定义每一帧的调试记录如何存储和处理。
在之前的设计中,我们使用了一个表来存储调试值。这个表中包含了最大调试事件数量,控制每一帧最多可以有多少调试事件。这样做的目的是为了避免每次写入调试信息时产生额外的性能开销,因此我们没有在写入调试数据时增加额外的成本,这样就可以实现高效的调试记录存储。
目前,调试事件的数组会存储我们预设的调试数据,最多存储 8 帧的数据。这意味着,我们可以在一个循环中存储多达 8 帧的调试信息。如果我们想要减少内存带宽的使用,避免将调试事件从数组中复制出来,我们可以直接从数组中查看这些数据。但我们也可以选择将这些数据作为临时存储,减少数组的最大存储量,将它限制为 2 帧数据,并使用一个类似乒乓缓冲区的策略,在两个缓冲区之间交替写入和读取数据。这样,数据就可以在一个缓冲区中写入,而另一个缓冲区中读取。
最终选择的方案并没有太大的差别,主要的区别在于一个方案允许我们更灵活地处理特定帧的数据,比如捕获某一帧的数据,而另一个方案则可能需要更多的内存带宽来进行复制。
黑板:事件缓冲区与帧之间的不一致
在讨论调试系统时,有一个问题让我觉得特别有趣。即使我们将事件数据按帧进行划分,但这些数据并不完全对应每一帧的实际数据。让我详细解释一下这个问题。
假设我们有一个调试缓冲区,里面存储了多个调试事件,每个缓冲区对应一帧数据。这些缓冲区包含了最大数量的事件,并且每个线程都将事件写入这些缓冲区。为了确保线程之间不会互相干扰,使用了自增的方式填充这些事件数据。当我们在游戏的主循环中准备显示帧时,调试系统会将当前的帧数据"锁定",然后每个线程继续写入下一个缓冲区。
但是,这里有一个重要的问题,实际上这些缓冲区并不能准确地表示一帧的数据。原因是调试系统的工作发生在一帧结束之前。具体来说,帧开始时进行一系列的工作,帧结束后调试系统才开始处理数据。因此,调试系统的实际工作时间是在帧的结束之前,而缓冲区的"翻转"也在这一时刻发生。
问题更加复杂的是,事件有可能跨越多个帧。举个例子,如果我们加载一个纹理,可能需要很长时间才能完成,这样事件就不仅仅局限于某一帧,它可能会跨越帧与帧之间。比如说,某个事件在第 0 帧开始,但在第 1 帧结束。那么该事件的数据实际上不仅仅属于某一帧,而是跨越了多个帧。
这就导致了一个困境:帧的数据与缓冲区的数据并不完全对应,它们之间的关系并不是一一对应的。因此,我考虑将这些缓冲区作为临时存储,而不是将它们作为持久性的存储来处理。我可以从这些缓冲区中提取数据,并将其存入永久存储中,这样可以更灵活地处理这些数据,而不是仅仅依赖于每个缓冲区对应的帧数据。
虽然这样做可能会增加内存带宽的使用量,导致一些额外的开销,但我认为这个方法可以简化系统的管理,避免缓冲区与事件数据之间的不一致。通过将这些缓冲区视作临时存储,我们可以在需要的时候将数据提取出来并存储到永久存储中。而当我们不再需要这些数据时,可以按帧的基础丢弃它们,而不是依赖于缓冲区本身的结构。
我觉得这个方案是值得尝试的,虽然可能会带来一些问题,但它能有效减少不必要的重叠和复杂性。因此,我打算尝试这种方法,看看它能否解决当前的问题。
game_debug_interface.h:开始通过减少 MAX_DEBUG_EVENT_ARRAY_COUNT 来使事件成为临时缓冲区
我的第一个想法是完全放弃当前的概念。我希望完全去除调试事件存储在数组中的想法,而是将其视为一个临时缓冲区。当我们需要计算时,我们将需要的信息复制出来,并且丢弃所有其他不需要的内容。换句话说,其他的东西都应该被丢弃,不再保留。
所以,我们可以尝试这种方法,看看结果会如何。我的计划是将其简化为两个缓冲区,这样就能减少不必要的复杂性和存储空间的使用。

运行游戏,增加 MAX_DEBUG_EVENT_ARRAY_COUNT 并玩摄像机
从理论上来说,如果这些数组对调试系统没有风险,它们应该仍然能够正常运行。原本我以为会出现调试信息,但实际上我没有看到任何调试信息。这是因为我们通常是在流程的最后,必须在其中添加检查,以防止这种情况发生。
可以看到,我们确实需要一个额外的帧,以防止合并(collation)从一开始就触发。虽然目前这样看似有些问题,但这是可以接受的,因为我们稍后会进行更改。此外,你可以看到这与我们预期的bug一致,调试系统现在的表现确实更接近我们想要的样子。
实际运行时,调试系统看起来工作得相当好。我启用了调试功能,能够在实时中调整参数,效果也很不错。总的来说,我们过去两天编写的代码似乎运行得很顺利,没有遇到意外的情况,这真的很令人高兴。
目前,我们主要讨论的还是调试存储的问题。

game_debug_interface.h:移除 MAX_DEBUG_EVENT_ARRAY_COUNT 和 MAX_DEBUG_EVENT_COUNT,并将它们的值直接写入事件中
为了简化调试系统,现在将调试事件的缓冲区简化为一个"乒乓缓冲区",不再使用循环缓冲区的方式。原先的最大事件数 MAXI_BUG_EVENT_COUNT
现在被去除,因为不再需要这个值,系统只需要有一个简单的事件计数。
目前,系统会仅存储一个事件计数,并且仅存在两个事件缓冲区。所有与事件相关的数据都会存储在这两个缓冲区中,简化了整个结构。
进一步来看,事件数组的设计也不再需要复杂的实现。可以将其简化为一个简单的事件计数和两个指针,指向这两个缓冲区。实际上,事件计数可能并不必要,因为索引就足够标识存储的事件。
所有这些改动都是为了简化代码,去除不必要的结构。MAXI_BUG_EVENT_COUNT
这一部分似乎没有实际的用途,可以直接删除。在多线程环境中,我们保留了乒乓缓冲区的设计,确保每个线程能够安全地读取和写入数据,尤其是在渲染和加载系统中,尽管系统并不复杂,但为了线程安全,保留了这两个缓冲区。
通过这些简化,调试系统的结构变得更清晰,代码更加简洁,不再有多余的复杂性。同时,系统的线程安全性也得到了保证。

game_debug.h:开始将其余的代码移植到使用 copy outα
现在的目标是将调试系统的代码迁移到一种新的实现方式,在这种方式中,事件数组不再直接使用,所有的调试信息将被复制到一个更通用的、持久化存储的结构中。这种结构将类似于一个空闲链表(free list)或其他类型的存储系统,用来持久化和存储调试信息。
对于调试系统,特别是调试帧的存储方式,计划依然保持"调试帧"数组的概念,使用一种按帧存储的方式,只是改为每次新的一帧进来时,将数据推送到永久存储中,而不是将多个帧堆叠在一起。这样,旧的帧数据将被丢弃,存储空间得以回收。
调试帧的存储方式可以采用链表结构,每个调试帧都有一个指向下一个帧的指针,并且可以存储最早的帧和最新的帧。每次新的帧进来时,会更新链表,丢弃最老的帧数据,保持一个连续的帧记录。
此外,每个调试帧会有一个"空闲帧"指针,指向当前未使用的帧数据,以便于管理空闲的调试帧并为后续的帧数据分配空间。通过这种方式,可以有效地管理帧数据的存储和释放,确保调试系统的内存使用高效且灵活。
总之,这一修改的目标是将帧数据存储结构化,并通过引入链表和空闲帧管理的方式,使得调试系统能够在每帧之间进行数据的存取和更新,同时保持内存的高效使用,避免数据冗余和无效存储。
game_debug.h:重组 debug_state
现在的目标是开始逐步拆除之前为处理帧数据而设置的结构和代码。原本的做法是通过一种预设的方式来处理调试帧,而新的方法是希望将其转变为动态的、按需处理的系统。具体来说,当需要绘制轮廓或其他调试信息时,不再按传统的方式处理每个帧数据,而是要遍历所有的帧,这可能会带来一些挑战。
为了更好地管理帧数据,考虑引入"帧计数器"来追踪目前存在的帧的数量,这样可以更清晰地了解当前存储了多少帧数据。原本的一些索引和相关的帧数据结构可能会被修改,特别是那些指示帧是否完全处理完毕的标志(例如"相关帧"),需要重新评估是否保留它们。此时,有两个选择:可以将"相关帧"标记为"当前最最近的帧",或者直接依赖于已经完成的帧来确保不显示不完整的数据。
在进行这些修改时,意识到原来的数据结构可能需要进行精简,减少不必要的部分。例如,之前的"关联帧索引"和"临时内存"数据结构将不再使用,因为每个帧将按需独立处理。另一个需要考虑的事项是,是否将"帧数量"和"帧缩放比例"之类的信息直接存储在每个帧对象中,而不是单独的全局变量。这样的改变可以让每一帧独立持有这些信息,避免了过多的外部依赖。
不过,在进行这些更改时,也要注意不要一次性做太多的改动,因为在初步开发时,做太多的修改可能会影响系统的稳定性。要一步一步地推进,确保每个步骤都能顺利完成,并且可以在出错时迅速发现并修正问题。

game_debug.cpp:调整 DrawProfileIn
现在的目标是通过不同的方式迭代帧数据。之前的处理方式可能是将调试状态中的所有帧一次性处理,但新的方法是从最早的帧开始,逐步迭代直到到达最新的帧。这个过程允许在每一帧上执行相应的绘制操作,并且在需要查看像是条形图比例等信息时,也可以根据新的迭代方式来调整处理方法。
具体而言,帧的处理不再依赖于复杂的调试状态,而是通过更简化的方式进行。例如,之前在处理"帧条形图"时会用到的"lane count"(每帧的计数)现在需要重新调整。在遍历每一帧的过程中,首先初始化一个最小的lane count(比如0),然后根据每一帧的lane count来判断最大值,确保所有帧的lane count都能在最终图形中正确显示。
对于"frame bar scale"这个变量的处理,之前的用法是没有实际意义的,当前的目标是通过更合理的方式来替换它。新方法可能会设置一个最小值,确保所有内容能够适当显示,并且随着帧的不断迭代,动态调整这个比例。如果帧的bar scale值小于当前值,就进行更新,保证显示内容符合预期。虽然这个过程在未来可能会随着渲染功能的完善有所调整,但现在的主要目的是保持原有功能的可用性。
在继续开发时,仍然可以保留一些定制的帧索引,例如标记每个帧相对于其他帧的位置,这对于最终的绘制和调试都是必要的。


game_debug.cpp:用可分配值替换 GetDebugThread 中的 CollateArena 概念
现在需要做的是通过一种更有组织的方式来关联这两个独立的帧,而不是像之前那样随意地处理。第一步是摆脱当前的"合并区域"概念,转而使用实际分配的表值来管理这些帧。
在分配调试线程时,需要确保每个调试线程可以通过适当的方式获取。已经有一些现成的机制,比如 debug thread
,这些线程是否已经返回到一个空闲存储区呢?如果没有,那么就需要确保它们能够在需要时被正确处理。
关键的部分是,首先会有一些空闲线程(free threads)。在这种情况下,可以利用链表结构进行处理,比如插入删除操作,使用双向链表。在这个链表中,如果没有空闲线程,则可以尝试通过某种方法分配一个新的线程。
具体流程是:首先检查是否有空闲线程,如果有,从空闲线程列表中取出一个并更新空闲列表指针;如果没有空闲线程,那么就会通过某种方式分配一个新的线程并使用它。通过这种方式,确保了每次都能合理地获取需要的线程,并且维护空闲线程列表的完整性。
在实现过程中,还需要确保在分配线程时,如果需要获取一个线程并且当前没有空闲线程,那么就必须确保正确处理空闲线程的排队和重新分配。

game.h:#define FREELIST_ALLOC
我们现在想要整理一下这部分逻辑。虽然这段代码本身并不复杂,但可以考虑把它封装起来,使其结构更清晰。我们设想实现一个自由链表(free list)的分配器,也许加个锁之类的,用来安全地从链表中取出可复用的对象。
首先我们明确我们要实现的目标是什么:
我们需要从自由链表的指针中取出一个元素。如果能成功获取,就使用该元素的 next
指针更新链表头;如果不能获取,就通过 push_struct
或类似的方式,从内存池中分配一个新的对象。
我们需要几个基本要素:
- 当前自由链表的头指针;
- 分配失败时的备用分配方式(如
push_struct
); - 分配对象的类型;
- 分配使用的内存区域(arena);
我们可以设想这样的模式:
- 尝试从自由链表中获取对象
- 如果成功,更新头指针并返回该对象
- 如果失败,调用分配函数从内存池分配新对象
我们还可以借助 C++11 的 decltype
来自动推导类型,无需重复写类型定义。例如:
cpp
decltype(freelist_pointer->next)
可以让我们自动获取节点类型,而不需要手动指定。这样接口更简洁,也更安全。
我们还可以进一步将这个逻辑封装成一个函数(比如 freelist_alloc
),让它返回一个可复用的对象,调用者无需关心背后的分配逻辑。
然后我们还可以用更加简洁的方式写出这个逻辑,比如:
cpp
auto result = freelist_pointer;
if (result) {
freelist_pointer = result->next;
} else {
result = push_struct(arena, type);
}
或者,如果我们真的想写得极端简练,也可以把它压缩成一个三元表达式(虽然这样会牺牲一点可读性)。
总之,这部分内容的重点是实现一个通用的 free list 分配机制,在性能敏感的系统中,我们优先复用内存块,只有在 free list 空了之后才真正去分配新的对象,从而减少内存碎片和分配开销。这也是游戏引擎或底层工具中常用的一种模式。我们可以继续完善这个分配器,加入调试信息、线程安全支持或者对象清零等功能,适配更复杂的使用场景。


game_debug.cpp:在 GetDebugThread 中使用 FREELIST_ALLOC
如果我们想要实现这个功能,可以考虑通过宏来简化代码,创建一个通用的自由链表分配器。具体来说,这个宏的功能就是根据当前链表的状态决定是否从自由链表中获取内存,或者在链表空了之后,从内存池中分配新的内存块。
在实现时,我们需要考虑的一个关键点是如何安全地访问和修改指针。例如,我们可能会遇到错误,如果试图使用某个不合适的引用(比如尝试引用一个空指针)。为了避免这种错误,我们需要确保操作的是正确的指针类型,而不是一个引用。
此外,需要特别注意的是,first_free_thread
这样的变量是否已经正确地定义并初始化了。在设计自由链表时,first_free_thread
应该是一个指向链表头的指针,表示下一个可以复用的内存块。如果没有正确地初始化或分配该指针,就会导致后续操作失败。
因此,我们可以定义 first_free_thread
并确保它在整个分配流程中始终保持正确的状态。如果链表为空,我们就需要通过其他方式(如调用内存分配函数)来创建新的对象。如果成功从链表中取出对象,就更新 first_free_thread
指针,指向下一个可用对象。
网络:decltype1
在这段讨论中,提到了一个名为 decltype
的 C++ 关键字,目的是通过它来推导某个表达式或类型的具体类型。decltype
用来获取某个表达式或变量的类型,而无需提前知道该类型。这样做的目的是为了简化代码,避免手动指定复杂的类型。
然而,在实际使用时,decltype
可能会有一些细节和挑战。例如,使用 decltype
时,结果可能会返回一个引用类型(例如 T&
),这可能与期望的类型有所不同。这个问题在处理引用时尤其值得注意,因为有时不希望返回引用类型,而希望直接获得值类型。
另外,讨论中提到了"地址"(address)和"引用"(reference)之间的区别。通常来说,decltype
在处理表达式时,可能会根据表达式的实际类型返回一个引用类型(T&
),而不是值类型(T
)。这就会导致与原本预期的结果不一致。
在这种情况下,作者表达了希望返回一个非引用类型的需求,而不是 T&
类型。他们并不希望 decltype
返回一个引用,而是期望返回一个值类型。这个问题的根源可能是 decltype
在推导类型时的默认行为(例如返回引用),而这与他们的需求不完全契合。
game.h:与 decltype 作斗争
在这段讨论中,主要关注了使用 decltype
来推导类型时遇到的问题。首先,尝试使用指针引用时,编译器提示错误,表明"指针到引用"是非法的。这意味着无法从指针类型直接转换为引用类型,导致出现类型不匹配的错误。
接着,讨论中提到想要通过 decltype
获取指针类型,并对其进行一些操作,但编译器仍然抱怨无法完成所需的转换。尝试添加括号后,仍然无法解决问题。这表明可能有一些复杂的类型推导机制,导致直接获取类型变得困难。
在探索如何获取类型时,涉及了 typeid
的概念,它可以提供运行时的类型信息,但这并不是解决问题的关键,因为目标是获取编译时的类型信息,而 typeid
更多的是处理运行时类型信息。
进一步讨论中,虽然可以通过一些间接的方法尝试获取所需的类型(比如手动声明变量并进行推导),但这些方法显得过于繁琐且不直观。最后,表达了对这种看似简单的操作为何如此复杂的困惑,认为这本应是一个容易实现的功能,而现有的设计和类型定义方式却增加了不必要的复杂性。
总的来说,问题的核心在于如何简洁地获取指针的类型,并处理类型推导中的一些细节和限制,这让原本简单的任务变得复杂且不直观。
game.h:使 FREELIST_ALLOC 工作
可以通过这样的方法来解决问题,虽然可能不是最理想的方式,但最终仍然能使系统正常运行。虽然这种方法可能不是最优雅或者最符合预期,但最终能达到预期效果,并且在实际操作中不会造成太大的问题。总结来说,这种方式可以作为一种应急方案,虽然不完美,但在实际开发中并不会导致严重的后果,最终还是能让程序正常工作。
game_debug.cpp:引入 NewFrame 来初始化合并
现在的目标是改进与框架相关的假设,使得我们不再依赖以前的调试框架,而是能够使用协同框架。我们计划将所有原本在调试框架上执行的操作转移到协同框架上,这样的改动将从启动时开始,一直持续下去,不再使用重启协同机制。
新的结构将涉及创建一个"新框架"的过程,替代之前的重启协同方法。我们会创建一个新的调试框架,并初始化相关资源,如线程、框架条目的计数、调试记录等。新的框架将采用与调试相关的共享内存资源,简化了之前的复杂操作,使得调试功能更加统一且高效。
例如,调试框架会在每次新框架创建时通过设置零值进行初始化,仅初始化需要的部分,而不再保留不再需要的资源。框架条目的规模和线程初始化等也将被统一管理,这样在后续的执行过程中,无需再考虑额外的框架配置和临时解决方案。
此外,在创建新的"变量组"时,我们不再需要重复编码和命名这些协同框架的元素,而是通过通用的"创建变量组"操作来实现,这些操作会统一到调试内存区域中进行,减少了冗余的代码和管理负担。
经过这些改进,原本分散的框架资源将被整合到调试内存区域内,从而简化了系统的结构,并消除了临时解决方案所带来的各种问题。在执行过程中,框架的创建、调试记录的保存和协同操作将变得更加清晰和高效。
这些改动使得框架的管理变得更加灵活和统一,调试过程中的所有操作都可以在一个地方完成,避免了之前的一些复杂操作和资源的浪费。同时,这些改动也确保了在调试过程中,我们可以灵活地创建、更新和销毁框架,确保程序的正常运行。
game_debug.cpp:实现合并
我们在处理事件类型为 frame marker 的情况下,会执行以下逻辑:
首先,我们需要断言当前的 collation frame 是存在的。然后我们会将这个 collation frame 推送到一个列表中,也就是我们维护的帧集合中。
目前不太确定当初为什么移除了这个机制,也不清楚它之前为什么没能正常工作。理论上,我们现在应该将其重新命名为类似 "ui_say_it_can_enable" 的东西,表示我们可以重新启用这个逻辑。
一旦我们接收到这个 frame marker,我们会执行以下几件事:
- 增加帧计数;
- 更新调试状态;
- 将当前帧设为我们维护的"最旧无帧区域"的帧;
- 如果之前存在"最近的帧",则将当前帧设为其下一个帧,实现链式连接;
- 如果之前不存在任何帧(即链表为空),则将当前帧同时设置为"最旧帧"和"最近帧";
- 以上过程确保当前帧被加入我们维护的帧列表中。
这些步骤都比较直接。
接下来,我们需要做的是创建一个新的 collation frame。旧的帧已经完成了它的生命周期,需要被"退休"。因此我们需要为接下来的内容处理创建一个新的帧。
目前存在一个问题,就是 begin clock 的处理比较奇怪。我们不清楚什么时候应该开始记录时间。因此,我们需要在创建新的帧时显式地传入 begin clock,因为我们无法在函数内部自行得出一个准确的起始时间。
只要 begin clock 被正确传入,其它逻辑应该都能正常工作。现有的流程逻辑可以大致保留,只是某些部分可能需要稍微调整。
另外,帧区域(regions )部分也需要做得更复杂一些,目前的实现还不够完善。需要在后续对其逻辑做更精细化的处理。
game_debug.cpp:提供获取新帧的能力
我们当前的目标是为新的帧提供所需的信息。
一开始原本的想法是直接初始化这个新帧的某些值,但现在决定改为将其初始值设为零,然后在第一次处理事件时再赋值。具体来说,每当我们处理一个 debug frame marker 的时候,就会假设当前帧需要被"退休"(retire)。但实际上,无论遇到什么事件,我们都应该默认这些事件必须属于某个 collation frame。
因此逻辑上,只要发现有事件发生,就需要确保存在一个 collation frame 。我们需要检查当前是否已经有一个帧了,如果没有,就懒加载地创建一个,并使用当前事件的 clock 时间作为帧的起始时间。这是因为懒加载方式确保了我们总会有一个可用的 clock 时间值可以传递。
接下来的逻辑是,只要进入事件处理流程,我们就可以断言 collation frame 一定存在。这成为一种强制性要求,不能再存在没有帧的事件。通过这个机制,我们不再需要在处理逻辑中做繁琐的条件判断,只需直接断言当前帧存在即可。
当我们调用 collect debug records 的时候,处理方式也需要调整:不再是针对特定的 collation array 进行处理,而是对整个事件数组进行统一处理。
该函数将遍历事件数组,处理其中的每一个事件,并将其"退休",也就是从活跃状态中移除。它只需要传入两个参数:事件数量以及事件数组本身。这个函数的职责非常明确,就是完成这些事件的处理和清理工作。
至于事件的来源(即具体使用哪个 buffer),可以留待上层来处理,我们不在这个模块中关心它的具体来源。这样实现可以保持逻辑的清晰和独立性。
game_debug.cpp:处理编译错误
在当前的设计中,我们对调试框架的内存和事件处理流程进行了多项调整和简化,以下是详细总结:
帧的处理逻辑更新
我们现在不再依赖于 FrameBarLaneCount
来处理帧的状态,因为这些信息已经转移到 debug_state
上进行维护。之前的 collation
结构中曾负责这部分内容,但现在被归类到更高层次的调试状态中。
在处理调试帧时,使用了 lean index ring FrameBarLaneCount
的机制,每次需要时会抓取当前可用的区域,这种方式是正确的,逻辑并没有发生变化。
调试块的分配逻辑统一
我们在重新分配 debug blocks
时,始终通过 debug_arena
来进行内存分配。这一机制现在与自由帧(freelist)中所采用的方法完全一致:
- 先检查是否有可用的空闲指针;
- 有则推进指针;
- 没有则重新分配。
这个逻辑已经被抽象并重用,无论在哪个上下文中都适用。变量的命名统一采用 next_free
,并通过 arena
来分配内存。
在调试帧结构中,我们为 next_free
提供了一个占位空间。这个空间并不额外占用新的字节,只是借用了结构内未使用的部分,例如之前可能用作 parent_pointer
的空间。只要大小没有问题,这种复用是可行的。
帧回收逻辑更新
我们现在的 freelist 分配函数只需要处理 next_free
指针的推进,整体结构简洁明了。
与此同时,我们清理了一些旧逻辑,例如 create_variable_group
中的冗余代码,还有一些 permanent
变量也不再需要,已被移除。
事件计数与事件数组的更新
事件计数的获取逻辑也发生了变化:
- 不再使用原来的逐帧递增方法;
- 事件数组的索引采用双缓冲策略,在 0 和 1 之间交替;
- 不再需要复杂的包裹(wrap)逻辑。
事件数组的索引通过原子操作切换,最多只会有两个 buffer,所以只需要 1 bit 来表示状态,完全可以使用 uint32
来代替 uint64
,节省空间。这其实早就可以优化,只是之前没做而已。
事件收集流程简化
collect_debug_records
函数现在只需传入两个参数:
- 当前使用的事件数组索引;
- 该数组对应的事件数量。
其职责是处理并"退休"所有事件,将其从活跃状态移除。
暂停状态下的帧丢弃逻辑
为了更好地处理"暂停"状态下的帧行为,我们做出如下策略:
- 如果当前系统处于暂停状态(paused),则当前帧会直接被丢弃;
- 否则,当前帧会正常被使用,并推进帧链表;
- 无论暂停与否,我们依然进行帧的 collation,以确保后续的事件不会遗漏;
- 这些未被立即处理的帧我们称之为"stragglers"(落后帧),它们依然会在后台被处理;
这个处理方式可以兼顾暂停期间的完整性,也保持逻辑的清晰性。
其余改动与清理
- 移除了不再需要的旧代码,如
refresh_escalation
; - 简化了原本用于 buffer 切换和计数处理的复杂逻辑;
- 明确了一些原子变量的使用细节和边界条件(如断言索引仅为 0 或 1);
- 为帧处理提供了更稳健的回收和状态同步机制。
总结来说,这一系列更新主要实现了:
- 简化帧与事件处理逻辑;
- 统一内存管理机制;
- 加强暂停状态下的健壮性;
- 通过懒加载和原子操作提升性能与可读性;
- 移除历史遗留代码与冗余逻辑,提升系统整洁性。
整体上,我们正在构建一个更模块化、自动化、且高效的调试记录与帧管理系统。



game_debug.cpp:引入 FreeFrame
在当前的调试框架逻辑中,我们进行了一些进一步的实现安排与结构修正,以下是详细的中文总结:
帧的清理函数定义
我们为释放调试帧预留了一个内部函数 free_frame
,当前暂时未实现,其作用是在未来用于回收和释放不再需要的调试帧资源。
这个函数目前仅包含一个 assert not implemented
,意味着我们已经确定了这个操作的存在必要性,并将在稍后进行补充实现,用于统一帧的回收逻辑。
调试数据结构命名修正
在代码中对调试相关的数据结构做了命名上的澄清与修正:
- 原先错误地引用了一个变量
debug
,实际上我们真正需要使用的是全局的debug_table
; - 在
debug_table
中引用事件数组成员时,原本使用了错误名称event_erasure
,实际上正确的成员名称应该是events
; - 这表明我们已经理清了调试表结构的实际构成,并对事件记录的读取和处理路径进行了校正。
小结
- 明确预留了调试帧回收的钩子函数,后续将作为释放逻辑的核心;
- 修正了对调试表结构成员的误用,确保正确使用
debug_table.events
来访问事件数据; - 确保整个事件收集与帧管理模块在命名和逻辑上的一致性与健壮性;
这些调整虽然是局部修正,但对于系统稳定性和后续可维护性具有关键意义,为实现完整的帧生命周期管理和事件处理链打下基础。
game_debug.cpp:修复一些复制粘贴的代码
目前在处理调试帧(debug frames)内存分配与增长方面,我们的逻辑基本完成,但也存在一些重要的注意事项与后续可能出现的问题,以下是具体的总结:
帧无限增长问题
当前设计下,调试帧的分配逻辑并未设置上限或者回收机制,意味着:
- 每次新的调试事件产生,我们就会不断地创建新的帧;
- 如果这个过程持续下去,内存中的帧将会无限增长;
- 最终会耗尽为调试缓冲区分配的所有内存空间;
- 当帧数量超过我们允许的最大帧数限制时,将触发断言(assert)失败,系统中止运行;
- 换句话说,当到达极限后,程序将无法继续生成新的帧,进入"没空间"的错误状态。
这是一种"懒回收"机制,也就是说我们先不管释放,等空间耗尽后通过断言暴露问题。
关于复制粘贴错误(Copy Pasta)
在代码中发现了"复制粘贴"导致的命名错误:
- 有一处原本应当设置为"结果帧"(result frame)的字段,被错误地使用了"最老帧"(most mutilation frank),
- 这一错误可能源于之前某段逻辑的复制粘贴时未做适配,必须进行修正;
- 实际上该字段会被后续的
zero_struct
初始化处理覆盖掉,但显式地修正仍然更为安全与清晰。
总结
- 当前帧会无限增长,直至内存耗尽或触发断言;
- 缺少帧的回收或复用机制,存在明显内存隐患;
- 复制粘贴造成命名错误,需修复以避免潜在混淆;
- 结构初始化会掩盖部分命名问题,但不应依赖其自动掩盖功能。
这些问题虽不影响调试框架的基本运行逻辑,但从可维护性和系统健壮性角度来看,需要后续进一步引入帧池管理或自动回收策略,以防内存泄漏与异常终止。
运行内存耗尽
在运行游戏过程中,调试系统如预期般出现了内存耗尽的情况,整个过程验证了我们此前的设计假设,具体总结如下:
调试帧持续增长直到内存耗尽
- 游戏运行后,调试系统持续创建新的调试帧;
- 因为当前系统没有对调试帧数量进行限制或回收机制,所以这些帧不断增长,占用内存;
- 最终,当内存使用达到系统分配上限时,触发了内存耗尽的状态;
- 这正是事先设计中预期的断点,目的是检测调试系统在极限状态下的表现;
- 系统此时输出提示:"你已经耗尽了内存",这是预设的断言行为。
系统行为符合预期
- 当前行为验证了调试系统在资源极限时能够通过断言机制清晰报错;
- 说明事件收集和帧分配逻辑在基本路径下是正常工作的;
- 同时也暴露出帧管理缺乏回收机制的问题需要后续解决;
- 这种"以失败提醒开发"的方式虽然原始,但也可以帮助我们尽快发现潜在的资源泄漏。
总结
- 调试帧系统已验证在高负载下会持续分配帧直到耗尽内存;
- 当前并无自动回收或复用机制,系统最终通过断言报错;
- 系统如预期般提示"内存耗尽",验证断点逻辑有效;
- 后续需引入调试帧回收策略,避免内存不断增长导致崩溃。
整体来看,这次运行是一次成功的压力验证,证明系统核心机制正常,同时也为资源管理优化提供了明确方向。
game_debug.cpp:实现 FreeFrame
这一阶段我们开始实现调试系统中 内存回收机制,这是首次在整个项目中引入类似"析构器"的逻辑,之前大多数情况下并不需要手动释放内存,因为大多数数据结构是持久化存在的。而本次则是为了应对调试帧无限增长导致内存耗尽的情况。以下是详细的中文总结:
目标与背景
- 目前调试系统会不断创建新的调试帧(frame),没有回收机制;
- 当调试帧数量无限增长时,会耗尽内存,触发断言;
- 为了解决这个问题,我们要实现调试帧的释放与重用机制;
- 这是首次在调试系统中手动回收资源,是一种"析构"风格的处理方式;
- 虽然游戏主逻辑依然不处理这类问题,但调试系统作为附加组件确实需要它。
实现思路
建立 Frame 回收机制
- 我们准备在
debug_state
中新增一个 frame 的 free list; - 当某个调试帧不再使用时,我们会将它放入这个空闲列表;
- 下次需要新的帧时,优先从 free list 中取,而不是重新分配内存;
- 这种机制减少了反复申请/释放内存的开销,提升效率。
Free List 操作逻辑
- 回收到 free list 的时候:
- 如果当前指针非空,就把当前 free list 指针挂到这个帧的
next_free
上; - 然后把这个帧设置为新的 free list 头部;
- 如果当前指针非空,就把当前 free list 指针挂到这个帧的
- 从 free list 分配时:
- 从头部取出一个帧;
- 如果取到了,清零结构体内容,但保留原有的 region 指针(即内存区域);
- 如果 free list 为空,则进行正常分配。
技术细节与注意点
保留 region 指针
- 因为一个 frame 包含了一个指向内存区域(region)的指针;
- 在回收到 free list 时,要保留这个区域,以便重用;
- 清零结构体内容时必须注意跳过 region 成员;
- 目前没有方便的方式清除结构体中除了某个字段以外的内容,因此需要手动处理。
Debug Variable Group 也需回收
- 除了 frame,本轮也需要支持 debug variable group 的释放;
- 调试变量组与帧有类似的生命周期,也应支持回收到各自的 free list;
- 这部分的释放逻辑暂未完全展开,但作为下一个工作重点。
小结与展望
- 引入了调试帧的内存回收机制,采用 free list 实现;
- 优化了内存使用,避免调试帧无限增长导致崩溃;
- 在清零结构体时注意跳过保留的 region 指针;
- 后续还需实现对 variable group 的完整释放;
- 当前实现可能稍显笨重,未来随着内存分配区域机制的改进可进一步简化。
通过这次处理,我们第一次在系统中引入了内存释放的逻辑,为后续复杂系统的资源管理打下了基础。虽然只是用于调试系统,但体现出了系统工程中的必要考量与健壮性建设。

game_debug.cpp:引入 FreeVariableGroup
我们回顾并检查了当前调试系统的内存管理实现状态,确认目前结构基本完善,只剩最后一点工作未完成,以下是详细中文总结:
当前已完成内容概览
- 成功引入了调试帧(debug frame)的 free list 分配与释放机制;
- 对调试帧的申请过程进行了封装,使用
FreeListAllocate
方法进行管理; - 实现了将不用的调试帧放回 free list 的逻辑,实现内存重用;
- 为
VariableGroup
(调试变量组)也准备了相应的 free list 支持; - 已添加结构体及分配函数的雏形,并完成了基本布局;
- 调试帧的重用逻辑已能正常工作,内存耗尽问题得以规避。
待完成内容说明
- 调试变量组的释放逻辑(FreeVariableGroup) 尚未实现;
- 这一部分需要补充 free 操作,把不再使用的调试变量组也放回 free list;
- 与帧的释放类似,变量组的释放同样涉及链表指针的操作;
- 因为时间关系,目前暂停在这一阶段;
- 后续工作中将继续补全变量组的释放流程,实现完整的内存循环机制。
此阶段工作为调试系统建立了核心资源管理能力,解决了长期存在的内存积累问题,为系统的长期稳定运行打下了坚实基础。
运行游戏并看到我们仍然触发 Arena->Size 断言
目前的进展基本上已经接近完成,但仍存在一些关键问题需要解决。我们目前还无法真正继承旧系统的行为,因为当内存达到一定量时,会触发断言,导致程序中断。这种情况表明,在达到内存上限之前,系统还没有正确地进行内存回收。
具体总结如下:
-
断言触发问题 :
当前系统在不断分配内存以构建调试帧时,一旦内存分配到达上限,就会触发断言。这说明旧系统的"遗留"数据还无法正常获取,因为一旦分配超过预期,就会马上报错。
-
进度与计划 :
我们整体的工作已经接近尾声,代码大体上已经完成,只差一些细节上的完善。断言触发问题和内存回收机制还需要进一步处理,计划在休息回来后详细查看和调试这部分内容。
-
后续完善内容:
- 增加触发内存释放的机制,尤其是在内存分配时需要调用帧释放函数,比如在 arena push 时触发释放调试帧。
- 在调试帧的整理(collation)过程中,需要正确设置触发条件,确保当内存达到临界状态时可以及时释放资源,而不是直接触发断言。
- 总结来说,这部分工作只是最后的收尾,主要需要做进一步的内存回收逻辑优化,以完成整个调试系统的闭环。
总体来看,我们的系统在不断接近预期目标,只需要增加一些"爱心"来完善这一收尾阶段,确保内存释放的机制能够在内存紧张时自动触发,从而避免程序由于内存耗尽而报错。
game_debug_interface.h:确保调试系统可以被编译掉
目前正在处理调试系统的一些清理和兼容性问题,目的是让其他人可以更方便地使用和编译游戏代码,即使不启用调试系统也不会受到影响。以下是本阶段的详细总结:
主要目标:让调试系统支持条件编译
- 目的是为了更好的用户兼容性 :
为了避免玩家或其他使用源码的人在没有使用调试系统时仍被迫处理调试相关的代码,需要让调试功能支持被条件编译掉。
操作步骤与处理方式:
-
进入调试接口相关代码位置(game debug interface):
- 打开调试接口所在的模块,对调试相关的宏(macro)进行更新。
-
宏的设置与调整:
- 检查并更新
#ifdef
或#if
相关的条件编译宏,确保在未定义调试功能的情况下,调试系统的代码不会参与编译过程。 - 比如移除或重构
#define GAME_INTERNAL
等宏,避免不必要的预处理定义导致编译混乱。
- 检查并更新
-
简化条件判断逻辑:
- 比如将
#if defined(...)
的判断改为更加清晰的表达方式,确保只在特定条件下才启用调试系统的相关模块。
- 比如将
-
问题与修复:
- 在过程中发现了一些语法错误或宏使用不当的地方,例如多余的定义、括号缺失等。
- 修正这些问题,确保条件编译的逻辑能正确生效,不会导致编译器报错。
当前状态与后续安排:
- 目前调试系统的条件编译逻辑正在逐步完善。
- 语法方面的一些小错误已经被识别出来并正在修复。
- 接下来会进一步确认整个调试系统在不启用宏的前提下是否能被完全跳过编译,以便确保用户在构建游戏时的灵活性和简洁性。
这个阶段的主要意义在于:优化可维护性和可移植性,确保即使移除调试相关内容,整个游戏的构建过程依然顺畅,不会引入多余负担。
完成合并后,我们会开始做层次结构吗?
我们已经完成了所有的工作,一旦整理(collation)完成,接下来就会进入层级(hierarchy)处理阶段。层级部分非常简单,因为之前已经实现了相关的功能。现在只需要对字符串进行搜索,基本上就是字符串匹配的操作,非常直接和轻松。
目前的重点和难点是在于确保整理工作的正常运转,并且能以一种稳定、持续的内存循环方式运行。这是整个过程中真正困难的部分。
至于编程方面,现在没有特别紧急的问题需要处理,看起来一切都比较平稳。
你会做一些编译时字符串搜索的技巧吗?
我们提到了"quarter tron",但这个词的具体含义不明确,似乎是一种误解或误听,不清楚是指什么内容。接着有人询问我们是否会在编译期对字符串搜索做一些技巧性的优化,例如使用编译时常量、模板元编程或其他静态分析手段来提升字符串匹配的效率。
我们的回答是否定的,暂时没有打算在编译期做这种优化处理。整体意思是我们目前不打算在编译时对字符串搜索进行特别的技巧或优化,当前的做法足够满足需求。
我曾在最后一家软件公司工作,如果现在看到这些代码审查,我会惨败。你对这些做法怎么看?
我们讨论了关于代码评审制度的一些看法,认为如果现在去参加代码审查,可能会被狠狠地打回票,而这本身也反映出我们对某些开发实践方式的质疑。
我们观察到一个趋势:越是重视和强调代码评审的公司,最终产出的软件质量往往并不理想。比如某些大型公司对代码审查的要求非常严格,几乎每一行代码都要经过审核,但他们的产品却常常问题重重,修复一个功能性问题甚至要花上几年时间。例如,一些简单功能的缺失,用户在论坛上抱怨多年都没有解决。这种现象让我们觉得,从结果来看,这种高压式的审查制度并没有带来更高质量的交付,反而效率低下、问题频出。
我们认为,如果有人推崇这些开发流程,希望让我们相信它的价值,那么他们首先应该能交付高质量、性能优越、及时更新的软件。而实际情况是,这些高度依赖代码审查的团队常常无法做到这些。
相比之下,一些不那么强调代码审查的行业,比如游戏开发,反而能定期、高效地发布大型、复杂而且性能卓越的产品。游戏开发团队每年都能推出内容截然不同的新作,而那些执行严格审查流程的公司却常年难有改进,这种反差让我们质疑审查机制的实际效果。
从外部视角看,越依赖代码审查的团队,发布速度越慢,质量越差,软件复杂度也往往更低。这使我们产生疑问:是否问题根本不在代码审查本身,而是整体流程的问题?或者也许代码审查确实没那么重要?
我们并不完全否定代码审查。在特定情况下,如果我们遇到具体技术问题,也可能会主动邀请他人提供意见,第二双眼睛确实能带来帮助。但我们对系统化、流程化的代码审查持怀疑态度,认为其效益有限,甚至可能徒增阻力和成本。
你使用 SQL 吗?如果使用,你是如何结构化你的数据库的?
我们有使用过 SQL,不过具体"如何结构化数据库"这个问题太开放了,不太确定提问的重点是什么。
SQL 本身是我们熟悉和使用过的工具,但数据库结构的设计通常依赖于具体的项目需求、数据模型、性能预期以及系统复杂度等因素,因此没有一种统一的标准答案。这个话题可以很广泛,也可以很具体,比如是否采用范式化、如何设计表关系、是否使用索引优化查询、如何处理扩展性和数据一致性等。
所以我们对于这个问题的回应是:我们确实使用过 SQL,但如果要具体谈数据库结构,还需要更明确的上下文或细节,才能给出有针对性的回答。
我想所有的 "路径" 在编译时都会是已知的,所以你可能不需要使用 strcmp,但是我不确定是否直接使用地址会有效
我们在讨论系统是否能够运行的问题。虽然所有路径在编译时可能已经是已知的,因此理论上可以避免进行字符串比较(string compare),但我们也不完全确定编辑器在这种情境下是否能正常工作。
实际上,我们仍然需要进行字符串处理操作(如字符串计数或比较),原因在于可执行文件在重新加载时,其内存地址会发生变化。因此,路径信息在运行时仍需要被解析,这样我们才能正确地重新关联和对齐相关的数据。
此外,我们还需要对路径进行解析,以便在内部结构中进行正确的层级定位。我们的系统通过使用下划线 _
来表示层级结构,因此必须解析路径字符串,将其映射到正确的层级体系中。路径解析不仅有助于结构化管理资源,还确保了在重新加载或解析过程中层级信息的准确对齐。
顺便说一下,我在 clang 上试过 decltype(),它在同样的上下文中有效(我想)。cl 可能是坏的
我们尝试在某种上下文中解决类型和类的问题,结果发现它成功了。我们认为,可能存在某些问题,尤其是对于 CL 的实现,可能已经出现了问题。不过,这种情况并不是第一次发生,类似的情况已经有过多次。
另外,提到的 Clang 通常比 cl 更加灵活,能够提供更高的适应性和更多的配置选项。这也解释了为什么在一些特定场景下,Clang 会表现得更好、更有弹性。
能给我们一些编程挑战,直到你回来吗?
我们提出了一个编程挑战,目的是优化一个之前没有优化过的函数。在渲染错误处理的部分,有一个绘制矩形(draw rectangle)函数,它目前只是绘制一个填充的矩形。这个函数没有经过性能优化,因此我们建议首先对它进行性能测试,计算它当前的执行时间,然后进行优化,看看能在多大程度上提升执行速度,和最初的执行时间相比能够提高多少。
这是一个很好的挑战任务,目的是通过优化一个简单的图形操作,来提升渲染性能。
写一个使用数据库模型并将其逻辑作为数据库转换的游戏是否可行?
使用数据库模型来处理游戏的逻辑和数据转换并不是一个推荐的做法。尽管我们曾经尝试过这种方法,但发现数据库并不适合游戏通常需要的操作。
数据库存在一些严重的问题,这些问题使得它们在游戏的使用场景中并不适合。虽然数据库在许多其他方面表现良好,比如强大的事务管理、数据一致性和持久性等,但它们在处理像游戏数据那样具有图形结构(如图论和空间搜索)时表现很差。传统的关系型数据库尤其在处理这些"图形性"的操作时有很大局限,尤其是在进行复杂的图遍历和空间搜索时,数据库往往会崩溃或表现非常差。
如果游戏只是存储一些基本信息,比如玩家的分数或比赛结果,数据库完全适用,因为这些任务数据库本来就擅长处理。可是如果游戏的逻辑本身也依赖数据库,尤其是复杂的游戏逻辑,传统的关系型数据库就不太适用了。这样的数据库适合存储行和列的关系数据,而不擅长处理图结构或类似的复杂数据关系。
如果真的要尝试将数据库用于游戏的复杂逻辑,可能需要使用一些更现代的数据库技术,而不是传统的 SQL 数据库。例如,图数据库(Graph databases)更适合处理这种复杂的图论问题,而传统的关系型数据库(如 SQL)则完全无法应对这种需求。因此,如果需要在游戏中使用数据库,可能需要考虑一些更先进的技术,而不是依赖旧有的关系型数据库。
我的脑袋一直在想着你和 Jon 在预播中谈到的那个 scratch memory 问题,抱歉如果有些离题。它是一个每个线程都保存的内存(并且会稍微增长)。我理解的是否正确?你是将它传递到调用栈中,这样每个函数都可以从中推送和弹出一块 scratch memory 吗?
我们讨论了关于线程的"临时存储"或"scratch memory"的问题。基本上,这是一个在每个线程的调用栈中传递的内存片段,每个函数调用时会将一块内存"压入"栈中,函数结束时会将其"弹出"。这种方式的目的是为了避免依赖线程本地存储(thread local storage),特别是在一些情况下,线程本地存储可能并不可用,例如在使用共享库时。
为了解决这个问题,通常的做法是将每个函数的第一个参数设为一个"上下文(context)",并且这个上下文是每个线程特有的。这样可以为每个线程分配一个专用的内存堆,每个线程可以使用自己的堆进行内存分配。只有在多个线程需要竞争堆内存时,才会出现竞争问题。
我通常的做法是让这个内存缓冲区是可扩展的,并且它会在线程中持续存在,直到线程的需求增长到一定程度,内存大小就会固定下来,之后不会再做太多修改。这种方法非常实用,尤其是在避免频繁调整内存大小时,可以提高效率。
我以前从未想过元编程,现在听起来很有趣。你有什么更多的信息来源推荐吗?
我们讨论了一个相对冷门的编程概念,虽然这并不是现在大家经常讨论的内容,但它在过去是比较流行的,尤其在以前的编程环境中。虽然这个概念现在不常被提及,但我们个人对它非常喜欢。
至于更多的信息,我们并没有具体的推荐资源,因为这是一个相对较为隐晦的领域,鲜有人深入讨论。它有些属于"老派"技术,过去曾较为流行,但随着时间的推移,它逐渐被现代的其他技术所取代。尽管如此,依然有一定的魅力,值得对它感兴趣的人去探索。
如果你同时按住并拖动窗口的标题栏,粒子的(简化的)碰撞检测会发生什么?因为在 Windows 的拖动/大小调整消息循环中,游戏定时器会暂停吗?
我们讨论了当同时按住并拖动窗口标题栏时,粒子系统的行为。通常在这种情况下,窗口的消息循环会处理拖动操作,这可能会影响游戏的表现。实际上,粒子系统在这种情况下可能不会有太大的不同,因为我们在测试时并没有在游戏中保持运行状态,所以很难观察到粒子的变化。
但如果你问的是关于更一般性的情况,比如在帧率急剧波动时,碰撞检测系统的表现,答案是肯定的------它们会出现问题。在这种情况下,粒子系统和其他基于物理的系统可能会发生不稳定,尤其是当帧率发生剧烈变化时。
如果将来要在游戏中实现真正的粒子系统,就需要特别注意这一点。为了避免帧率剧烈波动带来的问题,我们需要限制最大帧率,确保它不会超过某个阈值。这样可以保证游戏中的算法在不同帧率下都能正常运行,避免由于帧率变化引起的物理计算错误。设计时需要格外小心,以确保物理引擎和粒子系统能够在各种帧率下稳定工作,避免定义的物理行为在帧率波动时出现问题。
如何 "抽象" 一个数学库?比如,游戏/引擎使用 v2/v3/v4 类,而你必须使用图形库中预定义的向量类来渲染,但你又不想将游戏与其中某个库耦合在一起?
我们讨论了如何抽象化一个数学库,尤其是在游戏引擎中,假设引擎使用了一个三维向量类(例如,v3或v4类),用于渲染等图形操作,但我们又不希望游戏与这个特定的库紧密耦合。
我的做法是避免使用这些库所提供的具体向量类。如果一个库不能接受像浮点指针(float pointer)这样的通用数据类型,那么我就不会使用它。换句话说,最理想的做法是保持尽可能的灵活性,不让游戏的核心逻辑与某个特定的图形库或数学库紧密绑定,确保能够自由选择其他工具和库,而不会限制实现的方式。