仓库:https://gitee.com/mrxiao_com/2d_game_5
回顾之前的内容,并遇到了一次一阶异常(First-Chance Exception)。
欢迎来到新一期的开发过程,我们目前正在编写调试接口代码。
当前,我们已经在布局系统上进行了一些工作,今天计划进一步优化它。此外,我们还希望研究如何与性能分析(profiling)代码的接口进行交互。目前,我们拥有大量的分析数据,但由于缺乏合适的界面,难以快速浏览和提取有用的信息。因此,今天的目标是开始构建一个交互式的调试界面,使我们可以更高效地浏览和利用这些数据。
当前的调试系统已经具备了大部分核心功能,但仍然需要进一步优化,以确保其能够满足我们的需求。因此,我们会以分析数据的可视化作为切入点,推动整个调试界面的改进。
当前进度
调试环境设置
- 打开命令行,查看当前目录,并启动调试工具。
- 编译并运行代码,确保环境正常。
回顾上次的进展
- 之前的改进主要是让交互系统更加统一,使不同的界面元素可以共享相同的交互逻辑。
- 另外,布局系统也得到了增强,现在能够自动排列界面元素,使界面组织更加有序。
接下来的目标
优化性能分析数据的可视化展示
- 目前,性能分析数据仅以基本的文本或数值方式呈现,难以从中快速获取有效信息。
- 目标是开发一个更友好的界面,使数据的展示更具可读性,并允许快速导航。
探索更高级的界面交互方式
- 需要思考如何让用户能够更灵活地操作调试界面,例如添加筛选、排序、层级展开等功能。
- 目前尚未完全确定最佳方案,需要进行一定的实验和探索。
总的来说,今天的主要任务是继续优化调试界面,并开始为性能分析数据提供更友好的展示方式。
调查这个异常的原因。
在当前的调试过程中,我们遇到了first-chance exception(第一次改变的异常),这表明某个记录无效。经过分析,发现该异常与性能分析(profiling)代码有关。
目前,我们的调试模式并未提供足够的保护,因为字符串数据没有被安全存储,而是作为临时数据处理。这意味着当动态链接库(DLL)重新加载时,可能会发生调试记录被移除的情况,导致异常。
分析问题发生的可能性
数据更新时机存在潜在问题
- 调试记录的更新时间在
DebugFrameEnd
之后,但渲染发生在GameUpdateAndRender
里。 - 如果在
DebugFrameEnd
和GameUpdateAndRender
之间执行了DLL重载,那么某些记录可能会被移除,导致异常。
- 调试记录的更新时间在
代码结构导致潜在的不一致
- 当前的调试框架分散在多个地方:
DebugFrameEnd
负责调试数据的收集和整理。GameUpdateAndRender
负责渲染调试信息。
- 这种分离可能导致数据同步问题,从而引发崩溃或错误。
- 当前的调试框架分散在多个地方:
改进方案
考虑到当前调试代码的组织方式,我们提出了一种优化方案:
- 将调试渲染(debug overlay)与
DebugFrameEnd
进行整合,而不是在GameUpdateAndRender
里执行。 - 这样可以保证渲染时数据是最新的,同时避免因DLL重载导致的数据丢失问题。
- 另外,由于我们的帧处理逻辑与显示完全解耦,因此可以更自由地调整执行顺序,而不影响整体运行。
下一步计划
- 调整代码逻辑,将调试渲染逻辑移至
DebugFrameEnd
。 - 优化代码结构,减少调试代码的分散程度,使其更加清晰易维护。
- 观察改动后的效果,验证是否有效减少异常发生的可能性,并确保调试系统仍然稳定运行。
最终目标是简化代码结构,同时提升调试系统的可靠性,避免因数据更新不一致导致的问题。这一改动不仅让代码更加整洁,还能提升调试体验,使调试信息更加直观和实时。
在 win32_game.cpp
文件中,将 DebugCollation
移到 FramerateWait
之前,并将 NewInput
和 &Buffer
传递给 DEBUGFrameEnd
。
我们计划对调试系统的代码结构进行调整,以优化其组织方式并减少对游戏主逻辑的依赖。具体来说,我们希望将调试代码的位置向前移动,使其在帧等待(frame wait)之前执行,从而完全独立于游戏逻辑。
调整方案
将调试代码从
GameUpdateAndRender
中抽离- 目前,
GameUpdateAndRender
负责更新游戏状态并进行渲染,同时还调用了一些调试相关的代码。 - 我们不希望调试系统与游戏逻辑相互耦合,因此计划将其完全独立。
- 目前,
在
DebugFrameEnd
之前执行调试代码- 在帧等待(frame wait)之前,我们已经有
DebugCollation
,负责整理调试信息。 - 现在,我们计划将
DebugFrameEnd
直接放在这里,并让它接收GameUpdateAndRender
需要的绘制缓冲区(buffer)。 - 这样,调试系统将独立处理游戏数据,而不会干扰游戏的更新和渲染逻辑。
- 在帧等待(frame wait)之前,我们已经有
清理
GameUpdateAndRender
内部的调试调用- 之前,游戏逻辑内部直接调用了一些调试函数,比如
DebugFrameEnd
。 - 但这样做会导致游戏逻辑与调试系统耦合,使代码变得不清晰、不易维护。
- 通过调整结构,我们让游戏逻辑仅负责写入缓冲区,调试系统再从缓冲区中读取数据并执行调试任务。
- 之前,游戏逻辑内部直接调用了一些调试函数,比如
调整后的优势
- 调试系统完全独立,不再直接调用游戏逻辑,游戏逻辑也不依赖调试代码。
- 代码更加整洁,减少
GameUpdateAndRender
内部的调试调用,使主游戏逻辑更专注于游戏更新和渲染。 - 更好的扩展性,未来如果需要调整调试系统,不需要修改游戏主逻辑,降低了维护成本。
下一步计划
- 验证修改后调试系统是否正常运行,确保
DebugFrameEnd
仍能正确处理数据。 - 检查是否有其他依赖于
GameUpdateAndRender
的调试代码,并进行必要的调整,以确保所有调试功能都在新架构下正常工作。 - 观察运行效果,确认调试信息的显示仍然准确,并进行必要的优化。
此次改动的核心目标是提高代码的模块化和可维护性,同时确保调试系统与游戏逻辑解耦,使整体架构更加合理。
在 game.cpp
中移除 DEBUGStart
和 DEBUGEnd
。
我们目前的调整遇到了一个问题,不过我们稍后会处理它。现在,我们先进行一些清理工作,以简化调试代码结构。
当前调整
去除
DebugStart
和DebugEnd
- 这些函数在当前方案中暂时不再需要,因此我们先将它们移除,以减少不必要的代码。
- 之后,我们会重新评估它们的作用,看看是否需要以新的方式引入它们。
保留
DebugCollation
和DebugFrameEnd
作为主要调试流程DebugCollation
仍然用于整理调试信息。DebugFrameEnd
现在独立运行,不再嵌套在GameUpdateAndRender
内部,而是在帧等待(frame wait)之前执行。
下一步计划
- 解决当前遇到的问题,确保新的架构能够正常运作。
- 观察代码运行情况,确认
DebugFrameEnd
仍然可以正确处理调试数据,并且调试信息能够正确显示。 - 进一步优化调试系统,看看是否需要新的方法来管理
DebugStart
和DebugEnd
,或者以更合理的方式替代它们。
我们的目标是让调试代码更加清晰、独立,从而提高代码的可维护性和可扩展性。
在 game_debug.cpp
中,在 DEBUGGameFrameEnd
内部调用 DEBUGStart
和 DEBUGEnd
。
我们目前正在对 game_debug
进行调整,使其代码结构更加合理,减少不必要的调用和依赖,并将 DebugStart
和 DebugEnd
整合到 DebugFrameEnd
中,以确保调试流程更加紧凑和清晰。
当前调整
移动
DebugStart
和DebugEnd
到DebugFrameEnd
内部- 之前
DebugStart
和DebugEnd
是独立调用的,我们现在将它们直接集成到DebugFrameEnd
之中,使其成为调试系统的一部分,而不是外部调用。 - 这样可以减少额外的函数调用,使代码更加紧凑。
- 之前
修改
DebugFrameEnd
以接收更多参数- 现在
DebugFrameEnd
需要接受DebugState
作为参数,这样可以直接在内部访问调试状态,而不需要全局访问。 - 另外,我们还需要确保
DebugFrameEnd
具备绘制所需的DrawBuffer
和NewInput
,以便完成最终的调试渲染。
- 现在
调整
DebugFrameEnd
调用顺序- 目前
DebugFrameEnd
直接在主循环内调用,之前的DebugCollation
可能需要提前执行,以确保数据整理过程在渲染之前完成。 - 这样可以避免在渲染阶段处理未整理的数据,提高调试信息的完整性。
- 目前
优化
game_debug.h
- 移除不必要的导出函数,如
RefreshCollation
,因为它并没有被其他模块调用。 - 清理
game_debug.h
头文件,使其更加简洁,确保只有必要的内容被暴露给其他模块。
- 移除不必要的导出函数,如
下一步计划
- 测试新的
DebugFrameEnd
结构,确保它能够正确处理DebugState
并渲染调试信息。 - 优化
DebugCollation
的调用位置,确保数据整理发生在正确的时间点。 - 进一步简化
game_debug.h
,减少不必要的外部接口,使game_debug.cpp
内部逻辑更加自洽。
我们的目标是让调试代码更加模块化、清晰,并减少不必要的耦合,这样可以提高可维护性,并避免潜在的错误。
在 game.cpp
中引入 DEBUGGetGameAssets
,让调试系统能够访问 TranState->Assets
。
在当前的开发过程中,接下来的任务是使调试系统能够直接访问游戏中的资产。为了实现这一目标,我们打算让调试系统可以直接获取游戏的资产,而不是让游戏主动调用调试系统。这样做的好处是,调试系统会独立于游戏,只有在调试系统启用时,它才会与游戏进行交互;如果调试系统被编译移除,游戏将完全不受影响,这使得调试和游戏代码的分离更加清晰,并且有助于在最终发布时去除不需要的调试代码。
具体步骤:
为调试系统提供获取游戏资产的接口:
- 通过在游戏代码中提供一个简单的
get_game_assets
函数,调试系统可以直接访问游戏的资源。这一函数会接收游戏的内存结构作为参数,然后直接从游戏内存中提取相关的资产数据。
- 通过在游戏代码中提供一个简单的
简化调试系统与游戏的关系:
- 这种方式的关键是让调试系统能够主动从游戏中获取数据,而不是游戏向调试系统发起调用。这使得调试系统与游戏代码之间的耦合度降低,游戏本身也不需要了解调试系统的细节。
代码实现:
- 在游戏代码中,增加一个简单的获取游戏资产的接口。比如一个
get_game_assets
函数,它从游戏内存中读取并返回游戏的资产数据。只要资产被正确初始化,调试系统就能通过这个接口访问到它们。
- 在游戏代码中,增加一个简单的获取游戏资产的接口。比如一个
简化并提高代码可维护性:
- 通过这种方式,调试系统与游戏逻辑之间的界限变得更加明确,调试系统的开关也变得更加简单。如果需要在发布版本中移除调试功能,完全不需要修改游戏的逻辑,只需从编译中排除调试代码即可。
总结:
这种做法的最大优势在于,它使得调试系统不再依赖于游戏主动调用它,而是通过游戏提供的接口来主动访问必要的数据。这样不仅清晰地分离了调试系统与游戏逻辑,而且还为以后的调试代码移除提供了更好的支持。
在 game_platform.h
中,使 DEBUG_GAME_FRAME_END
的调用结构与 GAME_UPDATE_AND_RENDER
相同。
在当前的工作中,主要目标是确保调试系统可以顺利获取游戏中的资产,并且确保输入和绘制缓冲区的数据能顺利传递。为了实现这一目标,关键是要确保在游戏更新和渲染过程中,所有相关的数据(例如游戏内存、屏幕宽高、绘制缓冲区和输入信息)都能正确地传递给调试系统。
主要步骤:
确保游戏更新与渲染的结构一致:
- 需要调整游戏的更新和渲染流程,确保调试系统可以像渲染一样,正确获取游戏内存和所有必要的数据(如输入和绘制缓冲区)。这意味着在更新和渲染的过程中,所有这些数据都需要通过相同的调用结构传递。
调整调用结构:
- 调整代码结构,确保在进行游戏更新和渲染时,输入和绘制缓冲区能够被正确传递给调试系统。这样就能确保调试系统可以独立地访问这些数据,而不需要过多的耦合。
考虑代码命名:
- 为了更清晰地表达这层关系,可以将调试系统的相关代码命名为
debug_update_and_render
或类似的名称,以便更好地体现出它与正常渲染流程的一致性。
- 为了更清晰地表达这层关系,可以将调试系统的相关代码命名为
简化数据传递:
- 通过确保传递游戏内存、宽高、输入和绘制缓冲区这些关键信息到调试系统,调试系统就能有效地执行所需的操作,而不需要过多依赖于其他复杂的逻辑或流程。
总结:
核心思路是通过确保调试系统与游戏的更新和渲染过程有一致的调用结构,使得调试系统能够独立地接收到所需的所有数据,简化调试代码与游戏逻辑之间的耦合。这样做不仅能够提高代码的清晰度,还能方便未来的扩展和调试功能的移除。
在 game_debug.cpp
中为调试系统创建 DrawBuffer
。
在当前的工作中,主要目的是确保在调试过程中,能够正确地使用缓冲区来绘制数据,并且确保调试系统和游戏渲染系统能够有效地协同工作。具体步骤如下:
主要步骤:
创建全屏缓冲区:
- 需要在游戏的渲染系统中,创建一个全屏的缓冲区来进行绘制操作。通过使用现有的缓冲区(如离屏缓冲区),可以获得宽度和高度等信息,这样就可以为调试系统创建一个相应的缓冲区。
设置缓冲区参数:
- 在渲染之前,需要确保将宽度、高度和内存地址等参数正确设置到缓冲区中。这样,调试系统可以正确地将数据绘制到缓冲区,并在屏幕上显示出来。
简化调试功能:
- 将调试系统的绘制过程分离出来,确保它独立于游戏的其他渲染逻辑。这使得调试系统更加简洁,并且能在游戏中进行高效的调试操作,而不需要依赖于复杂的游戏逻辑。
调整函数和参数:
- 确保函数调用正确,特别是确保调试结束的函数
debug_end
接受正确的参数。通过适当调整函数接口,使得代码更加清晰和易于维护。
- 确保函数调用正确,特别是确保调试结束的函数
确保缓冲区的可访问性:
- 需要确保缓冲区在合适的位置创建,并能够供后续的调试操作使用。确保调试系统能够在需要的时候访问和写入该缓冲区,以便正确地显示调试数据。
总结:
核心目标是通过创建全屏缓冲区,并正确设置其参数,确保调试系统能够独立地进行绘制操作。这样一来,调试功能就能与游戏的渲染系统有效分离,避免不必要的耦合,提高代码的可维护性和清晰度。
使用调试器进入 DEBUGGetState
。
当前遇到的问题是 DEBUGGetState
函数出现了问题,原因是它在调用时并没有正确初始化。这个问题发生的原因是因为 DEBUGStart
函数在代码中的调用顺序,使得在调用 DEBUGGetState
时,调试状态还没有被初始化。
解决方案:
- 这个问题实际上是因为在整合和简化代码时,调整了调试系统的结构,导致某些代码的执行顺序发生了变化。
- 需要对代码做轻微的重组,确保在调用
DEBUGGetState
之前,DEBUGStart
已经被正确地初始化并执行。也就是说,确保调试状态在整个调试流程中被正确设置。
总结:
在整理代码时,调整了调试系统的结构,导致了初始化顺序的问题。通过轻微重组代码,确保调试状态的正确初始化,将能够解决这个问题。
在 game_debug.cpp
中,将 DEBUGGetState
复制到 DEBUGGameFrameEnd
内部。
现在,遇到的问题是 DEBUGGetState
可能没有被正确初始化。为了处理这个问题,决定将 DEBUGStart
的相关代码直接放到这个位置。实际上,当前已经不再需要 debug_state
作为一个独立的功能,因此这部分代码将被简化和合并。
解决方案:
- 直接将
DEBUGStart
的内容放到合适的地方,这样可以避免debug_state
的初始化问题。这样做的目的是简化代码结构,因为debug_state
已经不再需要独立存在。 - 在执行
DEBUGStart
之前,不需要再断言它已经初始化,因为知道它在当前的代码结构下会被直接初始化。
总结:
通过将 DEBUGStart
的初始化代码直接放置在调用的地方,避免了 debug_state
不需要的复杂性和初始化问题。简化了代码流程,使其更加清晰和直接。
运行游戏,发现整体状态有所改善。
现在,整体代码结构有了显著的改善,之前的问题已经解决。具体来说,原本的 bug 是在于可执行文件在每次调用和显示之间可能会重新加载,这种情况虽然不会影响游戏本身,但会影响调试代码的稳定性。通过调整,确保了可执行文件在调试过程中不会在两次调用之间被重新加载,从而避免了这个 bug。虽然这是一个调试中的小问题,但修复它非常重要,因为如果调试代码中存在问题,就会减少开发者使用调试工具的频率,而调试工具的目的就是帮助发现并解决难以察觉的 bug。
关键改动:
- 确保调试代码不受影响:通过改变代码结构,避免了调试系统直接与游戏代码交互,从而保持了调试代码的独立性和清晰性。现在,游戏代码只负责写入调试缓冲区,而不会直接调用调试系统。
- 简化调试流程:减少了调试过程中潜在的复杂性,使得开发者可以更轻松地使用调试系统,而不会因为调试系统本身的问题而影响使用频率。
虽然这个更改解决了当前的一个问题,但结构上仍然有很多可以改进的地方,例如尚未采用滚动缓冲区来处理帧数据的相关问题。这个可以在未来进一步优化。
总体而言,这次调整是一次成功的改进,确保了调试过程的顺畅和可靠,进一步提高了调试代码的可用性。
关闭 GAME_INTERNAL
选项。
现在,我将 game_internal
设置为零,并希望查看如果将其关闭会发生什么情况。我也不太确定 game_profile
的作用,但目前似乎不太重要。总的来说,我的计划是将 game_internal
关闭,看看效果。
在 game_platform.h
和 win32_game.cpp
中,添加 GAME_INTERNAL
相关的条件编译判断。
现在,计划是去掉那些不再使用或以后不会再使用的部分。首先是 game_internal
,因为实际上我们并没有真正使用它。接下来,查看了一些内部的文件和内存管理功能,比如 debug_free_file_memory
和 get_process_state
,这些都是内部功能,也应该去掉。然后是全局调试表 global_debug_table
,这个表格存在于平台的头文件中。对于这个表格,处理方式就是确保这些内容在关闭 game_internal
时也能被移除。
所以,实际上,只要确保在关闭 game_internal
时,相关的调试和内存管理功能都能按需去除就行。如果 game_internal
被关闭,那么就不需要这些调试和内存管理功能了。
在 game_platform.h
中,移除 GAME_PROFILE
,改为使用 GAME_INTERNAL
。
现在,计划是去掉那些不再使用或以后不会再使用的部分。首先是 hand_internal
,因为实际上我们并没有真正使用它。接下来,查看了一些内部的文件和内存管理功能,比如 debug_free_file_memory
和 get_process_state
,这些都是内部功能,也应该去掉。然后是全局调试表 global_debug_table
,这个表格存在于平台的头文件中。对于这个表格,处理方式就是确保这些内容在关闭 game_internal
时也能被移除。
所以,实际上,只要确保在关闭 game_internal
时,相关的调试和内存管理功能都能按需去除就行。如果 game_internal
被关闭,那么就不需要这些调试和内存管理功能了。
现在,考虑到代码的简化,决定去掉 game_profile
,只保留 game_internal
,因为 game_profile
看起来可能有点多余,变成了太多开关的组合,增加了复杂性。因此,决定只使用 game_internal
,并保持 made
和 made_32
作为平台配置的唯一开关。
这样,game_internal
控制是否启用调试输出,而 made
控制是否启用断言。这个结构现在看起来更简洁了。
接下来,修改了一些逻辑。对于全局调试表 (global_debug_table
),只有在 game_internal
开启的情况下才会使用。在卸载调试表的代码中,也只有在 game_internal
启用时才会执行。这样,所有和调试相关的功能都可以根据是否启用 game_internal
来动态编译和移除。
最后,虽然有一些地方代码略显杂乱,但总体来说,现在能够更加干净地编译代码,避免不必要的调试功能和开关,同时保留必要的调试功能,整体结构更清晰。
运行游戏,成功执行。
现在,检查了一下代码,确认它能够正常运行,一切都很好。接下来,计划确保能够重新启用所有功能,确保在需要的时候调试代码能够再次启用。
切换 game_INTERNAL
选项后,游戏仍然能正常运行。
现在检查了一下代码,确保调试功能能够正常关闭并且游戏能按常规运行,结果一切正常。对于之前的设计,感觉将“game profile”和“game internal”放在一起可能有些过于复杂,虽然现在它能正常工作,但未来可以进一步简化这些配置,减少代码中的复杂度。尽管如此,当前的解决方案还是可以接受的。
接下来计划继续进行改进,虽然已经接近完成,但仍然有一些优化的空间,可以将一些调试的部分进一步简化。接下来可以继续关注如何处理一些剩余的调整和未来可能的优化,确保在保持功能完整的同时,代码结构尽量简洁。
思考接下来的开发方向。
目前的系统已经实现了一些基本的功能,包括调试菜单和调试界面的设置,接下来打算进一步改进调试功能,加入更多的交互界面。例如,可以通过右键启动“离合模式”,在这个模式下可以访问一些功能,停止数据记录并进行进一步的分析。
具体来说,考虑在调试界面中加入一些交互按钮,比如“暂停”按钮,用于暂停正在运行的游戏,或者其他类似的操作。同时,也计划设计一些调试界面分组的功能,当进入“离合模式”时,这些分组会展开,退出模式后则会重新收起,这样可以在调试时灵活控制显示的内容,让界面更加简洁和易用。
在 game_debug.h
中,将 debug_variable_group
的内容移到 debug_variable_reference
里。
为了实现所想要的改进,计划解决当前调试系统中的一个问题,即在复制调试层次结构时,状态和数据并没有被完整复制,只是作为引用存在。这个问题虽然看似简单,但实际上需要对调试变量的结构进行一些调整。
目前的调试系统中,调试变量是存储具体数据的,而调试变量组(debug variable group)则负责管理这些数据,包括指向子元素的指针。现在的目标是将调试变量引用(debug variable reference)的概念提升到调试变量组的层次上,也就是说,把所有关于层次结构的元数据移到调试变量引用中。这样,调试变量本身只包含数据,没有层次结构的信息。调试变量组作为管理结构的概念,将被移除,不再直接存在于变量内部。
通过这样的修改,调试系统的结构会更加清晰和简洁,同时也能更方便地复制和操作调试数据和状态。
在 game_debug.h
中,考虑是否要对变量进行更细致的拆分。
为了更好地组织和处理调试变量,需要对现有结构进行一些调整,尤其是要将调试变量和其显示属性分开。当前的调试变量(例如调试位图显示)包含了两类信息:一类是实际的显示数据(如位图的大小、是否显示透明度等),另一类是调试变量的标识符(即具体被调试的内容)。这两类信息目前是合并在一起的,但实际上它们应当分开处理,以便更清晰地管理。
计划的思路是将调试变量和它的属性(例如显示属性)分开成两个不同的层次。调试变量本身只包含数据,例如位图的标识符;而显示属性(如位图的大小、透明度等)则作为一个独立的部分存在。这种做法有助于使调试系统更加模块化和清晰,从而提高其可扩展性和灵活性。
通过这样的结构调整,调试变量和其显示属性将各自独立,避免了信息的混合,使得系统在管理和扩展时更加高效和易于维护。
在 game_debug.h
中,将 debug_variable_
结构体重命名为 debug_tree
和 debug_tree_entry
。
为了进一步优化调试系统的结构,可以考虑将现有的调试变量和调试层级进一步简化,避免过度复杂的结构。当前的系统中,调试变量和它们的显示属性、层级结构等有些过于分散,考虑将它们统一到一个清晰的层次中,以简化管理和使用。
具体来说,可以引入类似“调试层级”或“调试树”的概念,将调试信息按树形结构组织起来,而不是像现在这样有多个重复的元素。这不仅能让结构更加简洁,而且也便于后期扩展和维护。比如,调试变量的层级关系可以通过简化的方式来表达,避免不必要的重复,使得系统的可读性和管理更加直观。
这些调整可能会使代码更为清晰,也有助于后续的维护和升级。同时,也可以考虑适当合并一些冗余的结构,以减少不必要的复杂性,确保系统的高效性。
在 game_debug.h
中,新增 debug_tree_entry_group
结构体。
在设计一个调试树(debug tree)时,可以将每个节点设计为一个具有子节点的结构体,这样形成层级关系。这些节点不仅存储调试变量,还可以包含其他相关的数据,以便在调试过程中提供更丰富的信息。每个节点可以有指向下一个节点的指针,允许树形结构的扩展。
例如,树中的每个节点可以表示一个“组”,这个组内包含多个变量或调试项目。每个组都可能有一个“扩展”标记,用于表示是否展开这个组,允许灵活的管理显示内容。同时,节点还可以包含一个“联合体”数据结构,这样可以根据需要存储不同类型的调试数据,确保每个节点能够灵活地包含不同类型的调试信息。
这种设计将数据的组织方式从原先的简单层级提升到一个更灵活、更高效的结构,使得调试信息的管理和访问更加直观和高效。通过这种方式,调试系统不仅能更好地应对不同的数据类型,还能够通过动态展开和收缩的方式优化调试视图,提高用户的调试体验。
在 game_debug.h
中,重写 debug_tree
。
在调试树的设计中,考虑将调试树的结构简化和调整。调试树应该包含字典条目来表示组,而这些组可以作为树的节点。每个节点可能包含一些基本的调试信息,如组的名称、类型等。调试树结构中的每个节点都可以包含一个指向下一个节点的指针,这样便于构建树形结构。
考虑到简化结构的需求,节点的组织方式应去掉一些不必要的元素,例如不需要“下一节点”和“前一节点”的指针,因为每个节点已经可以通过树的结构自然连接起来。通过这种方式,可以减少不必要的冗余,确保结构的简洁和高效。
在考虑是否将树放在调试信息中时,意识到可能没有太大必要在中间嵌入树结构。实际开发中,调试树应更简单有效,避免不必要的复杂化。重新思考后,决定去掉树的中间插入部分,专注于一个更直观和易于管理的结构。
在 game_debug.h
中,新增 debug_tree_entry_window
结构体。
在设计调试系统时,计划引入一种集成化的结构,包括调试树(debug tree)和一些简单的“窗口”类型。这些“窗口”可以用来表示需要显示或操作的调试数据,用户可以轻松使用这些功能,而不需要额外的定义或复杂配置。例如,对于那些只需要简单调试功能的用户,可以直接使用这些预定义的调试树条目,而不必自己去定义更复杂的结构。
此外,设计中考虑到将“窗口”和“组”作为调试系统的核心组成部分,简化了变量的管理和结构。为了避免冗余,计划移除一些不必要的设置,尤其是那些在当前版本中没有实际内容的配置项。这些空的配置项会作为占位符保留,以便未来有需要时能够添加内容。
整体目标是使系统更简洁、更易于使用,同时保留足够的灵活性,以便在将来能够扩展更多的功能。
在 game_debug.h
中,新增 debug_tree_entry_type
结构体。
在设计调试系统时,计划引入两种类型的调试变量:一种是基础的调试变量类型,另一种是树形结构条目类型。树形结构条目类型(debug tree entry type)将是基于当前的调试条目类型,并将扩展为“变量”(variable)、“组”(group)和“窗口”(window)等具体类别。
这些类型将用于管理调试树中的条目,例如,基本的调试变量和树结构中的分组、窗口等。所有这些类型的条目将被统一归类为调试树条目(debug tree entries),并在系统中进行相应的管理。
这样,系统的设计将更加简洁和清晰,能够有效地将不同类型的调试信息进行分类和组织,使得在调试过程中,用户能够更容易地进行操作和查看相关信息。
回顾这些改动。
现在,可以看到所做的调整非常简单,只是稍微调整了一下结构。核心思路是将调试信息分为两套并行的信息集:一套是调试变量,这些变量可以被检视并进行操作。调试变量可以是由运行的应用程序动态创建,或者根据需求进行创建。
在这些调试变量之上,我们可以加上一层用于查看这些信息的结构,这样就可以在不同的树形结构中查看同一组数据,但每个树的视图方式可以不同。这样,多个树形结构可以同时查看相同的数据,但它们的呈现方式和视角可以不同,这正是目标之一。
此外,在实现时,我们将清理掉所有的编译器生成代码,最终的实现会是一个深拷贝的过程,将这些调试变量及其信息复制到新的位置,这就是所需要的操作。
在 game_debug.h
和 game_debug_variables.h
中,清理编译错误。
现在,开始进行相应的操作,可能需要一些时间。首先,我们已经定义了一个双重树的结构,并且需要提前声明这个结构,因为它是用来创建一个内部的链表的。
接着,我们的变量层级(之前是调试变量层级)现在被简化为“调试树”,因为原来的名称有些复杂,拼写起来不太方便。现在,这个名称更加简洁清晰,所以用“调试树”来代替。
在定义这些结构时,变量引用实际上是一个文档性的概念,并不是直接的条目,它更准确地应该是“调试树条目”。根组和调试树的树哨兵都已经定义好,现在我们需要调整相应的结构,确保它们的定义更加符合实际的使用。
目前的工作重点是创建两种主要的结构:一种是实际的调试变量,它们包含了需要检视的数据;另一种是创建调试树的层级结构,用于组织和查看这些调试变量。
考虑采用基于缓存的系统(caching-centric system)。
虽然手动创建层级结构有些麻烦,但目前并没有太多办法可以避免这个过程。尽管如此,还是有一些方式可以稍微绕过这个问题。比如,可以使用一种缓存系统,来避免每次都直接处理这些数据。
实际上,可以考虑使用这种基于缓存的系统,虽然它看起来与现在的结构相似,但它的解释器是解耦的。通过这种方式,可以简化管理数据的流程,避免直接维护每个层级。这种做法不仅在实现上更有趣,而且从功能上也更强大。所以,虽然现在的方式可以手动创建层级结构,但使用缓存系统可能会让这一过程变得更加高效和有趣。
具体的做法是,我们可以创建一个树结构来存储数据,并且将调试变量与树的层级结构分开。树本身保存了必要的层级信息,而调试变量则包含了每个变量的实际数据。这样,调试变量的不同组别可以分别管理,但其他的属性(如显示参数)则只是针对每个树结构的具体数据。
在这种方案中,每个调试变量将会从一个表格中查找它的状态,而不是一直保存和维护这些状态。这个表格会告诉调试变量如何进行格式化处理,确保在显示时能够正确呈现每个变量的实际数据。这种方法简化了变量的管理,并且使得调试过程中不需要每次都手动处理每个变量的显示属性。
在 game_debug.h
和 game_debug_variables.h
中,开始实现基于缓存的系统。
在这个方案中,目标是通过简化和改进调试变量的管理,避免过于复杂的树结构层级和变量引用,改用更加简洁和灵活的方式。具体来说,首先将调试变量与视图的关联进行简化,使用“调试视图”来表示显示变量的方式,而不再直接依赖复杂的树形结构。
这种做法采用了一个缓存机制,通过哈希表来存储调试视图,而不再依赖每次都重新构建复杂的树形结构。每个调试变量都会被存储为一个基本的视图类型,比如“行内块”,这个视图类型可以包含显示相关的信息,比如大小、状态等。调试视图会通过哈希表与调试变量关联,从而确保可以快速检索到对应的视图。
调试变量本身被看作是“树”的一种抽象,虽然树形结构的部分仍然存在,但更多的是通过变量列表和哈希表来处理。调试变量列表可以是一个包含多个变量的数组,变量在这个数组中被添加或移除。在实现上,通过引入调试变量列表,可以让调试工具更灵活地管理和呈现多个变量。
此外,调试视图不再包含复杂的层级信息,而是简单地依赖哈希表来检索视图,避免了不必要的复杂结构。通过这种方式,调试过程变得更加高效,变量的管理变得更加直接和清晰。调试视图和调试变量之间的关系由哈希表来处理,这样可以避免创建过多冗余的结构。
这种设计的优点是,调试工具变得更加灵活,可以根据不同的需求来展示变量,且不需要过多关注变量引用的问题。所有的变量都被当作普通变量来处理,简化了处理流程。对于复杂的调试信息,所有的数据都可以通过哈希表进行存储和快速访问。
总结来说,这个系统通过引入更灵活的缓存机制,减少了对树形结构的依赖,并通过哈希表来管理调试视图与变量的映射。这样既保证了调试信息的有效存储,也提高了代码的可维护性和灵活性。
暂时暂停调试,并关闭 GAME_INTERNAL
选项。
目前,我们暂时停止了变量系统的重构,以确保当前系统仍然处于可用状态。在此过程中,我们做了一些重要的调整,以优化调试变量的管理方式,并改进变量的组织结构,使其更加合理和易扩展。
我们最初的想法是通过树状结构管理变量,但最终决定简化这个思路,转而采用更轻量级的方法。我们将调试变量视为一个独立的调试视图(Debug View),而不再让它与树状结构过于耦合。所有调试变量都会被存入哈希表,并根据具体的需求决定如何呈现,例如是否作为一个嵌入式块(inline block)或者其他类型的显示方式。
在这个新方法下,我们取消了变量引用(Variable References)的概念,所有变量都统一管理,不再区分“变量”与“变量引用”。所有的调试变量都会存入一个数组,这个数组的大小可以根据需求调整。在存储结构方面,我们创建了 debug_variable_list
(或 var_list
),用来存储一组调试变量,并提供访问和管理的方式。每个 debug_variable_list
仅存储变量,不再额外存储引用信息,从而减少了复杂性。
在实现这一改动的过程中,我们调整了 debug_variable_group
及其相关逻辑。原来的 debug_variable_ref
和 debug_variable_unreferenced
逻辑被完全移除,现在所有变量直接存入 debug_variable_list
,并在需要时通过哈希表查找匹配的调试视图(Debug View)。当创建新的调试变量时,我们会在 debug_variable_list
中分配新的存储空间,并将变量存入其中。
此外,我们还改进了变量的展开状态(expanded state)管理方式。原本变量的展开状态是直接存储在 debug_tree_entry
中,但在新的实现方式下,我们使用 debug_view_collapsible
组件来管理展开状态,并支持不同的展开模式,例如“始终展开”(expanded always)或“根据不同视图展开”(expanded in alt view)。
在数据结构方面,我们在 debug_state
中添加了 debug_view_hash
,用于存储所有 debug_view
的哈希表。这样,每当需要获取某个变量的视图信息时,我们可以通过哈希表快速查找对应的 debug_view
,而不需要依赖树状结构进行遍历。
目前,我们已基本完成变量系统的改造工作,但仍有部分代码需要进一步整理。例如,我们需要优化 debug_variable_group
的创建方式,以确保在 begin_variable_group
之后正确地分配和存储变量组数据。同时,我们也需要考虑是否需要更智能的内存管理方式,比如使用可扩展数组(expandable array)来存储变量组,而不是手动设置固定大小的数组。
最后,为了确保系统稳定,我们暂时禁用了 debug_variable_group_internal
,并保留了原有的变量管理逻辑,以防止影响现有的功能。待后续进一步完善新系统后,我们将彻底切换到新的变量管理方式。新的方式不仅减少了冗余代码,提高了变量管理的清晰度,还使得调试变量的组织方式更加灵活,支持不同的视图模式和更好的参数管理。
接下来,我们将在下一次工作时完成变量系统的迁移工作,并优化变量存储方式,以便支持更合理的组织方式和更清晰的参数存储。
现在调试代码越来越多了,是否会创建一个调试调试系统(debug-debug system)来调试调试代码?
目前,随着调试代码(debug code)的增加,整个调试系统的规模也在变大。在创建调试系统时,核心内容主要围绕调试代码本身展开。与此同时,演示代码(demo code)的能力也得到了增强,甚至可以自我管理和调配资源,这使得整个系统在调试和演示时更加灵活。
实际上,我们已经具备了这种能力,并且在整个过程中都在利用这一特性。例如,在运行演示系统的同时,我们也在对调试系统进行计时(timing),这使得调试和演示能够协同工作,而不会彼此干扰。这种方式不仅提高了调试的效率,也使得调试过程更加直观。
在整个开发过程中,我们一直在使用这种方式,使得调试系统和演示系统能够无缝结合。这样,我们可以在测试功能的同时,对调试代码的运行情况进行监测,从而确保系统的稳定性和性能表现。这种方法的优点在于,它让调试系统能够在演示过程中自动适配,从而减少手动调整的需求,并优化整体的开发体验。
你如何看待预取缓存指令(pre-fetch cache instructions)?它们是否适用于通用编程,以获取最大性能?
在讨论特权缓存指令(privileged cache instructions)是否适用于通用计算以获得最大性能时,我们需要考虑其实际作用以及适用场景。
预取指令(prefetch instruction)的作用
预取指令的本质就是提前告知 CPU 即将访问的内存区域,以便 CPU 可以提前加载数据,减少因数据未就绪而导致的停滞(stall)。如果代码中存在需要访问某块内存的情况,而该访问可能会导致处理器停滞(例如,在一个循环中反复访问某些数据),那么可以在执行这些访问之前很多个周期(如 300 个周期)提前插入预取指令,这样处理器就有足够的时间去获取相应的数据,从而减少等待时间,提高整体执行效率。
预取指令的适用条件
需要提前知道访问的内存地址
预取指令的有效性完全取决于是否能提前预知即将访问的内存地址。如果代码执行过程中无法提前知道哪些数据将被访问,预取指令就无法发挥作用。因此,它最适用于那些访问模式可预测的场景,比如循环结构或结构化的数据访问。不能达到最大内存带宽
预取指令的作用是减少缓存未命中(cache miss)造成的停滞,而它的前提是系统尚未达到最大内存带宽。如果 CPU 已经完全占满了可用的内存带宽(full memory bandwidth),那么额外的预取指令不会提高性能,因为此时已经达到了吞吐量极限。换句话说,预取指令的作用是优化缓存命中率,而不是增加内存吞吐量。仅在缓存未完全饱和的情况下有效
预取指令的另一个限制在于,它仅在缓存访问导致停滞且内存带宽未被完全利用时才有效。如果处理器已经完全利用了带宽并达到了最大吞吐量,预取指令不会带来额外的性能提升,因为系统本身已经达到了硬件能力的上限。
总结
预取指令的作用是减少缓存未命中导致的处理器停滞,从而提升程序执行效率。它在以下情况下最为有效:
- 代码可以提前知道即将访问的内存地址;
- 内存带宽尚未被完全占用;
- 存在因缓存未命中导致的 CPU 停滞。
如果无法满足这些条件,预取指令的效果可能有限,甚至可能导致额外的指令开销。因此,在具体应用时,需要根据程序的访问模式、缓存命中率和内存带宽利用情况进行权衡和优化。
目标系统的架构是否会影响你设计调试系统的方式?
在设计调试系统时,目标系统的架构通常不会对设计产生重大影响。调试系统的基本结构通常是通用的,无论运行环境如何。不过,在某些特定情况下,例如目标系统的性能较低,可能需要做出一些调整。
如果目标系统是一个性能较弱的平台,例如 Nintendo DS 这样的设备,那么可能不会使用调试覆盖(debug overlays),因为这会占用系统资源。相反,调试信息通常会被导出到一个 调试端口,并由另一台更强的设备捕获和显示调试数据。
而在运行于高性能设备(如 PC 或强大的主机)时,调试系统通常可以直接在本机运行,而不需要额外的设备来承担调试任务。因此,在 弱性能系统 上,调试系统的设计可能会倾向于将调试工作交给另一台设备,而在高性能设备上则不会有太大限制,调试系统可以直接集成并运行在同一设备上。
你对 《无人深空》(No Man’s Sky) 有什么看法?我现在完全无法想象它的工作原理,学习你的开发方式后,我会对这类游戏的实现有更深入的理解吗?
《No Man’s Sky》的核心技术是程序化世界生成,其工作方式与《Minecraft》类似,都是基于按需生成的概念。当玩家移动到新的区域时,系统会请求该区域的世界数据,并根据所需的**细节层级(Level of Detail, LOD)**来生成对应的内容。
当某个区域的细节需求较低时,系统返回的只是一个粗略的版本,而当玩家靠近并需要更高的细节时,系统会进行进一步的细节填充并动态生成内容。这种技术并不是魔法,而是程序化生成技术的自然应用。在现代硬件的支持下,这种方法可以实现极高的细节度,从而创造出广阔而丰富的游戏世界。
这种技术并不是全新的概念,类似的方式在过去的项目中也曾被使用过。区别在于,现在的计算能力已经足够强大,可以在更高的精度上执行这些生成任务,从而创造更为庞大和复杂的世界。
当你发布源码后,我们能创建自己的boss、世界等内容吗?
如果拥有源代码,那么理论上可以进行任何修改和扩展。可以添加新的Boss、世界、机制,或者进行任何形式的改动,只要有足够的编程能力,就没有任何限制。源代码的开放意味着可以完全掌控游戏的逻辑和功能,因此可以自由地开发和调整各种内容,按照自己的想法来塑造游戏体验。
你会很快回到游戏本体的开发吗?
目前的编程学习方向并不是专注于展示某个具体的内容,而是更侧重于编程教育本身。因此,不会因为某些人希望快速看到特定内容的编写就匆忙推进,而是会花足够的时间深入学习各种知识点。由于所有内容都可以按需回放,如果对某个环节不感兴趣,可以随时跳过,等到后续感兴趣的部分再回来观看。
游戏玩法(Gameplay)的编码实际上是编程中最无聊的部分之一,通常相当基础,很多时候类似于脚本编写。例如,移动一个对象、改变其位置、让其旋转等,基本上是简单的数值调整和逻辑处理,没有什么特别复杂的内容。因此,与其关注这类代码,不如专注于更具挑战性的部分,比如构建高效的 UI 系统,这样的内容涉及优化和架构设计,真正具有技术难度,也更值得学习。
相比之下,游戏玩法代码相对简单,通常不会涉及太多深奥的逻辑。因此,并不会特意加快进度去进入游戏玩法代码的编写,因为相比之下,系统架构、优化、工具开发等部分才是更具挑战性和价值的内容,值得投入更多时间去研究和掌握。
树莓派(Raspberry Pi)或其他 ARM 架构的 PC 能否运行 game Hero?如果使用这样的系统,你会考虑在另一台机器上进行调试吗?
如果使用树莓派(Raspberry Pi)或其他基于 ARM 架构的桌面 PC 来运行当前的项目,理论上是可行的,但性能上可能会受到一定限制。
树莓派的新版本相比最早的版本拥有更强的处理器,尤其是树莓派 2 及之后的版本,引入了四核架构,性能有所提升。因此,能否流畅运行取决于具体的硬件配置。如果是较早的树莓派版本,想要完全通过软件渲染方式运行游戏,几乎是不可能的,因为其计算能力远远不及 x86 处理器,必须依赖 GPU 进行加速。
至于较新的树莓派,或许可以运行,但可能无法达到 1080p 分辨率,需要降低画质或分辨率来保证流畅度。不过,由于没有在树莓派 2 或更新的设备上进行过测试,因此暂时无法给出明确的结论,不排除其具备一定的运行能力的可能性。
我指的是游戏内部,是否可以在调试菜单里切换优化选项?
可以在调试菜单中添加一个选项来切换游戏的某些特定状态,这是完全可行的,并且实现起来非常简单。
具体来说,可以使用程序优化技巧来完成这个功能。这样,就可以把这个状态变量作为标准变量,直接放入 DEBUG_VARIABLE_LISTING
文件中进行管理。这种方式能够简化变量的存取,同时也能确保它可以轻松地从调试菜单中进行切换。
因此,接下来的优化方向应该是:
- 调整代码,确保变量能够被调试系统识别并管理。
- 在调试菜单中添加对应的选项,允许实时开关某个特定状态。
- 验证实现效果,确保切换功能能够正确应用于游戏运行时。
接下来就可以着手实现这个调整。
你觉得可视化编程语言(GUI Visual Scripting)用于游戏脚本开发是否是个不错的主意?比如 Unreal 的蓝图(Blueprint)系统?
对可视化脚本的看法并不十分赞成,因为可视化脚本通常会变得非常复杂,充满了大量的节点,看起来像是一个巨大的混乱。对于程序员来说,这种方式并不特别有效,因为它最终可能只需要几行代码就能实现同样的功能。
从个人经验来看,可视化脚本并没有带来太多好处。它的存在似乎是为了替代传统编程,但往往没有显著提高效率,反而可能让流程变得更加繁琐,尤其是当涉及到复杂的逻辑时。对比起用代码直接解决问题,这种节点化的工作流显得非常冗长且不够直观。
简而言之,可视化脚本的优势并不明显,大多数情况下,直接用几行代码处理问题会更加高效清晰。
黑板讨论:可视化脚本(Visual Scripting)。
对可视化脚本语言的看法非常不喜欢。相比起传统的文本编程,可视化脚本在很多情况下显得非常笨重和难以理解。举个例子,当想要实现一些简单的功能,比如设置一个角色的健康值,可以直接写出类似“health = aimAmount * 2 / 5
”这样的代码,一行就能完成,既简洁又容易理解。而且,如果需要,还可以轻松在这行代码上方添加注释,解释它的作用,便于后续理解和维护。
但使用可视化脚本时,这个简单的逻辑就变得异常复杂。可能需要创建许多节点,比如一个“驱动节点”来设置值,再用另一个“乘法节点”将两个数相乘,然后再用“加法节点”进行加法操作,最后再进行除法处理。每一步都需要一个独立的节点,而且这些节点之间需要通过管道连接。结果是,原本一个很简单的计算逻辑,变成了一个充满节点和连接的混乱图,让人难以理解。每个节点背后都代表着一个看似简单却需要过多步骤的操作,最终让人觉得这一切变得极为冗长和混乱。
对于这种方式,尽管它可能有一定的使用场景,但无法忍受这种复杂的、零散的方式来做一些本可以轻松解决的任务。直接用代码写出来会更加简洁清晰,且更容易理解。虽然可能存在一些优势,但对自己来说,可视化脚本总是显得不直观,甚至让人感到沮丧。
你能否只整理上一帧的数据,而不是一次性处理所有帧的数据?
目前的问题在于,虽然每一帧都可以处理最新的数据,但实际上无法无限制地这样做。因为我们并没有使用滚动缓冲区,最终数据空间会被填满,一旦写入的数据没有地方存储,就需要清空缓冲区,然后重新开始处理旧的数据。因此,不能一直仅处理最新的数据。
解决方法就是实现一个循环缓冲区,这样就可以在数据写满后,自动覆盖旧数据,而不需要清空整个缓冲区。代码已经在增量处理方面能够正常工作,只是缺少了这个“滚动”的功能,导致在某些时刻需要清空数据并重新开始处理。
总的来说,问题的关键在于没有循环缓冲区,而一旦实现这个功能,代码就能更高效地工作。当前的队列处理看起来已经完成,可以继续推进。