游戏引擎学习第190天

我们编写用户界面的唯一原因是为了支持我们的调试系统

现在我们已经进入了可以开始进行一些UI相关操作的阶段。很多人之前问过关于UI的问题,可能是因为我在GUI方面的声誉,大家经常会问这个问题。老实说,并不是一个涉及UI的游戏,游戏中没有按钮或者小部件,玩家直接控制角色。所以,最开始我们并没有特别的理由去实现任何真正的UI元素。

但是,由于游戏中已经有了调试代码,并且我们希望能够有效地使用这些调试功能,通常在这种情况下,UI就变得很有必要了。

由于我们是唯一的UI用户,我们不在乎它是否直观,只要它能提供高效的交互且实现成本低

在这种情况下,我们尝试做的是编写用户界面(UI)代码,目标非常明确。通常,如果你在做面向最终用户的UI代码,目标是让它对用户尽可能友好、易用,不管是游戏中的玩家,还是一些商业软件的使用者,都是为了让他们能方便地使用这个界面。

但在我们这个情况中,UI的使用者只有我们自己,因此我们的关注点只有两个。第一个是能否快速使用它,让它足够高效地支持我们的需求,因为我们是代码的编写者,完全知道如何使用它。所以它不需要具有可发现性,也不需要特别容易上手,只要足够高效地完成任务即可。第二个,也是最重要的,就是实现成本要尽可能低,因为我们不想花大量时间去编写UI代码或者处理UI问题,特别是这些UI最终只会我们自己看到。

我们不在乎UI看起来有多简陋,因为只有我们能看到。任何花费在美化UI、增加更多功能或改善体验上的时间,通常都是浪费的,除非有特别好的理由去做。所以我们想要做的是尽量找到一种实现UI的高效方式,能够通过最少的代码实现最多的调试功能。这就是我们的目标。

回顾

已经看到我稍微勾画了一些UI的样子,上周我们也实现了一些基本的功能,比如可以悬停在某些元素上查看它们的状态,另外还可以点击它们,进一步钻取更深层次的信息。

我们的UI代码方法

注意到,当然在实现这些功能时,并没有做什么特殊的事情。我没有创建一个庞大的UI层级,也没有为了实现一个按钮而设计800,000个类。我只是简单地在代码里加了一个if语句,判断当前是否悬停在这个矩形上,如果是,就将它变成热区;然后如果点击了按钮,它就会执行相应的操作。就这样而已。这种做法是我认为在实现调试UI时应该采取的态度:做最容易实现的功能,既不浪费时间,又不引入代码腐化,简洁地与实际输出代码紧密结合。渲染和UI要保持一致,并且随着渲染的扩展,UI能够自然跟随。这就是我们在实现调试UI时要追求的目标,直到它足够好,能在项目中使用。接下来,我们会稍微调整一下进度。

今天的初步计划:关闭性能分析可视化

从上周我们正在做的工作开始回顾一下,因为在结束时我匆忙做了一些事情。我们可以稍微退一步,首先我们需要做的是想办法打开或关闭这个性能分析视图。现在,如果我运行游戏,屏幕上会显示很多性能分析条,虽然我们在玩弄这些功能时觉得还好,但问题是,这样会遮挡游戏画面。因此,最基本的需求是我们需要有一个方法来决定是否绘制这些内容。如果我们正在开发游戏,并且暂时不关心性能分析和细节内容,那么我们至少应该有一个选项来完全关闭性能分析,或者考虑设计一个精简版的视图,它只是一个非常小的调试监视版本,位于屏幕角落,只绘制那些必要的内容,提醒我们可能错过的潜在问题。

例如,可以显示每帧的总时间或帧率波动之类的信息,这样我们就能注意到异常情况,从而快速进行调整。我的目标是专注于这个调试渲染功能,确保它能以一种更加可靠的方式来实现。到目前为止,每当我们需要实现调试功能时,通常会遇到将它们强行嵌入到现有代码中的问题,我们没有一个好的方法来开启和关闭不同的视图。尽管我们有实时编码的能力,这虽然有所帮助,但还是存在一些问题。

调试UI可以很好地补充实时代码编辑

通过实时编码功能,虽然能够有效解决在调试某一特定代码段时的问题,因为我们正好在那一刻集中在那段代码上,实时编码是完全适用的。然而,当我们有多个不同的调试可视化功能时,实时编码就不再那么方便了。例如,如果我们想要绘制实体的边界框,虽然我可以通过修改代码来实现这一功能,然后重新编译回来,这种方式虽然有效,但相比之下它更加缓慢和不方便。如果我们能通过一个开关列表,快速点击开启和关闭功能,就会更高效。

因此,我们需要将这些功能集中在一个地方,这样可以快速地控制不同的调试选项。即便我们决定不使用调试UI,至少也要在某个文件中集中管理所有的调试开关,以便能够更轻松地访问和修改这些开关,确保能够快速切换。这是一个显而易见的需求,值得我们开始处理。

接下来,我将开始进行一些代码转换,尽管目前还没有明确的目标,但我知道我需要一些按钮或其他方式来与这些调试功能进行交互。现在,我将打开游戏调试文件,

计划变更:我们将去掉全局调试变量

首先,我想做的第一件事是清理这些全局变量,因为我们在之前工作的过程中随便把它们加进去了。现在我们已经有了调试状态的概念,实际上这些全局变量已经没有必要存在了,所以现在是时候清理它们了。我感觉现在是一个很好的时机,去掉这些全局变量,因为我们已经完成了存储调试状态的工作。

如果我没记错的话,使用这些全局变量的另一个问题是它会导致我们的实时代码编辑功能破坏调试系统。我不完全记得是否真的是这种情况,但我觉得应该是这样,因为本地变量不会正确重置。虽然我不确定这个问题是否真的存在,但当我重新编译代码时,调试系统就会出现问题,这显然不是一个好的情况。

因此,去除这些全局变量的另一个原因就是避免这些变量无缘无故出现在全局作用域中。这实际上是非常简单的事情,所以我决定首先清理这些全局变量。

将全局变量移入调试状态

首先,将相关的文件或组件移动到指定的目录中,并执行一些清理工作以确保没有多余的内容或错误。接着,检查代码中是否有因这些文件移动或更新而产生的编译错误。这些错误通常是因为某些引用或依赖项未正确更新。为了处理这些错误,需要逐一查看每个依赖项,确保它们能够通过正确的调试状态来调用相关的资源或模块。

然后,需要对代码进行调整,确保所有使用这些资源的地方都能适应新的结构和调用方式。这包括更新引用路径、修改函数调用方式等。每个可能受到影响的部分都需要逐步调试,确保它们可以正常运行并与新的系统架构兼容。

通过这些步骤,能够逐渐消除编译错误,确保所有组件都能顺利工作,最终实现一个功能正常且稳定的系统。

通过DebugGlobalMemory访问调试状态会更好。实现DEBUGGetState

在处理调试状态时,发现最关键的是将调试状态指针设为全局变量。具体来说,调试状态全局变量(DebugState global)实际上就是游戏内存的全局变量(DebugGlobalMemory)。通过这个内存全局变量,就能获取到调试存储(DebugStorage),从而确保每次进入系统时,调试状态能够正确地被访问和使用。

为了简化流程,决定在代码中创建一个函数,该函数直接从调试全局内存中获取调试状态。这样就避免了每次手动传递调试状态参数的问题,而是通过一个简洁的接口,比如 DebugGetState(),始终可以直接获取调试状态。这种方式的好处是,无论在哪个地方,只要能访问到调试全局内存,就能获取到调试状态,保证了代码的一致性和易维护性。

对于调试服务,代码中还需要做一个简单的判断,确保如果没有启用调试服务,就不会执行与调试相关的操作。这部分可以通过 if 语句进行判断,如果没有调试服务,则什么都不做,保持系统的正常运行。

另一个问题是调试渲染组(DEBUGRenderGroup)。这个渲染组的定义应该更直接地与调试状态相关联,而不应该通过单独的条件判断来进行。为了确保一致性,决定将渲染组的获取也放入调试状态中,这样一旦获得了调试状态,就可以直接获取渲染组,避免了重复判断和不必要的复杂性。

最终,所有的调试相关的全局变量,如字体 ID、字体缩放等,都应该直接通过调试状态来访问。这样,所有的全局变量都可以通过调试状态这个统一接口来获取,避免了多次传递和管理全局变量的问题,确保代码结构更加清晰、简洁。

最后,还考虑到调试覆盖层(debug overlay),之前它需要接收游戏内存作为参数,但现在因为所有的调试信息都可以从调试状态中直接获取,因此不再需要传递游戏内存,只需要通过调试状态就可以访问到所需的数据。这样可以减少不必要的复杂性,提升代码的可读性和维护性。

经过这些调整后,系统应该能更加高效和一致地处理调试信息,所有的调试相关操作都通过统一的路径执行,使得代码结构更加简洁、易于维护。

测试。我们去掉了那些全局变量

目前整体结构已经趋于合理,所有全局变量的使用已经移除,系统仍然能够正常运行。但仍然存在一个问题,即如果重新编译,调试渲染组(DEBUGRenderGroup)会消失,因此还需要进一步修正这个问题。

之前已经将大部分全局变量移入调试状态(debug state),但这还不够彻底。需要确保所有全局变量都被正确存储在调试状态中,并且系统中不再依赖全局变量,以避免潜在的冲突。因此,下一步的关键是确保调试状态能够存储这些必要的信息,而不是依赖外部全局变量。

在调整过程中,需要在调试状态内部创建一个渲染组,并确保它能够被正确初始化。这样一来,所有渲染相关的操作都可以通过调试状态来访问,而不会因为全局变量的消失而导致功能丢失。此外,为了确保初始化顺利进行,需要预先声明相关的变量,并确保它们在合适的地方被正确初始化。

在组织这些变量时,需要考虑最佳的初始化方式,以确保调试系统能够稳定运行。所有依赖调试状态的变量都会按照合理的方式进行初始化,并在使用时能够正确引用,确保整个调试系统的稳定性和一致性。这样一来,就能彻底解决全局变量的问题,同时保持代码的整洁性和可维护性。

DEBUGRenderGroup的内存来自临时内存池。它应该来自调试内存池

在渲染组(render group)的分配过程中,当前的实现方式是从瞬态内存(transient arena)中进行分配,但这并不是一个合理的做法。瞬态内存通常用于短期分配,而调试渲染组(debug render group)应该是一个持久存在的结构,因此不适合使用瞬态内存进行分配。

为了解决这个问题,决定不再使用瞬态内存,而是改为从调试内存(debug memory)中进行分配。这样可以确保调试渲染组始终存在,不会因瞬态内存的重置或释放而丢失数据。

为了实现这一点,需要调整当前的分配逻辑,将渲染组的创建过程迁移到调试状态管理的内存区域。同时,还需要确保可以正确获取资产信息(asset information),因为渲染组的某些操作依赖于这些信息。从代码结构来看,DEBUGReset 似乎已经包含了这部分逻辑,并且会接收游戏资产(game assets)作为参数,因此可以利用这一机制,在重置调试状态的同时确保渲染组能够正确初始化。

通过这种调整,可以优化调试渲染组的存储方式,使其更加稳定,同时避免因瞬态内存释放而导致的潜在问题。此外,这种方法也能让调试系统更加清晰、易维护,提高整体代码质量。

在DEBUGReset中执行DEBUGRenderGroup分配

当前的目标是将调试渲染组(debug render group)的分配过程彻底从原有的位置移动,并将其整合到 DEBUGReset 之中。在 DEBUGReset 内部,如果发现调试渲染组尚未创建,就在这个时机进行分配,并使用已有的资源来初始化它。这样可以确保调试渲染组在需要的时候被正确创建,同时避免不必要的重复分配或资源浪费。

DEBUGReset 运行时,系统已经具备了初始化所需的信息,因此在这里执行渲染组的分配是合理的。具体实现上,首先检查 DebugState 是否已经包含了渲染组,如果没有,就进行创建,并确保其正确初始化。这样可以保证整个调试系统在 DEBUGReset 执行后始终处于一个可用的状态。

此外,在执行分配时,还需要处理内存分配的来源。目前的 DEBUGReset 内部已经有一个内存分配区域(arena),但当前使用的是 collation arena,而 DebugState 可能需要一个更加稳定和专门的内存管理方式。因此,还需要考虑调整 DebugState 的结构,使其能够更好地管理自身的数据,而不仅仅依赖于 collation arena

总的来说,这次调整的核心目标是:

  1. 将调试渲染组的分配逻辑移至 DEBUGReset,确保它在适当的时机创建。
  2. 利用已有的资源和信息 ,在 DEBUGReset 内部完成初始化,而不是依赖外部临时分配。
  3. 调整 DebugState 的内存管理方式,确保它能够长期存储调试相关的信息,并在系统重置时保持一致性。

通过这些调整,可以让调试系统更加稳健,同时减少对全局变量和瞬态内存的依赖,使代码结构更加合理、易维护。

访问调试状态应该意味着它的初始化

当前的目标是确保无论函数调用的顺序如何,调试状态(debug state)始终能够正确初始化并正常运行。为此,采用了一种常见的编程约定:每次访问调试状态时,都会检查其是否已经初始化,如果尚未初始化,则立即进行初始化。这样可以确保所有需要调试状态的代码在任何情况下都能正常运行。

核心改进点:

  1. 在获取调试状态时自动初始化

    • 每次调用 DebugGetState 时,都会检查 DebugState 是否已经初始化。
    • 如果 DebugState 为空或未初始化,就在此时完成初始化。
    • 这样,所有访问 DebugState 的代码都可以保证始终获得有效的数据,而不必依赖外部手动初始化。
  2. 提供两种访问 DebugState 的方式

    • 如果有全局游戏内存(global game memory),则直接从中获取 DebugState
    • 如果已经知道具体的 game_memory,则可以直接传递它,绕过对全局变量的依赖。
    • 这样可以提升灵活性,在某些情况下,即使 DebugState 还未被显式设置,也能确保系统正常运行。
  3. 优化 DEBUGReset 的初始化逻辑

    • 需要确保 DebugStateDEBUGReset 时能够正确初始化,并且所有必要的数据结构(如 debug_render_group)都已分配到正确的内存区域。
    • DEBUGReset 过程中,使用 DebugGetState 来确保 DebugState 可用,并根据需要初始化相关数据。
    • 这样可以确保 DEBUGReset 之后,整个调试系统处于可用状态,无需额外的手动配置。
  4. 引入 debug_arena 进行统一管理

    • DebugState 内部创建 debug_arena,用于管理调试系统的内存分配。
    • 这样可以确保所有调试相关的资源分配都来自于同一内存池,而不会分散在不同的内存区域中,提高可控性和可维护性。

最终效果:

  • 无论函数调用顺序如何,调试系统都能自动初始化,不会出现未定义行为。
  • 减少对全局变量的依赖,使代码更易维护,同时提供更灵活的访问方式。
  • 通过 debug_arena 统一内存管理,提高调试系统的稳定性和可控性。
  • 确保 DEBUGReset 之后,所有调试相关的资源都处于正确的初始化状态,无需额外的手动调整。

通过这些优化,调试系统将更加健壮,能够适应各种复杂的运行环境,同时提高代码的清晰度和可维护性。

CollationArena将作为DebugArena的子内存池

当前的优化目标是将 debug_arena 作为主要的调试内存区域,并采用分层访问机制来管理内存分配,从而提升调试系统的稳定性和可维护性。

核心改进点:

  1. 引入 debug_arena 作为主调试内存池

    • debug_arena 作为整个调试系统的主内存区域,所有调试相关的分配都从这里获取内存。
    • 这样可以确保所有调试数据的管理更加集中,避免多个分散的内存区域导致管理混乱。
    • 统一管理调试相关数据,提升调试系统的可预测性和稳定性。
  2. 实现分层内存管理机制

    • 采用 子内存池(sub arena) 的方式,从 debug_arena 中划分出 collation_arena(归并处理内存池)。
    • 这样可以确保 collation_arena 的内存分配受 debug_arena 统一管理,同时避免影响主调试系统的其他部分。
    • collation_arena 的大小可以调整,当前暂定 32MB,但未来可根据需求动态调整。
  3. 调整内存对齐策略

    • 由于 collation_arena 主要用于数据归并处理,不一定需要 16 字节对齐,因此调整默认对齐方式,使其更加灵活。
    • 这样可以在不影响性能的情况下,优化内存使用,提高调试系统的适应性。
  4. 初始化 debug_arena 并确保子区域正确分配

    • debug_arena 需要在 debug_reset 时正确初始化,并划分 collation_arena
    • 这样可以确保在调试系统重置后,所有相关资源都处于正确的初始化状态,无需额外手动配置。

最终效果:

  • 所有调试相关内存分配统一管理,提升稳定性。
  • 采用子内存池方式,使不同功能模块的内存管理更加有序,避免相互干扰。
  • 调整内存对齐方式,提高内存使用效率,使 collation_arena 适配不同的内存需求。
  • debug_reset 时自动初始化 debug_arena,确保所有子区域正确分配,避免手动调整带来的错误。

通过这些优化,调试系统的内存管理将更加高效、稳定,并适应未来可能的扩展需求。

初始化调试RenderGroup

当前的优化主要集中在 调试渲染组(debug render group)的初始化和内存管理 ,目的是确保渲染相关的调试数据能够正确分配,并且在 首次使用时自动初始化,避免手动管理导致的不一致问题。

优化重点:

  1. 调整 debug render group 的初始化时机

    • 通过检查是否已经初始化,如果未初始化,就在首次访问时进行初始化。
    • 这样可以避免在各个模块手动初始化,提高系统的可靠性。
    • 统一管理渲染调试数据的初始化逻辑,减少重复代码。
  2. 内存分配调整:从 debug_arena 申请

    • 过去,debug render group 的内存可能是从多个不同的地方分配的,导致管理混乱。
    • 现在统一改为 debug_arena 申请,确保所有调试相关的内存都来源于同一内存池。
    • 这样可以避免潜在的 内存碎片化 ,并确保 debug render group 只在调试环境下生效,而不会影响主游戏逻辑。
  3. 设置合理的内存大小

    • 在初始化时,可以通过参数指定 需要多少 MB 的内存,避免不必要的资源浪费。
    • 未来如果需要调整大小,可以 直接修改参数 而不用改动底层逻辑,提高可维护性。
  4. 渲染调试组的分层管理

    • debug_render_group 初始化后,可以将其作为 debug_state 的一部分,避免全局变量的使用。
    • 这样可以确保所有调试组件能够 统一从 debug_state 访问 debug_render_group,避免外部代码直接操作它,提高封装性。

最终效果:

  • 渲染调试数据的初始化在首次访问时自动完成,不需要手动管理。
  • 内存分配集中在 debug_arena,提高系统稳定性并避免内存碎片化。
  • 可以通过修改参数调整 debug_render_group 的内存大小,提升灵活性。
  • 所有访问 debug_render_group 的代码都统一通过 debug_state 访问,减少不必要的耦合。

这一优化确保了 调试渲染系统的初始化流程更加稳健、可维护性更高,并减少了潜在的错误

DEBUGReset将调用BeginRender

当前的优化思路主要是 将调试相关代码封装到 debug 模块中,以提高代码的组织性和可维护性

优化要点:

  1. debug_render_group 的初始化和使用封装在 debug.cpp

    • 目前 debug_render_group 的初始化和使用散落在各个地方,使得代码结构混乱。
    • 现在的优化思路是:所有 debug 相关的操作应该 全部封装在 debug.cpp,而不应该暴露在其他模块中。
    • 这样可以 避免其他系统直接操作 debug_render_group ,确保所有调试逻辑都经过 debug.cpp 进行管理。
  2. 封装 debug 相关的逻辑,提供清晰的 begin / end 接口

    • 目前的 debug 逻辑分散,部分代码块看起来孤立,没有统一的管理方式。
    • 现在的做法是 提供 DebugBegin()DebugEnd() 这样清晰的接口 ,所有调试渲染逻辑都应该在 DebugBegin()DebugEnd() 之间执行。
    • 这样可以确保:
      • 所有调试代码都必须在合适的上下文中运行,避免状态未初始化的问题
      • 代码逻辑更清晰,便于调试和维护
  3. 优化 DebugReset(),让其自动处理调试状态

    • 目前 DebugReset() 只负责部分调试初始化,而 debug_render_group 需要单独处理。
    • 现在的优化方案是:DebugReset() 直接管理 debug_render_group ,确保 不需要额外的初始化步骤
    • 这样,每次调用 DebugReset() 时:
      • 不需要手动检查 debug_render_group 是否已初始化,它会自动处理。
      • 调用方只需要调用 DebugReset(),不再需要关注内部细节,提高代码的封装性。

最终效果:

  • 所有 debug 相关逻辑都封装到 debug.cpp,提高模块化和封装性。
  • 使用 DebugBegin()DebugEnd() 统一管理调试流程,提高代码的可读性和可维护性。
  • DebugReset() 统一管理 debug_render_group,调用者不需要再手动初始化,提高可靠性。

这次优化使得 调试代码的结构更加清晰,降低了耦合度,提高了维护效率

DEBUGOverlay将调用EndRender

优化要点:

  1. DEBUGOverlay 整合到 debug.cpp,作为一个独立的 time function

    • 目前 DEBUGOverlay 逻辑分散,部分代码使用了 timed block,导致代码可读性较差。
    • 现在的优化思路是 DEBUGOverlay 作为一个独立的 time function ,这样可以:
      • DEBUGOverlay 单独执行 ,无需嵌套在 timed block 里,提高可读性。
      • 减少重复代码 ,让 DEBUGOverlay 只负责自己的逻辑,而不影响其他调试代码。
  2. 清理和整理 debug 相关的代码

    • 目前 debug 代码逻辑混乱,部分代码不清楚是否需要保留或删除。
    • 现在的优化方案是:
      • 删除多余的检查和重复逻辑 ,确保 debug 代码尽可能简洁。
      • 统一 debug 代码的组织方式 ,比如 DebugBegin()DebugEnd() 之间的逻辑应该保持一致。
      • 这样可以 提高 debug 代码的可维护性,避免未来添加新功能时引入混乱。
  3. 确保 debug 逻辑能够正确获取 assets 信息

    • 目前 debug 代码需要访问 assets,但获取方式不够可靠。
    • 现在的优化思路是:
      • debug 初始化时,确保 assets 传入 debug 系统,而不是在 debug 逻辑内部自行查找。
      • 这样可以 确保 assets 始终可用 ,避免 debug 逻辑在某些情况下因为 assets 不可用而崩溃。

最终效果:

  • DEBUGOverlay 作为一个独立的 time function,提升代码整洁度和可读性。
  • 整理 debug 代码,删除不必要的检查,减少冗余代码,提高维护性。
  • 改进 assets 传递方式,确保 debug 逻辑能可靠访问 assets,提高系统稳定性。

这次优化使得 debug 代码更加清晰、稳定,减少了未来维护的复杂性

调试系统不应依赖资产系统;调试资产应编译到二进制文件中

优化要点:

  1. 减少 debug 依赖 assets,提高稳定性

    • 目前 debug 代码依赖 assets,但如果 assets 载入失败,debug UI(如字体渲染)也会失效,导致无法进行调试。
    • 优化方案:
      • debug 代码 尽量减少对 assets 的依赖
      • 例如,可以 预置一个默认字体 ,直接存储在 静态数组 中,而不是依赖 assets 系统。
      • 这样即使 assets 出现问题,debug 仍然可以工作,显示基本的调试信息。
    • 当前策略:
      • 暂时保留 assets 依赖,但未来可能考虑预置字体到可执行文件中,以提高稳定性。
  2. 优化 RestartCollation 逻辑,简化 CurrentEventArrayIndex 处理

    • RestartCollation 逻辑目前会检查 CurrentEventArrayIndex,但实际上这个索引在 restart总是 0,所以这个检查是多余的。
    • 优化方案:
      • 直接省略这个检查,因为 restart 过程本身就会确保索引归零。

      • 这样可以 减少冗余逻辑 ,提高 collation 处理的效率。

  3. 确保 debug 代码正确获取 HighPriorityQueue 信息

    • 目前 debug 代码需要访问 HighPriorityQueue,但获取方式不统一。
    • 优化方案:
      • debug 初始化时,从 全局 game memory 直接提取 HighPriorityQueue 信息,而不是在 debug 代码内部查找。
      • 这样可以 确保 debug 始终能获取正确的 HighPriorityQueue,提高系统稳定性。
  4. 调整 draw buffer 传递方式,优化 debug overlay 逻辑

    • debug overlay 需要知道在哪个 draw buffer 上进行绘制,但当前传递方式不够清晰。
    • 优化方案:
      • 显式传递 draw bufferdebug overlay ,确保 debug 系统知道正确的绘制目标。
      • 这样可以 提高代码可读性,减少调试错误

最终效果:

  • debug 代码减少对 assets 的依赖,提高健壮性。
  • 简化 RestartCollation 逻辑,减少不必要的检查,提高效率。
  • 优化 HighPriorityQueue 访问方式,确保 debug 代码能正确获取相关信息。
  • 优化 debug overlay 逻辑,明确 draw buffer 传递,提高代码可读性。

这次优化确保 debug 代码更加稳定、可维护,并减少潜在的调试问题

如果我们希望调试系统访问资产系统,我们需要确保资产系统首先初始化

优化 debug 系统的 assets 依赖与初始化时机

  1. debug 系统依赖 assets 主要是为了字体,但可能需要扩展支持贴图

    • 目前 debug 代码依赖 assets 主要用于字体渲染
    • 未来可能会 增加贴图查看功能 (例如 debug 面板显示 texture preview),这就需要 assets 访问能力。
    • 因此,debug 访问 assets 并不是完全不合理的设计,但需要优化初始化流程。
  2. debug state 的初始化时机难以确定,可能会与 assets 加载顺序冲突

    • debug state 的初始化时机 目前不确定 ,可能发生在 assets 加载前或加载过程中。
    • 如果 debug 依赖 assets,但 assets 还未完成加载,就会出现问题。
    • 潜在风险:
      • 未初始化时访问 assets → 可能导致 NULL pointer未定义行为
      • 多线程问题 → 可能在不同线程中读取 assets,如果没有同步机制,会导致 race condition
  3. 优化方案:强制 debug stateassets 配置完成后初始化

    • 解决方案 1:手动控制初始化顺序
      • assets 加载完成后 显式调用 debug state 初始化 ,确保 debug 访问 assetsassets 已准备就绪。
    • 解决方案 2:缓存 assets 引用
      • debug state 允许在 assets 还未完全加载时启动 ,但 不会立刻访问 assets ,而是等 assets 加载完成后,再从 assets 中获取所需数据(如字体)。
    • 解决方案 3:使用默认资源(fallback)
      • debug 代码 使用默认内置字体 ,如果 assets 加载完成后可用,就切换到 assets 提供的字体。
      • 这样即使 assets 失败,debug UI 仍然可用,提高系统稳定性。
  4. 确保 debug 线程安全,避免 race condition

    • 所有 debug 资源都应该缓存在 buffers ,确保 debug 代码的执行是 线程安全的
    • assets 访问应该是异步的 ,避免 debug 线程直接访问 assets,改为从 buffer 读取数据。

最终优化方案:

  • debug state 只有在 assets 加载完成后才初始化,确保 assets 可用
  • debug 允许 assets 加载失败时使用默认资源(如默认字体),提高健壮性
  • 优化 debug 线程安全,确保 debug 读取数据时不会引发 race condition

优化后的 debug 代码将更加稳定,不会因为 assets 加载顺序问题而崩溃,同时仍然支持未来的 texture 预览功能。

实现DEBUGStart和DEBUGEnd

测试。实时代码编辑和调试服务现在可以协同工作

  1. 字体和调试系统正常工作

    • 调试系统中的字体和相关功能已经正常工作,没有出现任何问题。
    • 确保调试功能与实时代码编辑(live code editing)协同工作,不会冲突。
  2. 实时代码编辑功能

    • 通过重新编译,确保实时代码编辑功能可以正常工作。
    • 这种方式使得调试系统和实时代码编辑能够和谐地运行在一起,避免了任何不必要的干扰或问题。
  3. 简化和优化

    • 实时代码编辑和调试服务现在已经没有问题,可以继续进行开发工作,不必再担心调试和代码编辑之间的冲突。
    • 对于当前的实现,可以继续以现有的方式进行开发,保持系统的稳定性和一致性。
  4. UI方面的改进

    • 接下来计划将重点转向UI部分,整理并改善UI的实现,使其更加清晰、直观。
    • 尝试简化UI设计和流程,以确保在整体开发过程中UI的表现更加流畅和易用。

总的来说,目前的进展非常顺利,调试系统和实时代码编辑功能已经协同工作,并且下一步将继续关注UI的改进,确保整体开发进程顺利推进。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
long long sum = 0;
vector<int> dp;
int fun(int n) {
    sum++;
    if (n < 3) return n;
    if (dp[n] != -1) return dp[n];
    dp[n] = 1;
    return fun(n - 1) + fun(n - 2);
}
void get(int n) {
    sum = 0;
    fun(n);
    cout << "n= " << n << "sum=" << sum << "\n";
}
int main() {
    dp.resize(20000, -1);
    get(5000);
    get(8000);
    get(10000);
}

递归调用层数太多会栈溢出

可以指定栈大小

cpp 复制代码
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:4194304,1048576")

通过改变正交投影来调整性能视图的大小

我们正在调整 Profile View(分析视图) 的尺寸,使其可以被缩放或调整大小。具体做法是创建一个 Rectangle(矩形) 作为 Profile Rect(分析矩形) ,并确保其中的 Profiler(分析器) 相关内容可以正确绘制。

Debug(调试) 过程中,我们发现代码中还残留了一些以前的代码,但其中可能仍然有用的部分,所以暂时保留。当前的渲染逻辑涉及一系列缩放操作,并且在绘制时应用了 Orthographic Transform(正交变换)。理论上,我们可以在代码的中间部分插入这个变换,使其能够动态调整。

具体操作步骤如下:

  1. 在绘制 Profile View 之前,推入一个 Orthographic Transform
  2. 先采用现有的变换方式,以确保功能保持不变。
  3. 代码中涉及 ro32 转换为 int32,Visual Studio 的错误提示不够清晰,需要手动检查是哪个参数导致了类型转换问题。
  4. 发现当前的 Orthographic Transform 变换计算中包含 Pixel Width(像素宽度)Pixel Height(像素高度)Meters to Pixels(米到像素的转换比例),缩放比例已被预先计算进来,因此不需要额外调整,只需修改缩放参数即可。
  5. 通过调整变换参数,使 Profile View 的高度减少一半,验证渲染结果正确。
  6. 进一步测试缩小比例,发现 Profile View 能够按照预期缩放,说明渲染管线中所有内容都会通过变换调整,不会影响其他部分的渲染。

当前问题:虽然 Profile View 能够缩放,但仍需考虑如何更精确地控制其大小,避免影响其他 UI 组件。

发现长度会不同增大感觉有点问题

排除一下

鼠标指针位置也应被转换

当前实现中,鼠标位置 并没有被转换到 变换空间(Transform Space) 中,而是直接使用原始鼠标坐标来进行 Hit Testing(命中检测),这显然是不合理的。为了让鼠标交互正确,我们需要计算鼠标在变换空间中的位置,否则交互检测会出现偏差。但目前的重点仍然是其他部分的调整,因此暂时不处理这个问题,只是需要注意后续修正。

变换调整

当前的实现允许使用 Orthographic Transform(正交变换) 来修改 Profile View(性能视图) 的形状。

  • 形状变换:可以利用正交变换调整其比例,例如缩放大小,使其更符合需求。
  • 保持形状不变:也可以选择不调整形状,仅进行位移或其他变换。

目前的重点是确保 Profile View 能够正确缩放,并观察变换对其外观的影响,而不涉及鼠标交互问题。

控制性能矩形的放置

我们正在调整 Profile View(性能视图)位置尺寸,并确保它能够正确缩放和移动到指定位置。

位置控制

当前的实现允许我们独立控制 Profile View形状屏幕上的位置。具体方法如下:

  1. 使用 ProfileRect 作为数据来源

    • ProfileRect 预定义了一个矩形区域,我们可以直接从这个矩形中获取 min_xmax_x 来确定 Profile View 的边界。
    • 例如,矩形可以定义为 (10, 10, 200, 200),表示一个 200×200 的区域。
  2. 计算 chart_topchart_right

    • chart_topProfileRectmax_y,因为屏幕坐标系是从 上到下 递增的。
    • chart_leftProfileRectmin_x,表示左边界。
  3. 处理坐标中心问题

    • 坐标计算过程中,需要注意有些系统使用 中心点作为参考 ,因此 50,50 的坐标可能代表 从中心点向右下偏移 50 而不是绝对位置。
    • 这样一来,计算 ProfileRect 位置时需要额外注意坐标的基准点。

缩放控制

  1. 调整 Profile View 的宽度和高度

    • 通过 ProfileRectget_width() 计算矩形宽度,并使用它来缩放 Profile View
    • 由于 chart 内部已经包含缩放逻辑,因此可以直接使用 ProfileRect 的宽高来调整比例。
  2. 修正 width 计算错误

    • 代码中 rect_min_max 计算的宽度出现问题,导致 width 实际上比预期值小 50 像素,正确做法应该是 max_x - min_x
    • 例如,如果 ProfileRect 设为 (50, 50, 250, 250),那么 width = 250 - 50 = 200

下一步调整

  • 确保 Profile View 的宽高比例正确,不受其他缩放因素影响。
  • 优化绘制逻辑 ,让 Profile View 能够更直观地调整大小并适应屏幕布局。
  • 后续处理鼠标交互问题 ,目前鼠标仍然使用 未变换坐标 进行检测,需要转换到 变换空间 才能正确响应点击和拖动操作。

调整lane高度

在当前实现中,我们正在调整 Profile View(性能分析视图) 的高度计算方式,以确保绘制的 时间帧(Frames)数据通道(Lanes) 能够正确适配视图大小。

1. 计算 Lane(通道)高度

为了合理分配 Profile View每个 Lane 的高度 ,我们采用 回推计算(Back-Solving) 方法:

  1. 获取最大时间帧数(MaxFrameCount)通道总数(LaneCount)
  2. 确保 MaxFrameCount 大于 0,以避免除零错误。
  3. 初步计算 Lane 高度
    • 假设 LaneHeight0,然后基于 MaxFrameCountLaneCount 计算其实际值。
    • 计算方式:LaneHeight = ((GetDim(DebugState->ProfileRect).y / MaxFrameCount) - BarSpacing) / (real32)LaneCount;

2. 处理间距(Spacing)

在绘制 Profile View 时,每个数据条(Bar)之间存在固定间距(Spacing),需要在计算高度时考虑:

  1. 定义 BarSpacing(条形间距) ,例如 4.0 像素。
  2. 计算总占用空间
    • BarSpacing 影响 LaneHeight,必须在计算时扣除。
    • 由于首个 Lane 上方没有间距,而其他 Lane 之间都有,因此可能会有 Fence-Post 问题(即边界计算误差)。

3. 计算最终 LaneHeight

  1. 获取 Profile View 可用的总高度
    • 通过 GetDim(DebugState->ProfileRect).y 获取 Profile View 的整体高度
  2. 计算每个 Lane 实际能分配的高度
    • 先计算 理论 Lane 块高度
      • BlockHeight = GetDim(DebugState->ProfileRect).y ÷ MaxFrameCount
    • 扣除 BarSpacing 后,计算最终 Lane 高度
      • LaneHeight = (Block Height - Total BarSpacing) ÷ LaneCount
    • 这样可以确保所有 Lane 在 Profile View 内正确排列,而不会出现溢出或过度挤压。
      LaneHeight = ((GetDim(DebugState->ProfileRect).y / MaxFrameCount) - BarSpacing) / (real32)LaneCount;

4. 下一步优化

  • 优化 Debug 输出 ,确认计算出的 LaneHeight 是否符合预期。
  • 提供移动 Profile View 的测试方法,确保调整位置后仍能正确显示数据。
  • 处理边界误差(Fence-Post 问题) ,确保高度计算精确无误,避免条形元素错位或超出范围。

显示性能矩形,以便我们可以调试代码,检查它如何缩放和定位

在当前实现中,我们尝试在 Profile View(性能分析视图) 内绘制一个矩形,并进行可视化调试,同时发现了一些与渲染逻辑相关的问题。

1. 绘制矩形框架

为了在 Profile View 内绘制一个 矩形边框(Rect Outline) 进行可视化,我们采取了以下步骤:

  1. 调用 push_rect

    • 传入 渲染组(Render Group)
    • 传入 矩形坐标(Rectangle Min/Max),定义矩形的范围。
    • 传入 Z 轴深度(Z-Depth),确保正确渲染层级。
    • 传入 颜色(Color) ,这里选择了 黑色半透明遮罩 以便突出显示矩形区域。
  2. 确认矩形绘制范围是否正确

    • 由于当前矩形范围由 min/max 计算得出,我们需要验证其值是否符合预期。
    • 发现当前矩形大小 看起来有些问题 ,但检查后确认是符合预期的(例如 50 像素宽度的矩形确实较小)。

2. 处理越界绘制问题

目前绘制的矩形 允许超出预定义的区域,原因是:

  1. 当前渲染逻辑不会限制绘制内容 ,如果某个元素超出了 Profile View 的区域,它仍然会被绘制。
  2. 这种行为 可能是有意的,因为希望看到超过时间范围的部分,而不是直接裁剪掉数据。
  3. 未来可以通过调整 裁剪逻辑增加视觉美化 让绘制结果更整洁。

3. 发现 Frame Recalculation(帧重计算) 的问题

Profile View 内部进行调试时,发现了一个有趣的现象:

  1. 点击鼠标左键时,会触发一次 刷新计算(Recalculation)

    • 整个时间帧(Frame)数据会被重新计算
    • 由于计算量较大,导致 渲染帧出现短暂的时间推迟
    • 可以看到 渲染数据出现大幅波动 ,甚至会有 一个完整的渲染帧延迟
  2. 最初以为是 Bug,但实际上是预期行为

    • 原因在于 Profile View 计算机制
      • 每当 刷新计算 发生,就会重新整理所有帧数据。
      • 这导致渲染时 时间戳 发生了偏移,表现出 刷新时帧率骤增 的现象。

4. 解决当前高度计算问题

目前绘制的矩形 高度不足,推测可能有几个原因:

  1. 计算矩形高度时,未正确考虑坐标系统 (可能与 y 轴方向有关)。
  2. 缺少高度缩放因子,导致矩形未能完全填充预期区域。
  3. 可能遗漏了 Profile View 高度的计算方式 ,需要修正 y 方向的变换逻辑。

5. 计划的优化方向

  • 修正矩形绘制的高度问题 ,确保 Profile View 能正确填充数据。
  • 优化 Frame Recalculation 机制,避免鼠标点击导致突发计算延迟。
  • 调整 Profile View 让超出区域的绘制更加美观,可能通过遮罩或渐变处理。
  • 添加调试功能 ,更直观地看到时间帧计算如何影响 Profile View 的显示结果。

修正lane高度

Profile View(性能分析视图) 的实现中,我们正在调整 最大帧数(max frame) 的数学计算,确保绘制时各个元素正确地匹配比例,避免渲染错误。

1. 计算帧宽度与间距

我们首先确定 最大帧数(max frame) ,然后计算 每帧所占像素宽度(PixelsPerFrame)

  1. 获取 Profile View 允许的 总宽度
  2. 确定 最大帧数 ,然后计算:
    PixelsPerFrame = 总宽度 最大帧数 \text{PixelsPerFrame} = \frac{\text{总宽度}}{\text{最大帧数}} PixelsPerFrame=最大帧数总宽度
  3. 考虑帧之间的间距(BarSpacing) ,我们需要从 PixelsPerFrame 中减去 BarSpacing,得到实际可用的帧宽度:
    实际帧宽度 = PixelsPerFrame − BarSpacing \text{实际帧宽度} = \text{PixelsPerFrame} - \text{BarSpacing} 实际帧宽度=PixelsPerFrame−BarSpacing
  4. 计算 每条 Lane(图表中的数据行) 的高度:
    LaneHeight = 实际帧宽度 Lane 数量 \text{LaneHeight} = \frac{\text{实际帧宽度}}{\text{Lane 数量}} LaneHeight=Lane 数量实际帧宽度

2. 确保计算出的值合理

为了确保计算值的合理性,我们进行了一系列调试和验证:

  • 计算 Profile View 可用的高度(应为 150 像素)。
  • max frame = 10,所以 PixelsPerFrame = 15 像素。
  • BarSpacing 被正确扣除,使得 帧实际可用宽度为 11 像素
  • Lane 高度计算为 1.2 像素(根据 9 条 Lane 分配)。
  • 计算出的 chart height 总值为 150,符合预期。

3. 发现绘制问题

  • 计算本身是正确的,但在绘制时,我们发现 渲染出的数据没有正确应用新计算的尺寸
  • 问题出在 BarSpacing 计算的应用上,它 没有被正确传播到绘制逻辑,导致帧高度计算错误。
  • 通过修正 bars + spacing 计算的传播,确保渲染逻辑使用正确的数据。

4. 解决方案

  1. 修正 BarSpacing 的计算应用 ,确保 Profile View 的绘制逻辑正确匹配计算出的尺寸。
  2. 验证 LaneHeightFrame Width 在不同数据集下是否一致,保证适配各种情况。
  3. 最终调整可视化窗口,使数据呈现在正确的范围内,并检查 UI 视觉效果是否符合预期。

5. 结果与优化方向

  • 绘制窗口内的帧数据现在正确可视化 ,并且能够适应不同的 max frame 设置。
  • 未来可以优化:
    • 动态调整 BarSpacing 以适应不同的分辨率,确保所有数据都清晰可见。
    • 添加 UI 反馈 ,当 Profile View 发生帧刷新时,可以高亮变化区域,帮助调试。
    • 提高性能,减少不必要的计算,优化绘制效率。

我知道有更强大的方式来实现这些功能,但使用调试数据来驱动一些游戏的视觉效果,例如火焰或更可能的某种异世界魔法实体,或者显示在游戏内电脑上,这样做可行吗?也许它可以成为一种彩蛋,只有知道游戏制作过程的人才能发现

在实现调试数据的过程中,探讨了一种有趣的可能性,即 利用调试数据来驱动游戏中的视觉效果 。例如,可以用于火焰效果超自然的魔法实体 ,甚至游戏内的计算机屏幕显示。这种方法可以作为一种彩蛋,让熟悉游戏开发过程的玩家发现并互动。

1. 可行性分析

尽管这个想法很有趣,但存在一些 实际的限制

  1. 内存占用大

    • 调试数据通常需要记录大量的游戏状态信息,这可能会占用较多的内存空间。
    • 在正式版本中,这些数据通常会被编译移除,以减少运行时开销。
  2. 计算成本高

    • 采集和处理调试数据需要额外的 CPU 周期,可能影响游戏的运行效率
    • 特别是在性能要求高的场景(如复杂的物理模拟、大量 NPC 交互等),使用调试数据驱动视觉效果可能会导致帧率下降
  3. 游戏发布后的问题

    • 在最终的游戏发布版本中,调试数据一般不会保留,因此如果依赖调试数据 来实现某些视觉效果,可能需要额外的保留机制
    • 这会影响代码结构,使游戏维护变得更加复杂。

2. 可能的解决方案

尽管存在这些问题,仍然可以采取一些优化方法,让调试数据在游戏中发挥作用:

  1. 选择性保留调试数据

    • 仅保留 一部分 关键调试信息,而非完整的调试日志,以减少内存占用。
    • 例如,可以存储某些特定变量(如 CPU 使用率、帧率波动等)并用作游戏中的某种视觉特效参数。
  2. 提供可选的 "开发者模式"

    • 游戏可以设计 "开发者模式"(Debug Mode),让玩家在特定情况下启用调试数据驱动的视觉效果。
    • 例如,可以通过 游戏内的秘密代码设置菜单 开启调试数据可视化功能。
    • 这种模式既能让游戏保持良好的性能,又能让对开发感兴趣的玩家探索更多细节。
  3. 将调试数据作为彩蛋使用

    • 非关键场景 采用调试数据进行视觉渲染。例如:
      • 游戏终端 :在游戏中的计算机屏幕上显示一些调试数据,作为世界观设定的一部分
      • 魔法效果:某些特效(如闪烁的光效、传送门)可能基于调试数据产生轻微的动态变化,但不影响核心游戏玩法。
    • 这种方式可以 提升游戏的沉浸感,让玩家在不影响性能的情况下体验这些隐藏细节。

3. 教育意义与开发价值

  • 由于这个项目本身带有 教育意义,开放部分调试功能可以鼓励玩家了解游戏开发的底层逻辑。
  • 可以考虑提供 开发工具MOD 支持,让玩家能够访问某些调试功能,并尝试修改或扩展游戏。
  • 通过合适的方式开放调试信息,既能提高游戏的可玩性 ,又能作为教学示例,让更多人了解游戏技术的运作方式。

4. 结论

是否使用调试数据驱动游戏内特效,主要取决于性能影响和设计需求

  1. 如果目标是优化性能,那么应当避免使用调试数据,并在正式版中移除所有相关代码。
  2. 如果目标是增加趣味性 ,可以在 开发者模式游戏彩蛋 形式下,适当保留调试数据作为特殊视觉效果的一部分。
  3. 如果目标是教育和开发者学习 ,则可以提供某种 Debug 可视化模式,让玩家了解游戏运行机制,同时不会影响主游戏体验。

这种设计方案可以让游戏在 性能、趣味性和开发价值 之间找到一个平衡点,使调试数据不仅仅是开发工具,还能成为游戏的一部分。

调试图表中有些品红色的尖峰发生得太快,我们甚至没有时间点击它们。我们打算如何捕捉这些呢?我觉得能有逐帧调试功能会很有用。另外一个想法是将调试文本保留在我们最后悬停过的位置

在调试图表中,洋红色的尖峰 可能出现得非常快,以至于无法及时点击查看,因此我们需要有效的方法来捕捉这些瞬时事件。为此,提出了几种解决方案:

1. 逐帧查看调试数据

  • 具有 逐帧回溯(Step Frame) 的功能将会很有用,这样我们可以手动检查每一帧,捕捉异常情况。
  • 这样可以确保我们不会错过那些发生得极快的尖峰,即使它们在屏幕上仅短暂出现。

2. 调试文本的优化

  • 另一种方法是 让调试文本停留在我们最后悬停的重复项上,这样即使尖峰消失,我们仍然可以看到对应的数据。
  • 这样可以减少因数据更新过快而导致的阅读困难,使我们有足够的时间分析异常情况。

3. 调试 UI 交互优化

  • 暂停功能:可以随时暂停调试界面,防止数据继续滚动,以便进行详细分析。
  • 历史数据回溯 :虽然界面上可能只显示几帧的数据,但实际上我们缓存了 64 帧的数据,可以回溯到先前的尖峰位置进行查看。
  • 拖动查看历史数据:在调试 UI 中,我们可以通过拖动时间轴回溯到特定的帧,并手动检查尖峰的具体情况。
  • 存储更长的历史记录 :在高性能计算机(如 拥有 32GB 内存的开发机 )上,可以分配大量内存(如 24GB )用于存储调试数据,允许回溯更长时间的帧数据,甚至可以存储整个游戏运行过程中的所有事件

4. 结论

  • 短时间的尖峰问题可以通过增加缓存和回溯机制解决,即使它们快速消失,我们仍然可以查看历史记录。
  • 提供暂停、逐帧回放和时间轴拖动功能,能极大提升调试数据的可用性,使分析更加直观。
  • 在高性能设备上,可以存储更长的调试历史数据,甚至可以回溯整个游戏过程,确保任何异常情况都能被捕捉和分析。

这样,我们可以确保即使是极快的调试尖峰,也不会被错过,从而提高调试和性能分析的效率。

相关推荐
学习是种信仰啊16 分钟前
QT文件操作(QT实操学习3)
开发语言·qt·学习
牵牛老人19 分钟前
C++设计模式-迭代器模式:从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析
c++·设计模式·迭代器模式
卷卷的小趴菜学编程25 分钟前
算法篇-------------双指针法
c语言·开发语言·c++·vscode·算法·leetcode·双指针法
钱彬 (Qian Bin)27 分钟前
QT Quick(C++)跨平台应用程序项目实战教程 5 — 界面设计
c++·qt·教程·音乐播放器·qml·qt quick
飞鼠_1 小时前
详解数据结构之树、二叉树、二叉搜索树详解 C++实现
开发语言·数据结构·c++
Allen_LVyingbo1 小时前
文章配图新纪元:OpenAI新推出的GPT-4o原生图像生成功能启示
人工智能·学习·架构·数据分析·健康医疗
傍晚冰川1 小时前
【STM32】最后一刷-江科大Flash闪存-学习笔记
笔记·科技·stm32·单片机·嵌入式硬件·学习·实时音视频
吴梓穆1 小时前
UE5学习笔记 FPS游戏制作33 游戏保存
笔记·学习·ue5
ElseWhereR1 小时前
困于环中的机器人
c++·算法·leetcode
淬渊阁1 小时前
汇编学习之《数据传输指令》
汇编·学习