运行游戏,开始今天的开发工作。
我们继续游戏代码基础上进行重构,目标是实现更多的性能分析界面功能,尤其是调试用的用户界面。
目前运行游戏并打开性能分析窗口后,发现界面功能上还有不少缺陷。现在的界面可以向下钻取查看具体某一部分的性能数据,但缺乏"返回"功能,无法方便地回到上一级视图。因此,我们需要添加顶部的按钮区,至少要有一个"返回"按钮。此外,还希望能在不同视图之间切换,例如"单帧视图"和更高级别的总览视图,只要添加一个按钮就可以轻松实现这个功能。
除了顶部按钮,还希望在界面下方的信息区域添加一些操作按钮,比如"暂停"按钮。因为性能信息是实时滚动更新的,所以需要一个暂停功能,用于暂时冻结当前界面,查看某一帧的数据。同时,也希望可以添加左右切换按钮,以便在暂停后能浏览之前的历史帧数据,回顾性能变化。
总之,计划在性能分析界面的顶部和底部都增加交互按钮,以实现更强的帧数据浏览、暂停和切换功能,让整个性能调试过程更加直观和可控。
在 game_debug.cpp
中引入 DebugType_LastFrameInfo
和 _DebugMemoryInfo
,并将它们集成到系统中。
我们准备将一些界面元素调整到顶部区域。目前有一部分旧的界面绘制逻辑被称为"主菜单",但实际上这个命名已经不合时宜,属于早期遗留下来的命名方式,这类遗留问题在直播时不常有机会清理,但在私下里有充足时间开发时通常都会处理。
现在的目标是将调试文本绘制完之后,再开始绘制主菜单部分。当前的界面元素绘制存在一个 Y 坐标的参考变量,我们需要确保绘制顺序合理,从而不会出现重叠或遮挡。
初步观察代码,发现现在是在绘制调试界面树形结构,每个树节点都是单独绘制的。如果继续沿用现有方式,就需要为每个节点单独调整位置。但这样做太麻烦,感觉没有必要去做这种特例处理。与其如此,不如将这些调试信息统一作为可以控制的"元素"进行管理,更合理。
因此,我们计划将这些原本写死的位置绘制逻辑全部替换,改为作为调试元素(debug element)的一部分统一绘制。这种方式和性能分析图表等其他调试元素绘制方式一致。我们已经在 DrawElement
相关函数中定义了不同类型的调试元素,现在只需要将这些信息(比如最近帧的信息、内存调试信息等)作为新的调试元素类型加入系统。
比如,我们可以定义一个名为"最近帧信息"(last frame info)的调试元素,以及一个"调试内存信息"(debug memory info)的元素。将它们像其他调试元素一样进行布局与渲染,不仅结构更清晰,也便于之后的维护和功能扩展。
目前的布局流程是:当拿到一个文本内容后,会计算其尺寸,然后生成对应的调试元素并排布到调试界面中,最后渲染显示。考虑到这一流程比较重复,已经略显臃肿,我们还计划将这部分逻辑封装成一个工具函数,提高代码复用率和清晰度。
总的来说,这一步是将之前写死的调试信息渲染逻辑,转变为统一、可配置的调试元素系统中的一部分,便于后续的功能扩展与界面优化。


在 game_debug.cpp
中引入 BasicTextElement
类型。
我们打算为绘制基础调试文本元素创建一个工具函数,用于简化逻辑,统一处理流程。这个函数是内部使用的,主要目标是处理类似"基础文本元素"的绘制,这种元素只需要文本内容、布局信息和简单的交互配置即可完成显示。
打算命名为类似 BasicTextElement 的东西。这个函数需要的参数包括:
- 要显示的文本内容;
- 当前的布局(layout)信息;
- 一个交互行为(item interaction),这个交互函数是可配置的,因为不同场景可能需要不同的交互方式;
- 可选的颜色参数,默认为白色,避免在无特殊需要时必须显式指定颜色。
大致的实现方式是,将这些参数传入函数后,由函数内部处理文本的尺寸计算、元素生成、位置布局以及最终绘制显示等逻辑。这样就可以避免在主逻辑中重复手写大量处理代码,提高开发效率。
这个思路源自对已有代码的观察:我们已经封装了 debug state
,不需要显式传入;文本绘制的核心逻辑是通用的;大多数绘制流程差别只在文本和颜色。因此通过封装这部分逻辑,不仅减少重复代码,也为后续维护和功能扩展打下良好基础。
接下来我们打算先试着把这个工具函数用起来,看它是否按预期工作。如果可以顺利运行,就可以正式整合进整个调试界面系统中。同时,也意识到现在使用的编辑器存在一些不便之处,像是在输入这类封装函数时效率不高,可能需要升级编辑器版本来提升开发体验。
编译并运行,验证 BasicTextElement
是否正常工作。
我们正在考虑是否能够使用当前的解决方案来打印菜单,并且已经进行了一些测试。首先,我们检查了文本的重新加载功能,并确认它能正常工作。接下来,我们尝试将这种方法应用到其他需要的地方,并计划利用它来处理这些任务。我们认为,只要当前的系统可以支持这些操作,就能够有效地应用到不同的部分,尤其是菜单打印方面。通过这种方式,我们能够保证在处理不同类型内容时,系统的稳定性和高效性不会受到影响,从而满足我们的需求。
在 game_debug.cpp
中进一步传播 BasicTextElement
的使用。
我们决定不再传递颜色参数,因为我们当前的样式已经是纯白色,没必要再额外指定。这种设定听起来就像是一部低质量的迪士尼电影。我们所需要的只是一些基础的文本展示,没有复杂的样式,也不需要多余的内容。实际上,之前准备的一部分内容也可以去掉,因为已经不再需要它们了。到这个阶段,我们所要做的事情已经非常明确,只需要保留必要的内容并进行简化处理即可。
在 game_debug_interface.h
中定义新的 DebugTypes
和 DEBUG_UI_ELEMENT
。
我们已经定义了这些特定的小组件,在我们庞大的调试系统功能列表中,这部分可以作为一个接口存在。现在,我们可以在这个系统中继续扩展更多功能,比如添加用于调试内存信息的内容,或是线程间隔图表等模块。
我们现在的做法是像以前那样处理线程图表调试,接下来我们计划添加一个用于调试 UI 的模块,比如叫 debug UI 或 debug UI element。我们只需要传入一个类型标识,比如"某某类型"的名称,然后构建对应的调试组件即可。
这样做的优势在于我们不必为每个不同的调试元素单独创建一个函数,因为我们本来就需要输入类型标识,那就直接利用这个标识生成所需的元素。只有在未来需要传入额外参数或者对某些元素进行特别处理时,才有必要另外写一个函数处理,而在当前这种情况下,统一用一个函数来处理所有调试元素是最简单高效的方式。
现在的设计逻辑是将这些调试组件的创建流程统一整合,只需调用一个函数并传入所需的类型标识即可,简洁明了,扩展性也强。虽然目前我们还没有特别确定具体要添加哪些 UI 元素,也许是上一帧的某些状态信息之类的,但框架已经准备好了,后续只需填充具体内容即可。




运行游戏,查看性能分析器(profiler)界面。
现在我们已经把这些组件集成进来了,可以看到,我们不仅保留了之前实现的性能分析(profile)功能,同时也成功引入了我们想要使用的旧数据。这一步的完成,使我们在整体架构中处于一个非常理想的位置,可以进一步开发和生成更多的 UI 控制元素,这是我们目前的目标之一。
值得注意的是,现阶段的结构已经为添加和展示新的 UI 控制提供了良好的基础。这意味着我们可以在此基础上扩展各种调试工具或界面交互组件,增强系统的可视化能力和调试便利性。不过在深入之前,还有一些细节值得进一步探讨,比如某些地方为何会出现特定的行为,这一点引发了我们的兴趣和疑问,有待后续深入分析和验证。
在 game_debug.cpp
中调查为什么 DebugTypes
出现顺序是反的。
我们注意到现在这些元素的顺序似乎是错的,怀疑在向集合中添加元素时使用了一种"向前插入"的方式。按照目前的观察,元素的顺序似乎是与它们添加的顺序相反的,这引发了我们对其底层数据结构的怀疑。
我们推测这里使用了某种单向链表结构,而且这个链表并不是在尾部添加新元素,而是在头部插入,这也就导致了元素顺序颠倒的现象。从代码中可以看到,当我们调用 add element to group
的时候,实际内部执行的是 d-list insert
。这意味着确实是以插入头部的方式进行的添加。
这种做法可能在某些场景下是有意为之,比如追求插入效率或为了快速访问最新添加的元素,但当前的使用方式下,它会破坏我们预期中的顺序。因此,这成为了一个需要处理的问题,值得进一步深入分析并可能调整插入策略。
为了进一步确认,我们查看了 get element from event
的实现方式,发现它确实如我们预期地调用了 add element to group
,而这个过程中的插入操作使用的正是我们怀疑的问题函数。整体流程已经清楚地暴露出插入顺序上的问题,也证实了我们对链表行为的判断。我们将基于这个发现,考虑是否需要改用尾插方式,或者对结果顺序做出后处理,以确保最终逻辑的正确性。
这两个宏 DLIST_INSERT
和 DLIST_INSERT_AT_LAST
都是用于双向链表(doubly linked list)的插入操作,但插入的位置不同。
DLIST_INSERT(Sentinel, Element)
这个宏把 Element
插入到链表的头部 ,即在 Sentinel
的后面。具体操作如下:
c
(Element)->Next = (Sentinel)->Next; // Element 的 Next 指向原来头节点
(Element)->Prev = (Sentinel); // Element 的 Prev 指向哨兵节点
(Element)->Next->Prev = (Element); // 原来头节点的 Prev 改为 Element
(Element)->Prev->Next = (Element); // 哨兵节点的 Next 指向 Element
效果 :Element
插在链表的最前面,成为新的第一个元素(Sentinel 后的第一个节点)。
DLIST_INSERT_AT_LAST(Sentinel, Element)
这个宏把 Element
插入到链表的尾部 ,即在 Sentinel
的前面。操作如下:
c
(Element)->Next = (Sentinel); // Element 的 Next 指向哨兵
(Element)->Prev = (Sentinel)->Prev; // Element 的 Prev 指向原来的尾节点
(Element)->Next->Prev = (Element); // 哨兵的 Prev 改为 Element
(Element)->Prev->Next = (Element); // 原尾节点的 Next 改为 Element
效果 :Element
插在链表的最后,成为新的最后一个节点(Sentinel 前的那个)。
总结区别
宏名 | 插入位置 | 插入前指针调整对象 | 结果 |
---|---|---|---|
DLIST_INSERT |
头部 | 插在 Sentinel->Next 前 |
元素出现在链表前端 |
DLIST_INSERT_AT_LAST |
尾部 | 插在 Sentinel->Prev 后 |
元素出现在链表末尾 |
这是因为两个宏插入的位置不同:
DLIST_INSERT
------ 插入在头部
每次新元素插入在哨兵节点 (Sentinel
) 的后面 ,即链表的最前面。所以顺序会被倒置:
插入顺序:1 → 2 → 3
实际链表:3 → 2 → 1
这是因为每次都像这样插在最前面:
初始状态: Sentinel
插入1:Sentinel <-> 1 <-> Sentinel
插入2:Sentinel <-> 2 <-> 1 <-> Sentinel
插入3:Sentinel <-> 3 <-> 2 <-> 1 <-> Sentinel
Sentinel 是同一个因为是DLIST
DLIST_INSERT_AT_LAST
------ 插入在尾部
每次新元素插入在哨兵节点前面,即链表的末尾 ,顺序就被保留了:
插入顺序:1 → 2 → 3
实际链表:1 → 2 → 3
每次都追加到最后:
初始状态: Sentinel
插入1:Sentinel <-> 1 <-> Sentinel
插入2:Sentinel <-> 1 <-> 2 <-> Sentinel
插入3:Sentinel <-> 1 <-> 2 <-> 3 <-> Sentinel
总结
宏名称 | 插入位置 | 顺序是否保留 | 示例插入 1→2→3 的结果 |
---|---|---|---|
DLIST_INSERT |
链表头部 | ❌ 倒序 | 3→2→1 |
DLIST_INSERT_AT_LAST |
链表尾部 | 正序 | 1→2→3 |
这个就好吧打牌摸牌插入一样,插前面和插后面顺序不一样
在 game.h
中定义宏 DLIST_INSERT_AS_LAST
。
我们目前的插入方式是将元素插入到链表的头部,也就是"首位插入"。换句话说,新的元素总是成为第一个节点,紧跟在哨兵节点之后。它的 Next
指向原来的第一个节点,Prev
指向哨兵节点;然后我们再更新原来第一个节点和哨兵节点的指针,使它们指向新的元素。
但现在我们需要的是相反的效果,也就是说,元素应该按添加顺序排列,而不是被倒置。因此我们想要的行为是"尾部插入",也就是每个新元素都加在最后一个已有元素的后面。
实现这个逻辑的方式也很简单:我们将当前元素的 Prev
指向哨兵节点之前的那个元素(也就是当前最后一个元素),Next
指向哨兵节点;然后更新前一个元素的 Next
和哨兵节点的 Prev
,让它们都指向新插入的元素。就像头插一样,最后只需要更新前后节点的指针即可完成链表连接。
本质上,两者结构是一样的,只是插入点不同。我们现在选择使用"尾插"的方式,是因为我们希望这些元素按照我们添加的顺序排列,而不是被倒序展示。这样在后续使用这些元素的时候,比如在某个容器或界面中展示它们,顺序才是准确的,也更直观、更有逻辑。
我们接下来要做的,就是将插入操作替换为尾插函数,比如使用 insert as last
,这样添加的元素顺序就能如预期地保持一致,确保在某些特定结构中展示的数据是有序的、准确的。
修改 game_debug.cpp
中的 AddGroupToGroup
,使其调用 DLIST_INSERT_AS_LAST
。
我们意识到原先的处理方式在创建元素的顺序上可能并不正确,因为这些元素并不是在生成顺序中被插入的,而是被插入到了链表的头部,导致顺序被倒置。而在其他地方,比如 add group to group
的操作中,也使用了相同的插入逻辑,所以保持顺序一致就显得更加重要。
我们明确地知道,在绝大多数情况下,我们并不希望这些元素是逆序排列的。逻辑上,这些元素在程序中的出现顺序才是最自然、最有意义的顺序。也就是说,无论是从数据的表达、用户界面的展示,还是逻辑处理角度出发,我们都更希望这些元素保持创建时的顺序。
因此,将插入方式调整为尾插(保持顺序插入)是更合理的做法。这使得链表中的元素顺序能够准确地反映它们在程序执行过程中出现的先后顺序,不会因为技术实现的细节而导致逻辑上的混乱。
这个修改方向是清晰且正确的,符合我们对功能完整性与可读性的双重需求。接下来,我们计划在此基础上继续扩展一些内容,进一步完善整体结构和功能表现。

在 game_debug.h
中为 debug_state
添加 ViewingFrameOrdinal
。
我们计划在当前系统中加入一些特别的处理逻辑,以提升可用性和交互性。之前的实现中,界面总是默认显示最新的一帧数据,也就是每当新的一帧生成,视图就立即更新到最新状态。但我们现在希望提供更多控制权,使用户能够自由选择查看任意一帧的数据,而不仅仅是实时的最新帧。
为实现这一目标,我们引入一个新的变量,例如 viewing_frame_ordinal
。这个变量默认情况下会自动跟随最新的帧编号,也就是说,它会等于当前帧编号,实现和之前一样的自动更新行为。但如果用户进行某种"拖动"或"滑动"操作,比如通过某种滑动条(frame slider)进行帧选择,那么 viewing_frame_ordinal
就会脱离自动跟随,转而指向用户选定的帧。这种机制允许在暂停状态下查看过去任意一帧的内容。
为此我们准备先实现一个简单的显示方式,展示 viewing_frame_ordinal
的数值,然后在系统中加入暂停功能,并进一步将所有依赖当前帧的模块改为读取 viewing_frame_ordinal
,而不是总是跟随最新帧。
滑动条(frame slider)将作为用户交互的核心组件之一,通过它可以精确选择要查看的帧号。如果用户不需要这个功能,那么这就不是他们的使用场景------但对我们来说,这是对功能的增强与拓展,也是提高系统灵活性和可控性的关键步骤。
总之,我们的目标是让查看历史帧变得直观、方便,让整个调试或回溯过程更加顺畅和高效。下一步就是着手构建这个"frame slider"组件,并将其整合到已有的系统架构中。

在 game.cpp
中添加一个名为 "Debug Control" 的 DEBUG_DATA_BLOCK
用于 FrameSlider
控件。
我们打算在调试系统中添加一个新的调试数据块,用于控制调试交互。这个模块被称为调试控制块(debug control),其核心用途是承载一组新的 UI 控件,尤其是我们刚刚提出的"帧滑动条"(frame slider)。
为了实现这一功能,我们会在这个调试控制块中创建新的 UI 元素,目前首要任务是构建帧滑动条。这个滑动条将允许用户在不同帧之间手动切换,而不仅限于查看最新帧。虽然现在看起来我们在构造 UI 元素时加了一些冗余定义(比如为每个元素指定明确类型),从结构上来看或许有些"过早的工程设计",理论上可以直接创建基础 UI 元素而不指定额外内容,但为了保持一致性和当前的架构风格,我们暂时仍然按现有方式继续实现。
我们会为这个帧滑动条专门命名为 debug_frame_slider
,这个命名是固定的,当前版本不会再提供更多变体。如果未来用户有更多需求,那么可以自行下载源代码进行拓展。
实现完这个控件后,我们将返回到调试绘制逻辑中,将这个新的帧滑动条注册为可绘制的调试项目之一。也就是说,它会出现在调试界面上,供用户使用,实现更直观的帧选择控制。这是向可交互式调试体验迈出的关键一步,后续我们会进一步完善这个控件的行为与数据绑定,使其真正对帧选择逻辑生效。
在 game_debug.cpp
中实现 DebugType_FrameSlider
。
我们正在实现一个帧滑动条(frame slider)作为调试界面中的一个 UI 控件,用于控制当前查看的帧编号。这个过程包含了一些设计权衡和对 UI 系统已有结构的简化利用。
在实现这个帧滑动条时,我们决定不让它支持缩放,保持固定尺寸。虽然理论上可以根据窗口宽度动态调整尺寸,但目前阶段我们选择简单实现,先固定大小,例如宽度设为 1800,高度设为 32。我们通过使用 inline-block
形式的元素来表示它,这样就不需要额外定义图表结构或复杂的类型。这是一个简单的 UI 元素,不需要额外的配置项或属性,目的就是允许用户通过滑动条的方式选择具体的帧。
在具体实现上,我们会使用 begin_element_rectangle
和 element
接口创建一个不可缩放的矩形区域来表示滑动条。这个区域仅用于渲染,并不包含实际交互逻辑。后续会在一个单独的绘制函数(比如 DrawFrameSlider
)中实现滑动条的实际渲染与功能。这种方式参考了之前的性能分析器(profiler)的实现结构,即将控件本身的数据结构定义、绘制逻辑和行为处理分别组织,既便于理解又利于维护。
在主处理逻辑的 switch
分支中,我们会加入这个新的 UI 控件的调用点,并把实际实现放到独立函数中,这样既可以保持主流程清晰,又便于未来扩展。我们不总是这么做,是否抽离逻辑到函数中,取决于具体的代码结构和当下的设计判断。
总的来说,我们正在构建一个简单明了、不依赖外部设置的滑动条控件,用于用户选择帧视图。这是调试体验中的一个核心交互部件,其后续还会进一步与 viewing_frame_ordinal
变量进行绑定,使其真正起作用于帧控制流程中。
在 game_debug.cpp
中实现 DrawFrameSlider
绘制函数。
我们正在实现帧滑动条(frame slider)的绘制函数 DrawFrameSlider
,其目的是在调试 UI 中展示一个横向的帧索引条,便于用户查看和切换特定帧。该控件采用图形化表示,每一帧对应一个条形块,排布成一排矩形条,形成一个简洁的时间线视图。
在具体实现中,我们参考了之前用于性能分析视图的 draw_frame_bars
函数,先复制其核心代码作为起点,然后对其进行大量裁剪,仅保留与绘制矩形条形框有关的部分。鼠标交互和元素层级的相关逻辑暂时不需要,因此被删去。我们不再使用 profile_rect
命名,而是将绘制区域命名为 TotalRect
,表示当前滑动条所覆盖的完整矩形区域,用于绘制所有帧对应的小块。
矩形的 X 区间是固定的,即 ThisMinY
为 TotalRect.min.x
,ThisMaxY
为 TotalRect.max.x
,每个小条形块沿着该区域横向分布,表示每一个逻辑帧的可视表示。条形的尺寸是固定的,不进行动态调整。
颜色方面,目前使用灰色或白色等简洁配色表示帧块,暂不区分当前帧与其他帧。绘制部分仍然使用统一的矩形绘制接口,通过遍历帧索引来逐一绘制每个小矩形。后续可能在交互功能中添加高亮或点击响应等行为,但当前阶段仅实现基础静态绘制。
由于绘图接口使用了通用的 element
参数,而函数内恰好也声明了一个同名变量,造成了参数遮蔽的问题,导致无法正确访问传入的绘图元素。这个问题通过将内部变量改名为 layout_element
解决,避免了命名冲突。
总之,这一阶段的目标是构建一个静态、可视化的帧时间线条形控件,为后续交互功能打下基础,包括帧选择、滑动等扩展操作。同时也优化了绘制逻辑的简洁性,确保其作为 UI 子系统的一部分能顺利集成。



在 DrawFrameSlider
的 for
循环中继续调试。
我们在绘制帧滑动条(frame slider)的过程中遇到一个问题:目前只绘制出了第一个帧块。问题的根源在于绘制逻辑未正确放置于循环内部,导致只执行了一次绘制。为了解决这个问题,我们将绘制每个帧条形的代码移动到循环体内,确保每一次迭代都能绘制出一个对应帧的矩形块。这是一个典型的编程经验问题,将逻辑置于循环中可保证针对每个数据项执行操作。
修正之后,帧滑动条就按照预期开始正常绘制多个帧条。每个帧在滑动条上显示为一个小的矩形区域,横向排列形成一条带状结构,用于展示历史帧时间线。
目前虽然帧条数量较多,导致视觉上显得略为密集,但这只是调试阶段的初始表现。后续可以通过调整显示比例、条形宽度或限制可视帧数等方式进行优化,从而提升可读性与交互性。当前重点在于实现绘制逻辑的正确性,确保每一帧都能被完整呈现。
整体来看,这一步验证了基础绘制机制已经工作正常,并为进一步加入交互(如滑动、帧跳转、缩放)做好了准备。帧滑动条的核心功能已基本实现,为调试系统提供了良好的时间维度控制入口。




在 game_debug.cpp
中将当前查看帧(ViewingFrame)标为黄色,"尿的颜色"。
我们现在的目标是在帧滑动条(frame slider)中,将某些关键帧以不同颜色高亮显示,帮助我们在调试时更直观地识别它们的角色与状态,尤其是当前正在查看的帧。我们将该帧用黄色填充,以增强视觉辨识度。
具体实现步骤如下:
-
标记当前查看帧(Viewing Frame) :
遍历所有帧时,我们检测当前帧索引是否等于
ViewingFrameOrdinal
。如果是,我们将该帧绘制为实心矩形,并使用黄色作为填充颜色。黄色之所以被选中,是因为它是尿液的颜色,象征"基础、必要且不容忽视",有强烈的存在感,能清晰地突出这一帧。 -
其他特殊帧的处理 :
除了查看帧,我们还为以下几种帧分配了不同颜色:
- 当前最新帧(Most Recent Frame) :用绿色标记,表示它是当前帧序列中的最前端。
- 最旧帧(Oldest Frame) :使用深绿色来区分,代表它是数据窗口中最久远的帧。
- 暂时不可查看帧(Collation Frame) :使用红色,暗示此帧当前无法查看或数据尚未准备好。
-
绘制逻辑调整:
- 如果某帧是上述四种类型中的一种,我们就使用一个实心背景矩形覆盖该帧位置,并在上层图层绘制,确保其在图形界面中最清晰、最醒目。
- 所有帧的绘制都基于一个统一的遍历逻辑,判断条件简单明了,只需比较索引值与
ViewingFrameOrdinal
、MostRecentFrameOrdinal
、OldestFrameOrdinal
等变量。
-
状态变量接入
DebugState
:所有与帧位置相关的状态变量(如
ViewingFrameOrdinal
)都从DebugState
中统一读取,确保调试模块逻辑清晰、集中,便于维护和扩展。
这套逻辑使我们能够快速识别查看帧和关键帧,有效提升调试效率和视觉反馈。是否还希望为这些颜色设置可配置参数?
运行游戏,查看 FrameSlider
并解释当每帧的内存空间耗尽时会发生什么。
当前我们已经可以在调试界面中直观地看到帧滑动条(frame slider)的工作效果。从图形显示上看,整体表现符合预期。具体的可视化状态包括:
- 红色帧:表示当前正在进行数据同步(collation)的帧。
- 绿色帧:紧随其后的帧为最新记录的有效帧。
- 另一绿色帧:代表调试数据的起始帧,即最早仍然保留的帧。
目前还没有实现暂停调试的功能,这是后续将要添加的内容。
此外,还观察到了一个重要的调试信息 ------ 每帧内存池(per-frame arena)剩余空间 ,目前该数值为约 130MB。这说明当前的调试系统中,为每帧事件数据分配了足够的存储空间。
我们使用的帧数组长度为 256,这表示系统设计最多可以缓存 256 帧的调试数据。然而这并不绝对:如果每帧生成的调试事件过多,超过了内存池的负载能力,那么系统就会自动丢弃旧帧的数据以腾出空间。
出现内存不足时的表现:
- 红色帧(正在同步的帧)与紧跟其后的绿色帧(最新可查看的帧)之间将出现空隙;
- 这些空隙帧中没有任何有效调试数据;
- 如果此时尝试查看这些帧,将显示"无数据"或"此帧无任何事件",因为它们的记录已经被清除回收。
这种行为可以被用作调试内存使用和事件密度的直观反馈机制。在运行游戏时,系统的内存占用会明显上升,但在目前的测试场景中,尚未达到填满整块内存池的程度,因此还无法看到红绿帧间的实际分离现象。
整体来说,这种机制非常有助于理解调试系统中内存与数据保留之间的动态权衡,并提供了一种基于颜色提示的可视反馈方式,来辅助分析调试数据的生命周期与状态。是否希望添加图形按钮来手动触发内存压力测试或暂停调试?

在 game_debug.cpp
中切换到较小的 SubArena
以存储调试信息。
我们可以通过人为限制调试系统的内存容量,来模拟内存不足的情形,从而观察调试数据丢失的过程以及其在界面上的可视化表现。
具体操作如下:
- 我们调整了调试系统中用于存储数据的子内存区域(sub arena)大小,原先可能是 128KB,这个容量太小,不足以观察显著效果;
- 将其修改为 8MB,作为一个测试使用的固定内存容量;
- 意思是从现在起,整个调试系统只允许使用这 8MB 的空间来保存所有调试帧的数据。
通过这个设置,我们可以:
-
人为制造内存紧张的情况;
-
快速填满调试内存池;
-
观察当系统无法再容纳全部 256 帧数据时的表现,比如:
- 哪些帧会被丢弃;
- 调试界面中红色帧与绿色帧之间是否出现了间隔;
- 这些被"挤掉"的帧是否在查看时提示"无内容"。
该方法有效地创建了一个受控调试资源环境,便于在不依赖游戏实际复杂度的前提下,重现和验证内存管理与数据丢失机制。
是否需要我们进一步通过配置测试场景或添加 UI 提示,以帮助快速观察这些内存淘汰行为?
运行游戏,并在没有足够内存存储调试信息时触发断言。
我们现在限制了调试系统的内存容量,并通过 UI 显示可以看到内存用量的实时变化。刚开始系统正常运行,内存用量逐步下降,直到达到一个固定的帧数(如 206 帧)时开始趋于稳定 ,这是因为调试系统现在限制了最大可回溯的帧数,而不是像以前一样能无限地存储所有历史帧。这种设计是为了让我们能够使用固定数组结构快速跳转访问,而不是依赖链表结构,从而提高查询性能。
我们随后将调试模式切换到了游戏状态,此时出现了一个警告现象:
- 系统检测到当前帧中记录的事件数量与释放帧时清除的事件数量不一致。
- 这是一个问题,因为我们对调试事件的**分配(allocate)与释放(free)**数量进行匹配校验,如果出现偏差,可能说明有些调试事件没有被正确回收或被重复释放了。
- 这个问题触发了我们先前添加的一个断言,用于确保调试系统中的内存使用逻辑是健全的。
在排查这一潜在错误的过程中,我们意识到可能存在这样一种情况:
当我们调用
free_frame()
来释放帧时,并不会清空(zero)该帧中存储的数据。
这就有可能导致帧被意外释放两次而不自知,从而打破了事件分配与释放的平衡。因此,为了防止这种情况发生,我们将清零操作提前,确保在释放帧之后立刻将帧数据置零。
这项修改虽然不一定直接解决了问题,但作为预防措施是必要的,也有助于后续调试工作的准确性。我们在真正开始调试前就做了这个修改,是为了避免后续被误导,也防止遗忘这个细节。
是否需要我们进一步添加更多的内存使用断言,或是做内存快照比对分析来更准确定位问题?

在 game_debug.cpp
中对 Frame
使用 ZeroStruct
清空。
我们在释放帧数据后,决定将其内容清零,确保其内部不再残留任何信息。这是为了避免后续对已经释放的数据误用,确保内存状态的干净与一致。
当我们做完上述清零处理后,再次观察内存事件的分配与释放数量,结果发现先前的问题竟然消失了。这说明之前的问题很可能是同一个帧被释放了两次,也就是说,我们在逻辑上重复释放了某些帧。
这是个非常奇怪的现象,因为按理说帧的释放应该是严格受控的,不能重复释放。说明我们的调试系统里存在某种不易察觉的错误路径,导致了某些帧被标记为已释放后,又被错误地再释放一次。正是由于帧中残留的调试事件信息没有被清空,导致我们在再次释放时还看到事件存在,从而触发了断言失败。
通过这次改动,我们采取了**"释放即清空"**的策略,也就是每当我们确认一个帧已经不再使用并调用释放操作时,立即将其数据清零,这样就能避免重复释放时出现异常,同时也起到了防止数据泄漏和逻辑混淆的作用。
这个修复似乎解决了我们在调试信息同步过程中的事件计数不一致的问题,看起来就是重复释放导致的异常逻辑分支。
是否需要进一步添加防御机制,比如在帧结构中添加标志位以标记是否已释放,从而更早地检测并阻止重复释放行为?


运行游戏,观察在内存耗尽的情况下 FrameSlider
的表现。
我们现在来更详细地观察调试系统中内存使用和帧数据同步的过程。
首先,我们启动程序并查看当前内存情况,发现还有约 7MB 的空闲调试内存。然后进入一个特定状态,在这个状态下每一帧都会同步大量调试数据。此时我们按下空格键后可以清楚地看到调试内存开始迅速消耗,内存占用量不断上升,空闲内存持续下降。
接下来,我们观察帧数据的有效范围。在内存压力加剧的情况下,只有一部分帧还包含有效的调试信息:具体来说,是从某一帧到另一帧之间的数据是有效的,其他帧(特别是"读取位置"之后的部分)已经无法再访问有效数据。这说明:由于调试内存容量有限,旧帧的数据被清除,从而导致某些帧区间"变为空洞"。
我们指出,这是一种合理的行为,因为调试系统为每帧分配固定的内存,如果当前可用内存不足以保存全部 256 帧的调试信息,就只能保留一部分较新的帧。
然后提到另一个重要的问题:之前出现了"帧被重复释放"的现象,也就是对已经释放的帧再次调用了释放函数。我们认为这可能是一个真正的 bug,虽然目前不打算在直播过程中深挖它,但在私下工作时,我们一定会优先排查和修复这类潜在错误。
还强调了一点,在公开演示或直播时,为了保持逻辑清晰、流程连贯,我们倾向于简化调试过程、略去一些深入排查步骤。但在实际开发中,这是不被推荐的------在日常工作中我们应当始终坚持彻底排查问题,避免养成忽视潜在错误的习惯。
总之,我们通过限制调试系统内存,成功模拟了"帧数据被回收"的情况,验证了系统在内存紧张情况下对无效帧的清理机制。同时,我们也识别出一个潜在 bug(重复释放帧),并说明在正式开发环境中会进一步追踪解决。


在 game_debug.cpp
中添加与 FrameSlider
的交互功能。
我们现在希望为调试界面中的帧滑动条(frame slider)添加交互功能:也就是用户点击滑动条某个位置后,能够将所选位置对应的帧设置为当前查看帧(viewing frame)。
首先,在绘制滑动条之后,我们准备添加一种新的交互类型,用于设置查看帧的序号。我们在 debug_interface.h
中进行扩展,加入一个交互枚举值,例如 DebugInteraction_SetViewFrameOrdinal
。然后我们在对应的交互逻辑中添加对该类型的处理。
为了实现这个功能,我们构造一个交互结构体,其中 Uint32
成员表示目标帧的索引值,也就是希望设为当前查看帧的编号。该交互结构体的 interaction.kind
就设置为 SetViewFrameOrdinal
。此外我们设置了一个 ID(尽管目前没有任何地方用到这个 ID,但我们还是附带设置了它,保持结构完整性)。
完成交互结构体设置之后,当用户点击滑动条某一位置并触发此交互时,我们在交互处理逻辑中处理这个 SetViewFrameOrdinal
类型的交互。具体来说,它的作用就是将 debug_state.viewing_frame_ordinal
设置为交互中传入的帧索引值。
为辅助调试,我们还可以在界面中打印当前设置的帧索引(ordinal),甚至可以额外打印"相对于最新帧回溯了多少帧"这样的信息,提升用户理解感。虽然这不是必须功能,但作为调试信息可能会有所帮助,如果有时间也可以加上。
我们还查看了现有的交互处理代码,并确认逻辑非常简单:根据交互类型设置查看帧的编号,这就是它的全部功能,没有其他额外操作。
总结来说:
- 在滑动条点击后,创建一个
SetViewFrameOrdinal
类型的交互; - 设置交互中包含的帧索引;
- 在交互处理阶段,将该帧索引赋值给当前的
viewing_frame_ordinal
; - 可选:打印帧索引,或显示与最新帧之间的距离,辅助调试;
- 此逻辑与其他交互类似,结构清晰简单。


运行游戏,并在与滑块交互时发生崩溃。
当我们尝试在界面中执行拖拽选择操作时,结果却出现了异常情况,显然这不是我们期望的行为。
在具体操作中,当我们点击并拖动滑动条(或其它调试元素)时,系统并未做出正确响应,而是表现出错误或异常的界面状态。这种情况令人困惑,因为并不清楚为何操作会导致这种结果。
随后我们尝试分析问题,并试图找出触发错误的原因。我们意识到,这种异常的表现可能与交互逻辑处理、坐标判定、控件状态管理等方面有关。
简而言之:
- 我们执行拖拽选择操作时遇到错误表现;
- 当前拖拽响应机制存在缺陷;
- 尚未明确导致问题的具体原因,但显然不是预期行为;
- 后续需要进一步排查拖拽相关的输入处理代码。

在 game_debug.cpp
中将交互代码放到正确的位置。
在尝试实现某个功能时,结果显然并没有达到预期,甚至可以说根本没有任何效果。虽然这本应该是显而易见的,但仍然感到有些困惑,不知道为什么这件事会变成这样。
在操作过程中,意外按错了某个键(例如 F 键),导致了一些不确定的反应,甚至不知道具体按下的是哪个键,也没有意识到自己做了什么错误操作。
总体来看,出现的问题并不意外,但依然让人感到疑惑和不解。
运行游戏,滑块交互功能正常。
当前实现的功能虽然可以进行一些操作,但并不理想,尤其是在滑动条(slider)方面的效果不好。为了改进这个交互功能,可能需要进行进一步的调整和优化。然而,暂时决定不做更改,保持现状,至少在短时间内先不做处理。

在 game_debug.cpp
中将 MostRecentFrameOrdinal
替换为 ViewingFrameOrdinal
。
为了优化代码,需要将当前所有使用"most recent frame ordinal" 的地方替换成 "viewing frame ordinal"。在这个过程中,首先做的是在代码中进行查找和替换,把所有涉及到 "most recent frame ordinal" 的部分改为 "viewing frame ordinal"。这样就能确保代码中都使用相同的框架来处理当前视图的帧。
同时,调整了一些显示和交互设置,比如启用"draw a profile","draw frame slider","stored event thread interval graph"等功能,但有些功能如"debug begin interact"并不需要启用,因此选择关闭这些选项。最终的目标是确保代码结构更加清晰,并且所有操作都围绕视图帧的调整来进行处理。
运行游戏,现在可以用滑块选择查看的帧了,但性能分析器的展开行为很奇怪。
在这个阶段,目标是使得可以查看任意帧的详细信息。例如,可以查看每一帧的毫秒数,了解当时的数据状态,以及该帧的性能概况等。然而,遇到一个问题:在查看这些帧时,系统当前显示的是当时事件的状态,这可能并不是想要的行为。
意识到这个问题后,决定修改这一点。具体来说,系统应当绘制的是在查看某个帧时的当前状态,而不是当时的事件状态。为了修正这一问题,需要调整代码中的一些处理方式。
在调试视图中,系统会根据不同的"debug ID"来获取或者创建对应的视图。通过哈希值来生成调试视图的ID。为了排查问题,需要检查该ID的生成过程,并确定它们是否正确地映射到视图上。当前的实现可能存在不必要的排序操作,这也许是引起问题的原因。
为了进一步调查,查看了调试变量的迭代器和生成ID的过程,发现可能存在一些不必要的复杂性,导致调试视图并没有按预期工作。
运行游戏,调查为什么会触发 ToggleExpansion
的问题。
我们希望进一步理解调试视图的行为,特别是关于展开和折叠(toggle expansion)的逻辑。在代码中,"toggle expansion"功能依赖于调试视图的唯一ID,而这个ID是通过当前交互中所引用的ID生成的。理论上,这应能正确处理是否展开某个条目,但目前观察到的行为似乎与预期不符,因此感到困惑。
在尝试处理这一点的同时,也开始思考另一个问题:是否应该允许编辑过去帧的事件。尽管理论上是可以编辑过去的事件,但这些编辑并不会生效,因此实际意义不大。更合理的做法可能是不允许用户编辑历史帧的内容,从而避免造成混淆或错误的状态。
进一步分析后,发现问题的真正原因并非视图折叠与否,而是数据本身尚未写入当前帧的槽位中。也就是说,当系统试图读取该帧的数据时,实际上那个时间点还没有任何调试数据被写入,导致看起来视图没有正确显示。认识到这一点后,发现这一行为是合理的:如果帧中没有数据,那么自然不应该显示任何内容。
这种现象发生的原因与之前人为地减少了调试系统的内存有关。由于内存受限,系统在某些帧之间留下了数据缺口,因此在跳转到这些缺口处时,看起来就像什么都没有发生。实际上,这正是当前实现的预期行为。
总结来说,现在可以理解为什么某些帧没有显示数据:不是逻辑错误,而是因为数据根本还没有被写入。同时确认了展开/折叠状态与数据本身无关,只要数据尚未写入,对应的调试视图也不会有任何内容呈现。这使得整个行为逻辑在目前看来是合理的。
在 game_debug.cpp
中,如果暂停状态下才设置 MostRecentFrameOrdinal
。
现在我们只需要完成最后的部分,就能把整个系统整理好。在实际执行 debug_start
的时候,我们希望系统能判断当前是否处于暂停状态。
我们接下来的逻辑是:如果当前没有暂停,那么我们希望把"最新帧的序号(most recent frame ordinal)"覆盖"当前查看帧的序号(viewing frame ordinal)"。也就是说,在正常运行(非暂停)状态下,我们默认查看的帧应该总是最新的一帧,就像之前调试系统的行为那样。
目前系统还没有完全实现暂停机制,因此这部分条件判断和赋值暂时未启用。不过从逻辑上讲,这将成为未来调试视图行为的一个核心部分:
- 若处于暂停状态,则保留用户查看的帧(viewing frame ordinal);
- 若未暂停,则自动跟随最新帧更新查看帧编号。
这种方式可以在用户进行调试时保持画面静止,而在正常运行时自动刷新调试数据,让用户在两种模式间切换时体验更自然、逻辑更清晰。整体目标是提升调试信息的可控性与可视化效率。你是否也需要我们进一步补充关于"暂停与继续"的具体机制?

运行游戏,欣赏当前状态下的性能分析器界面。
我们希望能够在调试过程中添加一个暂停机制,这样就可以更方便地查看和交互调试信息。
目前,在查看当前帧时,一切运行都正常,界面显示的内容是最新帧的数据。但问题在于:一旦用户点击调试界面,想要查看某一帧的详细信息时,数据会立刻被新的帧数据覆盖掉,导致根本无法停下来查看。这显然是不理想的。
我们考虑两种解决方式:
-
添加暂停功能 :
在某个时刻暂停调试系统,这样可以冻结当前帧的所有调试数据,用户可以在这个状态下自由点击、查看、分析,而不会被下一帧的更新所打断。实现方式是在调试系统中增加一个
paused
状态标志,只有在未暂停时才更新viewing_frame_ordinal
。 -
自动触发暂停 :
如果用户在调试界面点击某一帧或图表,我们可以让系统自动进入暂停状态。这样可以省去显式的暂停操作,提升交互体验。点击即冻结,有效防止被后续帧覆盖。
除此之外,还提到了一个小功能:可以随意拖拽调试窗口的位置,这个功能已经实现,可以将 UI 界面某个部分"撕下来"并独立移动,增加了灵活性。
总之,我们当前的目标是让调试系统支持用户点击调试帧后不被新的数据刷新打断。为此,暂停机制将是关键部分,后续可能还会进一步优化点击交互逻辑。是否希望我们接下来详细设计这个"暂停交互"的具体流程?

在 game_debug.cpp
中实现暂停性能分析器的功能。
我们之前确实设置了一个变量 paused
用于表示调试是否暂停,但目前这个变量实际上并没有被真正使用。换句话说,它只是在某处被赋值为 false
,之后再没有任何地方引用它,因此完全不起作用。这种情况说明:仅仅定义变量而不使用是没有意义的。
现在的目标是:在调试系统暂停时,不再覆盖当前正在查看的帧数据(viewing_frame_ordinal),从而保持数据稳定以供用户查看。为实现这一点,整体计划如下:
当前逻辑流程(未暂停):
- 每当新的一帧调试数据到来时,都会递增帧序号(frame ordinal)。
- 然后将当前帧数据记录并更新 viewing_frame_ordinal。
- 旧数据被释放或覆盖。
修改后的流程(加入暂停判断):
- 在处理调试数据的主函数
collate_debug_event_records
中,我们会检测每一帧的标记(frame marker)。 - 现在加入判断:如果
paused == true
,就不再更新 frame ordinal,也不记录该帧数据。 - 相反,我们会立刻释放当前帧所占资源(调用
free_frame()
),然后继续处理下一个调试事件。
代码层面类似这样:
c
if (debug_state->Paused) {
// 不记录当前帧,直接释放它
FreeFrame(&debug_state->Frames[debug_state->CollationFrameOrdinal]);
} else {
// 正常处理:递增帧序号,记录调试信息
++debug_state->CollationFrameOrdinal;
}
配套交互逻辑补充:
-
当我们设置 viewing_frame_ordinal(例如点击查看某一帧)时,应该同时设置
paused = true
,表示用户希望查看这一帧,暂停后续更新。 -
之后还需要实现一个"取消暂停"的操作,让系统重新开始记录后续帧。可能的方式有:
- 点击"播放"按钮;
- 再次点击帧时间线某个区域;
- 自动在特定事件后恢复。
整体效果与目标:
- 用户点击帧查看详细数据时,不再担心新数据刷掉当前信息。
- 调试信息不会被覆盖,调试分析更清晰、稳定。
- 实现了一个简洁实用的"手动暂停与恢复"机制。
是否希望我现在继续整理这一机制的完整代码结构或流程图?
运行游戏,尝试暂停性能分析器。
我们目前已经实现了帧滑块(frame slider)的基本交互功能:当用户点击某个帧位置时,系统会自动进入暂停状态,从而避免当前查看帧的调试数据被新数据覆盖。这一点已经成功验证,表现非常理想,效果也令人满意。
当前系统状态与功能进展:
- 帧滑块功能已实现:用户可以点击任意帧并查看对应的调试信息。
- 点击后自动暂停 :系统进入暂停状态后,不再刷新
viewing_frame_ordinal
,数据保持静止,方便分析。 - 调试交互体验良好:整体已经趋于完善,已经能够提供较为清晰的调试反馈体验。
后续待办事项(工作计划):
-
实现"解除暂停"的机制
当前还没有提供"恢复播放"或"取消暂停"的方法。下一个目标是添加一个明确的"取消暂停"按钮或交互方式,允许用户恢复数据更新。
-
增加调试控制按钮
除了暂停与恢复,还应添加一组基础控制按钮,比如:
- 手动前进一帧;
- 手动后退一帧;
- 跳转到最新帧;
- 自动播放/停止等。
-
修复 OpenGL 多线程相关问题
当前在多线程环境下 OpenGL 仍存在一些问题,特别是与纹理下载(texture download)相关的同步与一致性问题。需要修复这些问题,以确保在暂停与回看时纹理正确显示。
-
纹理下载优化问题
当前仍存在部分纹理下载结果错误的问题,后续要重新梳理纹理加载流程,可能不需要再反复研究白皮书,但要找出导致下载异常的根本原因。
总结:
现在的调试系统已进入可视化交互调试的高级阶段,能够自由选择帧查看内容并维持数据静态,非常适合做复杂的时序和性能分析。接下来重点是继续补齐暂停控制逻辑,修复渲染层面的问题,并进一步提升交互体验。
需要我帮你整理出按钮交互逻辑和 UI 结构图吗?
演示我们确实追踪了所有的数据。
我们现在已经能够完整地追踪所有调试数据。比如,当选择了一个实体后,打开模拟视图并查看该实体的数据时,能够清楚看到整个流程中的调试数据都被正确保留下来。虽然调试过程中用不到性能分析(Profile)信息,但这些数据依然被完整保存并且可以随时查看。
我们可以在时间轴上来回跳转到任意历史帧,不仅性能数据依然准确,对应的实体数据也仍然是当时的状态。一切调试信息都是实时捕获并且可回溯的,任何帧的数据都可以被深入分析。
此外,在调试视图中,我们可以对任意信息进行放大、缩小查看,针对具体条目分析其细节,并且不仅能看到一个时刻的数据,还能观察这个数据在多个帧中的变化。这样就可以轻松判断某项值是否随时间发生变化,从而推断逻辑上的行为和趋势。
整体来看,这个调试器的功能已经相当强大,集成了性能分析和状态追踪能力,而且二者都可追溯历史状态,非常适合复杂系统的深度调试和行为分析。从交互体验到调试能力,都已经达到了极高水平。我们对这个系统非常满意,认为它几乎可以称得上是目前最强的调试分析工具之一。
开始答疑环节。
如果想要在暂停时展示图形状态,是否可以保存绘图缓冲区快照?或者至少保存可以重新居中某一帧的数据?成本如何?
如果我们想在调试时保存绘制缓冲区(Draw Buffer)的快照,以便在暂停状态下显示当时的视觉状态,或者保存足够的数据以重新渲染那一帧,从成本角度来说,这其实并不算复杂。
对于软件渲染器来说,这件事基本是免费的。我们只需要分配比如 256 个后备缓冲区,每次渲染的时候轮换使用一个新的缓冲区即可。这种情况下完全不涉及额外成本,可能有一点点缓存失效的开销,但也很小,因此可以说是"免费"的。
在硬件渲染器(比如基于 OpenGL 的渲染器)上,操作可能稍微麻烦一些,但原理类似。我们仍然可以分配一批后备缓冲区(比如 256 个),只不过这次是在显卡内存中。然后每次渲染前先渲染到一个缓冲区,再将其翻转到屏幕上,之后也可以随时回到任意缓冲区来显示历史帧。实现起来不难,主要代价在于显卡显存的使用,需要有一块内存充裕的显卡。
虽然技术上可行,但我们对是否要花太多时间处理 OpenGL 相关工作有所保留。因为这些 GPU 编程相关的知识,在几年之后可能就完全过时,不具备长期的学习价值。相比之下,构建这个调试器和性能分析器时所涉及的每一步都具有普适性和持久的编程价值,从设计状态管理、数据结构构建,到交互逻辑、信息可视化,这些内容对任何阶段的编程训练都非常重要。而 OpenGL、Vulkan、D3D 或 Metal 等图形 API,往往是短期内频繁更替的临时技术,它们的使用方式很可能很快就被新的系统取代。
因此,虽然可以实现历史帧的图像快照和重现,但我们更倾向于专注在具有长期价值的逻辑和架构层面的改进上,而不是花费大量时间在容易被时代淘汰的图形 API 实现细节上。
在开发大型项目时,是否可能将整个代码库记在脑中?这对代码设计有什么影响?特别是像 game Hero 或 AAA 游戏项目中。
在开发大型项目时,不可能将整个代码库完整地记在脑海中,尤其是像三A级别的游戏项目这样的大型系统,这是绝对无法做到的。这种现实限制会对代码设计决策产生深远影响,而解决这一问题的关键策略,就是在开发过程中将系统划分为可理解、可复用的模块。
以当前正在开发的项目中的调试系统为例,这个调试系统目前是比较复杂、非标准的,它的形成过程也带有较强的实验性质,结构较为紧凑、耦合度高。如果这是一个真正面向长期维护和多人协作的大型项目,而不是一个即时开发的过程,那么我们就需要在完成阶段花费一到两周的时间来整理这一系统。
这个整理过程包括以下几个方面:
- 理清系统结构:将系统按逻辑功能进行划分,每个模块具备清晰边界,功能职责明确。
- 模块化处理:将一些复用性强或独立性高的逻辑提取出来,构建成独立的子系统或工具模块。
- 文档和说明:补充注释和设计文档,使得未来阅读这段代码的开发者(可能是别人也可能是自己)能迅速理解其运行机制。
- 简化接口:建立清晰的接口和调用路径,减少不同模块间的耦合,使得系统更易维护和扩展。
对于所有大型项目,无论是中型游戏还是三A级别的商业化游戏,合理设计系统结构、注重可读性和可维护性,都是极其重要的。核心理念是在系统开发推进的过程中,必须不断地把开发过程中临时构建的"实验代码"转化为结构清晰、行为可预测的稳定系统。
只有在这种方式下,才能确保即使项目规模不断扩大,代码依然能够被理解、被维护,并在后续的开发中继续支持功能扩展与稳定运行。你想继续探讨这方面在具体游戏架构上的实践方式吗?
提问:引擎部分会是项目中最大的一部分吗?
目前整个项目中,体量最大、最核心的部分确实是引擎开发,而不是具体的游戏代码。调试系统完成之后,大多数工作将会转向更偏游戏逻辑的部分,但这些内容本质上仍然是引擎层面的开发,比如实现最终版本的碰撞检测器、完善角色行为系统(如便利性系统等),这些都属于通用的底层机制,并不直接硬编码到某个具体的游戏内容中。
整个项目的核心目标是构建一个扎实的游戏引擎,而不是专注于游戏设计。完全不涉及具体的游戏设计思路,也不会围绕玩法、美术、关卡等游戏层面的内容展开。因此,这不是一个游戏设计项目,而是一个纯粹的技术项目------一个用于验证引擎设计和实现的过程。
不过,要想真正构建一个有意义的引擎,仅仅写好引擎模块是远远不够的,必须要有一个实际的游戏与之配合。只有通过开发一款实际的游戏内容,才能检验引擎的通用性、完整性和可用性。必须在实践中验证这个引擎是否能够支撑一个真实的、运行良好的游戏。比如,能否正确渲染、是否具有良好的响应性、各个系统协作是否顺畅等等。
因此,虽然整个项目表面上也会出现"游戏相关"的开发内容,但实际上这些都服务于引擎本身的验证和打磨。例如制作角色的动画系统,看起来是游戏内容,实际却是为了构建一个通用、可复用的动画系统。这类工作虽然靠近游戏层面,但依然保持在技术与架构的范畴内。
整体来看,这个项目预计会持续非常长的时间,至少需要约 600 集的开发过程。其中几乎所有内容都以引擎开发为核心,只是有些模块比其他的更接近游戏逻辑而已。例如制作动画控制系统看起来贴近游戏,但关注点仍然是"如何通用地构建动画系统",而不是"如何让某个角色做某个具体动作"。
这样的方式确保了项目最终产出的是一个实用、结构清晰、模块合理、性能优秀的底层游戏开发引擎,而不是一个仅服务于某个特定游戏的"一次性框架"。是否需要我帮你梳理一下项目模块化架构的大致框图?
能再解释一下贴图加载的问题吗?为什么有时候加载失败?
目前纹理加载偶尔失效的问题尚未找到明确的原因。虽然尝试采用了某厂商在技术白皮书中推荐的做法,试图通过重叠操作(overlap)在 GPU 端进行更高效的纹理加载,但在实际测试中发现该机制在消费级显卡上并没有真正实现支持。同时,另一家显卡厂商在类似机制下也存在兼容性问题,因此基本可以判断这种方式本身就是不可靠的,或者说不适用于通用的消费级硬件环境。
目前的问题具体表现为:尝试使用两个 OpenGL 上下文交错执行纹理加载与绘制操作时,系统不稳定,有时纹理根本加载不上。由于无法获取显卡驱动的源代码,因此对这些内部机制完全无法深入调试。除非显卡厂商内部人员愿意公开问题细节或者主动调试,否则从应用层面很难弄清楚真正的原因。
为了解决这一问题,计划后续完全弃用当前的并行加载方案,也就是不再试图通过两个 OpenGL 上下文来同时处理纹理加载和渲染,而是重新实现一个更可靠、稳定的方案,哪怕性能不如理论方案理想。
总结:
- 采用官方白皮书建议的纹理加载优化策略在实际硬件上未被支持。
- 多上下文重叠加载纹理机制在实际测试中表现不稳定。
- 无法获得驱动源代码,因此问题不可深入分析。
- 预计将改用不依赖双上下文的方案,牺牲部分性能以换取稳定性。
- 这是个将来要修复的问题,目前不急于处理。
需要我进一步整理目前纹理加载模块的结构问题和潜在改进方向吗?