当前位置: 首页 > news >正文

游戏引擎学习第214天

总结并为当天的任务做好准备

昨天,我们将所有调试控制代码迁移到使用新的调试接口中,但我们没有机会实际启用这些代码。我们做了很多准备工作,比如规划、将其做成宏、并将其放入调试流中,但实际上我们还没有办法进行测试。

今天,我们将实现这些功能。这样做的一个好处是,今后我们不再需要经过编译周期来切换调试开关。调试开关将可以实时切换,这样非常方便。因为我们发现,尽管编译周期只需要几秒钟,但每次编译都稍显冗长。我们希望能够做到几乎是瞬时响应,大约只需一帧的延迟,而不是当使用 MSVC 时,可能要等待一百二十帧的延迟。

我们之前的工作已经完成,我们的调试代码现在已经能够正确编译,并执行它们应该做的操作。但接下来,我们要开始让这些调试信息出现在屏幕上。为了做到这一点,我们显然需要一些方法,将这些调试信息真正接入到系统中。

到目前为止,我们知道的是,如果查看 game_debug.h,我们已经完成了相关工作。此外,game_debug_interface.h 里也已经做好了调试代码的规划,但还需要进一步的操作才能真正启用这些功能。

game_debug_interface.h: 开启 DEBUG_IF 并继续实现

现在的任务是解决调试变量的初始化问题。我们之前已经做了一个操作,用来关闭这些变量的初始化,但我们需要确保它们能够正常显示在调试层次结构中。具体来说,当这些调试开关被打开时,它们会调用 initialize_debug_value 函数。我们目前的工作是找出如何让这些调用能够正常运作,并且在调试层次结构中显示出来。

从目前的情况来看,存在一个问题。在进行路径拼接的时候,似乎还是在使用不应该用的 debug_variable 变量,而应该用 path 变量。需要修改这个问题,确保在需要的时候使用正确的变量类型。具体来说,路径拼接的地方应该使用字符串(string),而不是错误地使用路径类型。

在修正这些问题后,接下来的步骤是让 initialize_debug_value 正确地集成到调试系统中。我们已经完成了编译和一些功能实现,但还需要进一步将这些功能整合进系统,以确保它们能够正确显示并进行调试操作。因此,现在的重点是确保这些变量和函数能够正确工作并显示在调试层次中。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_debug.cpp: 引入 DEBUGInitializeValue

现在,我们需要确保调试变量的初始化能够清晰明了地与调试系统对接。为了实现这一点,我们决定给变量初始化函数加上 debug 前缀,像 debug_initialize_value 这样。这样做的目的是让代码中出现的任何调试值都能够明确地标识出来,避免混淆,并且清楚地表明这些值是与调试系统相关的。

接下来,我们的工作是将这个调试初始化值的机制与系统进行连接,将它们集成到整个调试框架中。这意味着我们需要找到一个合适的方式将调试值和系统中的其他部分进行挂钩,使得这些调试变量能够在需要时正确地被调用和显示。

在这里插入图片描述

在这里插入图片描述

考虑如何将其集成到系统中

现在,系统的实现并不会特别困难,主要的问题是我们需要创建一个调试层级(debug hierarchy)。这可以是一个静态的层级,因为这些调试值在程序运行过程中基本是保持一致的,只有在程序重新加载时才会丢失静态值。因此,我们需要特别注意当程序进行动态代码重载时,如何处理这些静态值的丢失。

为了使调试系统在动态加载的情况下依然能够正常工作,调试系统必须能够处理程序重新加载后丢失的指针和数据。如果没有这个机制,重新加载后的数据指针将不再有效。因此,在调试系统中,我们需要确保能够检测到这些调试变量的丢失,并处理这种情况,比如通过刷新调试值。

此外,如果想要支持动态代码重载并保持调试值的持久性,那么我们可能需要设计一种方法,将调试变量的初始值保留到下次程序启动时。这可以通过在 config.h 文件中存储这些全局常量实现。具体来说,初始化调试值时,可以将这些初始值传入,使得当程序重新启动时,调试系统能够恢复这些值。

这个过程可能需要一定的调整,但基本的想法是,当调试变量被初始化时,应该能传入初始值,这样在程序重新启动后,我们就可以利用这些初始值,确保调试系统能够恢复到正确的状态。

game_debug.cpp: 为不同类型创建多种 DEBUGInitializeValue

为了确保调试值在程序重启后能够持久化,系统需要一种方法来获取这些值的初始状态,并将其保留至下次运行。这是通过 config 文件来实现的,确保在程序重新启动时能够加载正确的调试值。

为了实现这一点,首先需要确保系统可以处理不同类型的值。因此,需要为每种调试值类型提供不同的实现方法。对于这些调试变量,可以使用各种类型,如 bools32 等,但因为调试系统没有办法自动区分这些类型(例如,bool 类型和 s32 类型会被认为是相同的),所以必须为每种类型提供专门的初始化方法。这意味着,对于 bool 类型,可能需要提供一个专门的初始化函数,比如 debug_initialize_value_bool,而其他类型如 s32 可以使用一般的初始化方法。

核心思想是,调试系统应该能够自动将这些值初始化为它们的默认状态,并确保它们的持久性。每次调用 debug_initialize_value 时,都应该传入一个基本的配置,这样系统就能确保初始化过程在调用时自动完成,并且可以根据传入的值正确设置调试变量。

总的来说,这一过程的目的是减少调用方的负担,自动化调试变量的初始化过程,使得调试系统更加高效和灵活。在实际应用中,这意味着调试变量的初始化和更新可以通过简单的接口自动完成,从而避免开发人员手动管理调试值的设置和更新。

最终,调试系统将能处理各种不同类型的调试值,并确保它们的值能够在程序重启后恢复,使得调试体验更加流畅和稳定。
在这里插入图片描述

在这里插入图片描述

game_debug_interface.cpp: 尝试使用序列操作符设置 DebugValue##Path

在实现调试初始化值的过程中,探索了是否可以通过使用逗号运算符(comma operator)来简化代码结构,从而避免一些冗长的操作。逗号运算符允许在一行代码中执行多个表达式,最终返回最后一个表达式的值。在这个场景中,目的是利用逗号运算符来嵌套执行多个操作,特别是在赋值操作中。

具体地,计划通过逗号运算符在调试初始化值的过程中进行赋值操作,这样可以避免多次重复的代码。然而,这样的做法可能会遇到一些编译器错误或语法问题,尤其是在初始化器中使用逗号运算符时,可能会引发一些编译问题。错误信息表明,编译器无法正确解析使用逗号运算符的初始化器,可能是因为在此上下文中不支持逗号运算符。

为了解决这个问题,尝试了多种方法,比如调整运算符的顺序或在参数列表中使用逗号运算符,但似乎都未能成功。最终,发现将逗号运算符的使用放入表达式中并使其工作,成功地避免了传统的冗长代码结构。

整个过程可以视为一种尝试通过非常规的编码方式来提高调试代码的简洁性,尽管这种方式在实践中可能不太常见或不推荐,但这种探索的思路体现了通过不拘泥于传统方法来优化开发流程的尝试。

总的来说,尽管遇到了一些编译问题,但最终成功通过逗号运算符实现了期望的功能,从而达到了简化代码的目标。

在这里插入图片描述

使用序列操作符初始化静态变量的方式

在这个场景中,尝试实现的是一次性初始化一个静态变量,并确保这个初始化操作只发生一次。为了实现这一点,使用了序列操作符(comma operator)来在初始化静态变量时,先执行某些操作(比如一个函数调用),然后再返回需要的值作为静态变量的初始化值。

具体过程:

  1. 静态初始化:首先需要确保静态变量只在程序的生命周期内初始化一次。静态变量通常会在第一次使用时被初始化,而此初始化操作需要在代码的右侧进行,以确保只执行一次。

  2. 使用序列操作符:为了在静态初始化时执行一个特定的函数调用,可以使用序列操作符(,)。该操作符允许在一个表达式中执行多个操作,确保在获取静态变量的初始值之前先执行某些操作。例如,可以在静态变量的初始化表达式中,先执行一个函数调用,然后再返回静态变量的初始化值。

    序列操作符的语法是:先执行左侧的操作,再执行右侧的操作,且整个表达式的值为右侧操作的结果。这种方式可以让函数调用与静态初始化同时发生。

  3. 实现中的“奇怪行为”:在实际执行时,可能会遇到一些编译器不接受的情况,比如语法错误、预期外的行为等。但这里采取了“先做再说”的策略,即不太理会编译器的警告或错误,直接尝试执行,尽管这样做有一定的风险。

  4. 重复执行:为了确保方法的可靠性和行为的符合预期,可能会在代码中尝试重复执行相同的操作,确保所需的行为能够稳定发生,虽然这种做法比较“大胆”,甚至有点“脏”或不规范。

总结:

在设计过程中,使用序列操作符(,)可以在静态变量初始化时执行多个操作,保证初始化时的行为顺序。但需要注意,这种做法虽然在某些情况下有效,但也可能带来潜在的语法和逻辑问题,特别是在复杂的代码中可能导致不可预见的行为。因此,建议在尝试此类方案时,谨慎检查可能的副作用,确保代码的稳定性和可维护性。

game_debug_interface.cpp: 使用序列操作符设置 DebugValue##Variable

在这个过程中,尝试通过使用序列操作符(,)来减少代码重复并简化变量初始化的方式。以下是实现的具体步骤和思路:

具体操作:

  1. 目的:减少冗余代码
    通过使用序列操作符,可以一次性处理多个变量的初始化,避免手动复制粘贴相同的代码。这将使代码更加简洁且易于维护。所做的操作是:在初始化静态变量时,先执行一些必要的设置(比如赋值),然后用序列操作符来连接不同的操作。

  2. 序列操作符的应用:
    序列操作符允许将多个表达式串联在一起,并且返回最后一个表达式的结果。这样,通过使用序列操作符,可以在一次赋值中完成多个操作,而不需要在代码中重复写类似的函数或赋值语句。每次执行时,操作符会依次执行这些表达式,最终返回最后一个表达式的结果。

    比如,在赋值操作中,可以先执行某些操作(如计算或函数调用),然后将结果赋值给目标变量。通过这种方式,可以省去手动重复编写赋值和操作的代码。

  3. 效果:减少重复代码
    通过这种方法,不仅避免了冗余的函数重复,还让代码更加简洁。所有的类型初始化和赋值操作都通过自动化的方式完成了,节省了大量的手动输入。

  4. 序列操作符的便利性:
    序列操作符的一个重要优势是,它允许将多个不相关的表达式放在同一行中,执行时不影响前面的操作,仅返回最后一个表达式的结果。这在某些情况下非常有用,尤其是当需要在一行代码中执行多个独立操作时。

  5. 实践中的风险:
    尽管这种方法能够大大简化代码,但它也可能带来一些潜在的风险。因为使用序列操作符时,表达式的执行顺序可能会影响程序的行为,特别是在较为复杂的程序中,容易引发难以察觉的错误。

总结:

通过使用序列操作符,可以有效地简化代码,减少冗余,避免重复编写相同的函数或赋值语句。它特别适合用来处理多个赋值或初始化操作,可以在不增加额外函数的情况下,简洁地完成这些任务。然而,需要注意的是,这种做法虽然能提高效率,但也可能带来一些潜在的风险,因此在使用时要小心,确保不会影响程序的正确性。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_debug.cpp: 编写 DEBUGInitializeValueα

在这个过程中,主要的目标是让 debug initialize value 正常工作,并且确保它能够接收和处理一些具体的调试信息,例如文件名、行号等。这是为了确保在游戏的调试接口中能够正确地记录和追踪调试信息。

具体步骤和处理方法:

  1. 定义调试事件的结构:
    在调试接口中,需要定义一个调试事件(debug event),该事件包含了必要的调试信息。具体来说,这些信息包括:

    • 时钟值(Clock Value): 初始化为0。
    • 文件名(File Name): 即当前文件的名字,记录调试信息来源。
    • 行号(Line Number): 调试事件发生的代码行号。
    • 块名(Block Name): 传入的块名称,用来标识调试的代码块。
    • 线程ID(Thread ID): 可能需要提供线程ID,尽管在某些情况下可能不需要。
  2. 传递文件名和行号:
    在调试代码中,为了确保每个调试事件都有足够的信息来定位源代码,文件名和行号是必要的。代码会期望在初始化时接收这两个参数。这些参数会在调试事件中被传递,并且在事件记录时提供准确的上下文。

  3. 调试接口的实现:
    在调试接口的实现中,调用 debug initialize value 时,可以要求传入文件名和行号。这样,调试系统在初始化时就能获得这些信息,从而能够记录下事件发生的位置和上下文。

  4. 可能的其他信息:

    • 线程ID: 尽管线程ID并非每次都必须,但在多线程环境下,它可能有助于追踪特定线程上的问题。如果需要,系统可以通过获取当前线程ID的方式来实现。
    • 核心索引(Core Index): 在某些情况下,核心索引可能不是很相关,因此可能不需要在这个过程中处理。
  5. 调试事件的初始化:
    通过要求传入文件名和行号,可以确保每次调试事件都能够记录正确的调试上下文。调试接口的实现通过接收这两个值,并根据这些信息初始化调试事件,从而确保后续调试操作能够追溯到正确的代码位置。

总结:

通过调整 debug initialize value 使其能够接收文件名和行号作为参数,可以有效地增强调试信息的准确性。这样每个调试事件都能够附带完整的上下文信息,帮助开发者更容易地定位和修复问题。此外,还可以根据需求选择是否包括线程ID等其他调试信息,进一步增强调试的能力。
在这里插入图片描述

在这里插入图片描述

game_debug.cpp: 考虑将 CollateAddVariableToGroup、AcquireMutex 和 ReleaseMutex 添加到 DEBUGInitializeValue 以便间接操作这些类型

我们正在调试系统中扩展对变量类型的支持,希望能以间接的方式操作这些变量,具体来说,是通过指针的方式关联到这些调试事件。为了实现这一点,我们计划将变量以事件的形式添加到调试系统的事件组中。

具体实现步骤和设计逻辑如下:


1. 将事件作为变量加入调试组

我们希望将这些初始化的调试变量加入某个调试组(Group)中,就像系统之前处理其他调试信息那样。我们已有的调试接口中,通过 open_data_blockclose_data_block 这对调用完成数据块的组织。因此,我们的目标是在这些数据块组织逻辑中,添加支持将变量事件记录进去。


2. 通过 debug_state 添加变量事件

具体做法是利用现有的 debug_state 结构,提供接口向其中添加变量。我们希望能够调用某个函数,将一个变量信息推入 debug_state 中。这样,我们就可以利用已有的框架,把调试变量像其它事件一样存储下来。


3. 线程安全问题与同步处理

目前我们遇到一个潜在的问题:添加变量事件的操作可能来自不同线程。如果不处理线程同步,可能会造成数据竞争或错误写入。我们当前的代码不是无锁的,它假设只有一个线程会修改这些指针数据。因此,为了支持多线程下的安全操作:

  • 一种方法是添加互斥锁(mutex),在写入共享的组结构时加锁。
  • 另一种更高效的方式是避免实时修改共享结构,而是将写入请求封装为事件,推送到事件队列中,延迟处理。

4. 采用异步事件推送方式

经过考虑,我们决定采用第二种方式更为合适。我们引入一个新的调试事件类型:记录永久变量(permanent variable),它并不会立刻写入结构中,而是记录下来,之后在系统合适的阶段统一处理。

这种设计的好处包括:

  • 不需要加锁,规避了线程同步带来的复杂性。
  • 不需要在变量创建时初始化整个调试系统,降低耦合度。
  • 保留变量信息持久化的能力,提升调试追踪的完整性。

5. 后续整理与收敛

这种事件推送方式还带来了一个额外好处:我们可以在调试系统运行的某个阶段集中处理这些变量信息。例如在每帧末尾,将所有等待处理的变量事件统一归档或显示。这不仅更高效,还更符合调试系统的逻辑分层。


总结:

我们通过扩展调试系统的事件机制,允许变量作为事件添加到调试信息中,同时避免线程安全问题。最终选择异步方式处理这些变量事件,规避了加锁问题,也简化了调试系统初始化逻辑。这样既保证了调试信息的完整性,也提升了系统的鲁棒性与可维护性。
在这里插入图片描述

"一种更好的方式"β

我们认识到当前的实现方式虽然能工作,但存在一些不足,因此决定采用更好的方式来优化整体逻辑和结构。


当前状况回顾:

我们之前为了在调试系统中记录变量信息,尝试将变量以事件的形式添加到调试状态结构中。最初的实现方式是直接修改调试组结构或状态,这在多线程环境下存在潜在风险,需要加锁处理。


存在的问题:

  1. 线程安全问题:多个线程可能同时向调试组写入数据,直接修改会产生竞争条件。
  2. 初始化依赖:直接修改结构可能要求调试系统已经初始化,否则会引发非法访问。
  3. 逻辑侵入性强:这种方式将调试代码深度嵌入业务逻辑中,可维护性较差。

更优方式的思路:

我们提出一个更合理的处理方式:通过事件队列的方式异步记录变量信息,将变量状态封装为一个独立事件,在调试系统内部统一收敛处理。


在这里插入图片描述

game_debug_interface.h: 编写一个新的线程安全的 DEBUGInitializeValue

我们现在要做的,就是将原本用于初始化调试变量的操作,转变为通过事件流的方式进行处理。整个改造主要集中在 game debug interface 这一部分。


改动逻辑与设计意图:

  • 保留现有变量定义逻辑不变:原先在调试接口中处理持久变量(persistent variable)的一些结构代码可以保留,不需要太多修改。

  • 改变核心行为:初始化变为事件记录
    核心改变在于 debug_initialize_value 这一函数或宏,改为在被调用时,不直接写入结构体或状态,而是通过一个 record_debug_event 的调用,将初始化事件推入调试事件流中。


操作细节:

  • debug_initialize_value 被设计为一个内联函数,它在执行时会生成一个“标记”事件(比如 mark_debug_value),该事件会通过 record_debug_event 推入调试事件队列。
  • record_debug_event 是线程安全的,因此整个操作从此变得线程安全。
  • 此操作不再依赖调试系统是否已初始化,也不需要立即归档,只是把事件记录下来,交由后续的调试系统在需要时处理。

事件结构细节:

  • record_debug_event 会返回一个事件对象指针。
  • 新添加的事件类型可能叫 value_debug_event(用于与已有命名保持一致),其字段指向被记录的调试变量信息。
  • 调试系统处理该事件时,可以读取它的子事件指针,并将其挂入适当的分组结构中。

代码组织调整:

  • 去掉原来那些直接修改状态的部分,直接在 debug_initialize_value 中内联完成事件创建和标记操作。
  • 将这些变量事件视为标准的调试事件,由调试系统统一解析处理,加入对应分组或处理逻辑中。

最终收益:

  1. 线程安全完全保证:事件机制本身是线程安全的,无需显式加锁。
  2. 结构解耦更强:不再依赖外部调试系统的初始化状态,避免了隐式依赖。
  3. 更好扩展性:所有调试行为都通过事件管理,便于统一调试视图和工具链扩展。
  4. 调试体验更佳:无论变量从哪产生,调试系统都能在后续阶段收敛处理,方便追踪和展示。

总结:

通过将变量初始化行为改写为调试事件的记录,我们彻底解决了线程安全、结构依赖、代码侵入性等问题。这个新的处理方式逻辑清晰,适配性强,能与现有调试系统高度集成,为后续维护与功能扩展提供了良好的基础。后续只需在事件解析阶段加一个 case,处理 mark_debug_value 类型事件,将其挂入变量分组即可完成整个闭环。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_debug.cpp: 实现输出所有变量列表的功能

我们现在要处理的,是将调试变量以非层级(flat list)的形式先正确添加到调试系统中,等这一部分完成后,再继续扩展成具备层级结构的版本。整个过程分两步走,先让变量在调试界面中能正常显示,然后再处理层级化的问题。


当前状态:

  • 已有部分无问题

    • CollateAddVariableToGroup 函数已经具备基础能力;
    • DebugState 状态也正常;
    • event 是我们从调试事件流中获取的事件实例;
    • 并且我们已经知道我们要添加的是事件中的 value_debug_event(即子事件部分)。
  • 问题的关键在于中间环节:分组归属未知
    虽然我们知道事件要添加,但不知道该将其放入哪个分组,也就是:当前没有一个确定的变量分组(group)目标


game_debug.cpp: 引入 GetGroupForNameγ

我们希望实现一种功能,比如 get_group_for_name 这样的东西,意思是:根据变量名或其他标识获取一个对应的分组。这个分组可以是已有的,也可以是根据名称自动创建的。无论是按名称查找已有组,还是按规则生成新的组,总之核心目标是将变量合理地组织起来。

这个函数的意义在于简化变量挂载的逻辑,让每个变量都能明确知道它应该被挂在哪个组里。我们只需要通过一个统一入口,比如 get_group_for_name(name),就能解决这个归属问题。

这个接口的存在带来了灵活性,比如:

  • 可以将不同前缀的变量放入不同分组(如 "Player_Health" 进入 "Player" 分组);
  • 可以支持路径分组(如 "UI/Inventory/ItemCount");
  • 甚至可以在调试工具中显示出结构化的层级树,用户可以展开收起。

我们暂时也不纠结它内部具体怎么实现,哪怕只是内部维护一个简单的哈希表,按名称检索对应 group,或者是 vector 线性查找都没问题。关键在于流程建立起来就好,哪怕现在只是草拟的接口,只要它逻辑通顺,就可以继续构建后续系统。

总之,这就是我们现在要做的 —— 建立一个通过名字找到对应分组的机制。至于细节,先不去担心,后面实现的时候可以慢慢优化。只要这套分组机制在逻辑上能够运作,后面无论是加缓存、搞层级、还是统一路径管理,都会变得容易得多。
在这里插入图片描述

game_debug.cpp: "加倍提高复杂性"δ

我们在思考将变量加入调试系统分组时,不仅仅是简单地把一个变量放进一个名字对应的 group,而是打算进一步做得更“花”一点,也就是搞一个更有层次感、更智能的结构。

思路是:我们想做一个更酷的处理方式,比如说通过变量的名字自动生成层级结构。例如变量名 "Player/Health/Max",可以自动解析出三层结构:PlayerHealthMax,然后把这个变量放到最末尾那一层,也就是 Max 节点上,而上面两层作为组的嵌套路径。

所以这一步我们需要做的是:

  1. 接收一个变量(debug 事件);
  2. 根据它的名字,提取一个路径(可以是带 / 的名字,或者其他格式);
  3. 把这个路径结构变成一个链式的分组结构,或者说是“链接结构”(link structure);
  4. 把变量插入这个结构的最末端。

而这个“link”的获取,就变成了一个核心操作,比如 GetHierarchicalLinkFromName(lock_name)。这个函数的任务是把我们给的名字解析出来,然后构建或返回这个路径对应的结构。

这就引出了另一个小问题:**原来如果那个位置已经有变量了怎么办?**我们需要处理“旧的变量”与“新来的变量”的关系。可能要替换、可能要合并,也可能是并列记录。这部分逻辑就变得稍微复杂了。

所以,总体而言:

  • 我们的目标不只是添加变量,而是将变量 插入一个具有分层结构的调试系统中
  • 变量的名字会被解释成路径结构,我们通过这个路径来找到正确的插入位置;
  • 系统需要有能力构造这条路径(如果还不存在);
  • 插入操作也需要考虑已有值、冲突的处理方式;
  • 最终形成的系统会有更好的可视化、更强的组织性,也更适合大型项目中调试变量的集中管理。

虽然处理起来确实有点“麻烦”,但如果搭好这套系统,后面无论是性能分析、bug 定位、还是调试数据呈现,都会变得非常有价值。
在这里插入图片描述

Blackboard: 通过 game_config.h 在执行时预加载值

我们本来在考虑一个潜在的问题:当我们创建一个调试变量的层级结构,比如:

foo  
└── bar  └── bass = 5.0

这个结构在运行时是存在的,其中 bass 是一个浮点值(比如 5.0)。在运行时,我们也许会去修改这个值,比如通过调试界面或者热重载方式进行更新。但是我们还想到另一个场景:当游戏的可执行文件进行重载(热重载、代码更新、重新编译等)之后,原本这些调试变量(比如 bass)对应的事件数据就会被清除或失效。这意味着之前写入的值(比如 bass = 3.0)可能就会消失。

一开始我们以为需要在可执行文件重新加载之后,重新恢复这些变量的值,于是思考要不要缓存原来的数值,或者在某个机制中保存下来再重新设定。

但随后我们意识到,其实这个问题我们已经解决了:

  • 我们之前构建了一个机制,将这些变量的值写入到一个全局配置存储中(比如 game_config.h 这样的头文件);
  • 当我们修改变量(如 bass = 3.0)时,系统会自动将这些变动写入这个配置文件;
  • 当新的可执行文件加载时,会从这个配置文件中重新载入这些变量值;
  • 因此,即使变量对应的事件数据在重载时丢失,值本身仍然被保存在配置文件中
  • 这样,我们就不需要在调试系统里做额外的操作来“记住”旧值,所有的值都会在重启后自动恢复。

这个逻辑让整个调试系统更健壮也更自动化,我们不再需要为“变量值在重载后丢失”这种情况做手动处理,原本担心的问题也就完全不复存在了。这个机制的建立等于把调试变量变成了“有状态的、可持久化的”系统组件。总结起来:

  • 可执行文件重载时变量事件会失效;
  • 但变量的值会被写入全局配置文件中;
  • 新可执行文件加载时会自动读取这些值;
  • 无需额外恢复逻辑,所有状态自动重建;
  • 我们之前做的持久化机制正好解决了这个问题。

所以,之前思考的问题现在已经完全不需要担心,逻辑闭环已经完善,可以安心地继续后续功能的开发。

game_debug.cpp: 撤销并引入 GetGroupForHierarchicalName

我们现在的目标是实现一个功能,让我们可以根据名称将调试变量添加到正确的分组(group)中,从而保持调试数据结构的清晰性与逻辑性。

我们需要实现的核心点是:根据变量的名称获取对应的调试分组。为此,应该编写一个函数,例如 GetGroupForHierarchicalName,其作用是:

  • 接收一个变量名称;
  • 查找或创建一个与这个名称匹配的调试分组;
  • 返回该分组的指针,以便我们后续将变量添加进去。

为实现这一目标,我们还需做出以下补充和准备:

  1. 传入 debug_state
    获取分组时需要访问当前的调试状态,因此函数中必须传入 debug_state,否则就无法查找或维护调试分组的结构。

  2. 默认分组机制:
    在之前的设计中,调试变量是在打开和关闭数据块(block)的时候被添加进特定分组的。但现在,我们希望这些变量独立存在,不依赖 block 的结构。因此,我们要确保存在一个默认分组或者一个静态分组容器,用于容纳这类“永久性”的调试变量。

  3. 分组搜索与创建逻辑:

    • 如果给定名称的分组已存在,就返回该分组;
    • 如果不存在,就新建一个;
    • 这个逻辑需要遍历当前的调试分组集合,并按名称匹配;
    • 名称匹配可以支持层级式结构,例如 foo/bar/baz 被解析为多层分组嵌套。
  4. 避免状态丢失与线程冲突:
    因为当前我们是将所有调试变量通过记录事件的方式推送到调试系统的,所以这个过程是线程安全的;
    同时我们也不需要再手动管理状态的持久性或线程同步,这一点在之前的步骤里已经解决了。

总结我们接下来的工作重点:

  • 编写 GetGroupForHierarchicalName(debug_state, name) 函数;
  • 在调试系统中增加默认分组或者命名式分组的维护机制;
  • 确保新的调试变量都被添加到这个分组中;
  • 这一机制将替代之前基于 block 开关的变量管理方法,使系统更简洁一致。

我们当前的目标是先完成“非层级”的添加方式,也就是说,先让所有变量可以简单地被加入一个基础分组中显示出来。接下来再扩展支持更复杂的分组结构和层级显示逻辑。整体方向是先跑通基础流程,再逐步精细化和美化调试系统的组织结构。
在这里插入图片描述

game_debug.cpp: 向 debug_state 添加 *ValuesGroup

我们现在面对一个结构性问题:我们正在将调试变量加入一个叫做 “values group” 的分组中,但这些变量是永久性的调试值,不应该被每帧擦除。然而目前的实现中,这些变量被错误地分配到了collation arena 上,这是一个用于临时帧数据的内存区域,每帧更新都会被清除。

当前的关键问题

  • 当前通过 collate_add_variable_to_group 添加的变量,是放在 collation arena 上的;
  • 这种设计适用于帧数据,但不适用于永久存在的调试变量
  • 我们真正需要的是一种方式,把这些变量放进一个持久化的内存区域(或者结构)中,不被每帧刷新掉;
  • 同时又要在某个时机手动清理那些不再使用的调试值,保持调试系统整洁、可管理。

我们理想的做法

  1. 持久化变量存储:
    我们应该有一个专门用于保存永久调试变量的区域,比如说一个“调试值持久存储区”或者“全局调试变量分组”;

  2. 临时与永久数据分离:

    • 临时调试数据继续使用当前的 collation 结构;
    • 永久性调试变量则需要跳过 collation arena,直接放入一个不会每帧清空的区域;
    • 这就需要我们在 collate_add_variable_to_group 之外,设计一种新的添加方式或者更底层的处理逻辑。
  3. 延迟处理问题:
    目前暂时不处理帧清除的部分逻辑,留作后续改进。未来我们需要实现帧间调试数据的有序清理逻辑,做到:

    • 每帧清除旧变量;
    • 保留永久性变量;
    • 对于已经不再使用的永久变量,定期做垃圾清理或标记删除。

未来计划

  • 后续我们需要重新设计调试系统的数据生命周期管理策略,完成整个 collation 模块的长期化改造;
  • 这个工作量不小,预计可能需要超过一天的时间;
  • 可以考虑等到下一次集中开发时再做,也可能在休息之后展开。

临时应对措施

  • 当前阶段,我们可以暂时跳过这个清除问题
  • 先确保这些永久变量可以正确地显示和添加到分组中;
  • 之后再系统性地重构整个调试数据框架,解决持久性与自动清理的矛盾。

小结

我们正在构建的调试系统开始面临一个生命周期管理的问题,临时与永久调试数据混在一起导致设计不清晰、管理复杂。解决之道是:分离两类数据的添加与存储方式,并实现更好的帧清理与数据保持策略。这个方向清晰,但实现复杂,是接下来我们重点要攻克的系统性难点。
在这里插入图片描述

game_debug.cpp: 让 CollateAddVariableToGroup 测试 Permanent

我们在当前的调试系统中遇到一个问题:缺乏对“永久调试数据”与“临时调试数据”的明确区分机制。为了解决这个问题,我们需要在 collate_add_variable_to_group 或类似的函数中引入一种新的方式,以便我们可以区分要加入的数据是永久保留还是仅限当前帧使用。

当前的问题与设计缺陷

  • 目前所有变量默认都被加入到 collation arena,这意味着它们属于“临时”内存,每帧会被擦除;
  • 然而,一些变量(比如调试值)不应该每帧丢失,它们应该是长期存在的;
  • 我们还没有实现“每帧处理机制”,所以暂时无法明确每帧该保留什么,清除什么;
  • 所以目前整个系统在“数据生命周期管理”上是残缺的。

临时解决方案

为了暂时解决问题,我们计划在数据添加接口中引入一个“是否永久”标志位:

collate_add_variable_to_group(..., bool permanent)
  • 如果 permanent == true,我们就把数据加入 debug arena(一个更长生命周期的区域);
  • 如果 permanent == false,我们就继续使用当前的 collation arena,表示它只是临时调试数据;
  • 这样,我们就可以对不同生命周期的数据进行分类处理。

实际实现要点

  • 在添加变量的时候判断是否为永久;
  • 根据判断结果选择存储位置(debug arena vs collation arena);
  • 所有永久变量都应该进入 debug arena,防止每帧被擦除;
  • 后续需要对这些永久变量进行手动或自动管理,以防占用过多资源。

长远目标

  • 彻底移除这种 flush(清除)机制,不再依赖 collation arena 来保存所有调试信息;
  • 把所有变量都视为永久变量,或者设计出更智能的帧管理系统;
  • 目前这种“flush every frame”只是一个临时权宜之计,不是最终方案;
  • 真正理想的调试系统应该能灵活地处理各种生命周期的数据,自动识别哪些数据需要长期保留,哪些是一次性的。

小结

我们正在对调试变量的生命周期做第一次真正意义上的分类——永久变量 vs 临时变量。虽然当前是临时实现,但这也是我们逐步迈向更加结构化、智能化调试系统的一步关键过渡。最终目标是全面构建一个具有生命周期感知能力的调试系统,不再依赖暴力清除、临时内存,而是以逻辑清晰的数据管理实现高效调试与分析。

在这里插入图片描述

game_debug.cpp: 编写 GetGroupForHierarchicalName

目前,针对调试系统的变量管理,我们希望让所有的调试值都加入一个统一的组——值组(value group),而不进行复杂的分组处理。这样做的目的是简化目前的调试流程,因为我们暂时不需要考虑层级结构或更复杂的分组,只要所有变量都放入一个统一的值组中,便于管理和追踪。

关键变化与实现

  1. 简化变量分组:目前,所有的变量会被放入一个“值组”,不再进行任何层级分组或者复杂的树状结构组织。

    • 这种简化是为了让调试系统尽早实现基础功能,后续可以再增加复杂的分组功能。
    • 通过把所有变量统一到一个组里,可以避免当前的复杂性,专注于基础逻辑的实现。
  2. 添加永久性选项:在创建变量组时,需要考虑是否让该组的数据永久保存。为此,我们会引入一个“永久”标志,决定每个调试变量是否应保持长久存在:

    • 永久的调试变量将进入 debug state 中的“持久性调试区”(permanent debug state area),并保留在内存中,直到明确要求清除。
    • 临时变量则不会如此持久,仅在当前帧有效,处理后会被清除。
  3. 函数设计与调整:对于创建和管理变量组的功能,需要添加对“永久性”属性的判断。具体来说,create_variable_group 函数需要加入对“永久”标志的处理逻辑:

    • 如果变量是永久的,它将被添加到持久调试区;
    • 如果变量是临时的,它会继续使用当前的调试区域。
  4. 初始化调试系统:在调试系统初始化时,应该创建一个初始的变量组来包含所有需要跟踪的变量。这些变量会根据它们的永久性属性被添加到适当的区域。这个初始化过程应该尽早完成,以确保调试系统在游戏启动时就能正常工作。

后续改进

  1. 细化层级结构:虽然目前所有变量都放入统一的值组中,但后续可以考虑根据需要为变量建立层级关系或分组结构,例如按模块、功能等进行组织。这将有助于提高调试信息的可读性和管理性。

  2. 增强数据生命周期管理:目前的方案依赖于“永久”和“临时”变量的分类,但在后续开发中,可能需要引入更智能的数据管理机制,自动决定哪些数据应该保留,哪些数据应当在处理完毕后删除。

  3. 完善的调试信息流:通过这种简化的方式,调试信息的流动更加直观和易于管理,但我们需要确保系统的可扩展性,以便将来能处理更复杂的调试需求。

总结

目前我们已经简化了调试系统的变量管理,集中处理所有调试值,并通过永久标志区分长期保存和临时保存的变量。这种简化方案为当前的开发需求提供了高效的解决方案,但未来可能需要进一步完善,以应对更复杂的调试需求和数据生命周期管理挑战。
在这里插入图片描述

在这里插入图片描述

game_debug.cpp: 初始化 DebugState->ValuesGroup

在初始化调试系统时,需要确保所有必需的组件和变量都得到正确配置。首先,需要确保创建一个值组(values group),这个值组用于存储所有调试相关的变量。为了实现这一点,我们将通过 CollateAddVariableToGroup 来创建该值组。

虽然当前命名方式不太恰当(因为这个命名并不符合我们后续的命名惯例),但目前将其作为一个临时的命名方式,以便完成初步实现。当调试系统启动时,values group 会被初始化并用于存储调试值。

实现步骤:

  1. 初始化值组:在调试系统开始时,通过调用 CollateAddVariableToGroup 创建一个专门用于存放调试变量的组。这个步骤保证了变量能够被集中管理,并能够在之后的调试过程中被正确使用。

  2. 命名调整:目前使用的名称“CollateAddVariableToGroup”可能不再符合实际需求,特别是由于该名称与“collation”(合并)不再相关。因此,可能需要在后续工作中更新名称,以便更好地反映其功能和作用。

  3. 后续操作:完成这一初始化之后,所有需要调试的变量就会被添加到这个值组中,方便管理和查看。当调试系统运行时,这个值组会在后台工作,确保调试数据能够被有效跟踪。

未来改进

在实现了基本的调试系统后,可能需要对命名和结构进行优化。比如,更新“创建变量组”的函数名称,使其更符合当前系统的需求,并确保变量分组能够在更复杂的调试场景中适应和扩展。

总之,当前的目标是简化调试系统的变量管理,确保所有变量都能有一个固定的组来进行存储和处理,避免混乱和冗余。
在这里插入图片描述

运行游戏并查看它是否正确获取了所有调试变量

现在,理论上系统应该能够识别并收集所有的调试变量。接下来,我们需要做的就是找到一个地方来显示这些变量。为了临时显示这些变量,可以使用当前已有的调试绘制菜单。

实现步骤:

  1. 显示调试变量:为了展示调试变量,首先要确保系统已经将它们成功加载到一个可访问的地方。通过已有的调试绘制菜单,可以方便地将这些变量进行临时展示。

  2. 调试菜单:通过调试菜单,可以将所有收集到的调试变量呈现给用户。这可以作为一种临时的解决方案,方便开发人员在调试过程中查看和调整这些变量。

  3. 后续优化:虽然当前的方式可以临时显示这些变量,但可能需要进一步优化界面和数据展示的方式。比如,将这些变量展示在更加结构化的界面上,方便用户快速找到和理解它们的含义。

目标:

  • 快速查看调试变量:在开发和调试过程中,能够快速查看系统中的调试变量,便于调试和分析问题。
  • 临时解决方案:通过现有的调试绘制菜单,可以临时展示这些变量,后续可以根据需要优化展示方式。

总的来说,这个阶段的工作是确保调试系统能够正确显示所有调试变量,便于开发人员进行实时调试和调整。
在这里插入图片描述

game_debug.cpp: 在 DEBUGDrawMainMenu 中设置 *HackyGroup 为不同值

在调试绘制菜单中,需要做的事情是,在遍历调试项时,可以通过将当前的“hacky group”(临时组)替换为一个新的组。新的组将会是 debug state values group,这个组将包含所有调试变量。通过这种方式,可以查看这个组中的内容,检查是否成功地收集了所需的调试值,并验证是否能正确地显示这些变量。

关键点:

  1. 替换临时组:当前的临时组(hacky group)将被替换为 debug state values group,这样可以显示该组中的调试变量。
  2. 验证数据是否正确:通过在调试绘制菜单中展示该组,可以验证是否成功收集并显示了所有调试变量。
  3. 调试验证:此举主要是为了测试当前系统是否能正确拾取并显示这些调试变量。如果这一过程成功,那么系统能够正确处理和显示调试信息。

目标:

  • 通过临时将数据组替换为 debug state values group,可以快速验证调试变量是否被正确加载和显示。
  • 为后续的调试工作提供一个简单的验证方式,确保所有数据都能正确显示并供开发人员检查。
    在这里插入图片描述

在这里插入图片描述

运行游戏并查看程序的表现

首先,系统已经基本接近预期的功能,虽然目前仍然存在一些问题。在调试时发现,尽管本地持久化值(local persist)应该只初始化一次,但却出现了多个相同的调试值。这是一个问题,需要进一步排查原因。虽然初始化已经接入正确,可以实时启用值,而不再依赖增长回调周期,但问题在于为什么会有多个相同的调试值被重复添加。

接下来的步骤是,首先需要排查为什么会出现多次添加相同的调试值,然后在解决这个问题后,系统就可以顺利地按预期工作。
在这里插入图片描述

game_debug.cpp 和 game_debug_interface.h: 调查为何我们会多次添加相同的变量

从头开始,当调用 DebugType_MarkDebugValue 并使用 CollateAddVariableToGroup 时,理论上这些操作应该正常工作。我们在添加到组时,应该已经正确指向了事件,并且该事件也指向了正确的 value debug event。经过进一步思考,应该使用该事件的名称,但目前我们还没有执行任何操作,因此这一点暂时不会产生影响。不过,如果我们接下来去做操作,这可能会是一个问题。

当我们调用 CollateAddVariableToGroup 时,检查接口部分,确保它能正确工作。我们已经正确初始化了子事件,包括名称、行号、线程ID和核心类型,所有这些看起来都设置正确。现在,问题在于为什么会重复添加相同的调试值。我们已经设定了 local persist 为静态,因此每次初始化应该只执行一次。如果没有设置为静态,应该会有更多的重复条目,但现在它是静态的,这意味着我们应该没有犯错。

整体来看,系统的调试部分看起来是正常的。没有明显的错误,理论上这些步骤应该顺利执行,重复值的出现可能是由于其他原因导致的,所以接下来需要仔细排查是否有其他潜在的错误。
在这里插入图片描述

在这里插入图片描述

调试器: 进入 DEBUGInitializeValue 并检查 Event

在调试过程中,发现问题本应该能解决,但实际上并没有解决。因此,设置了一个断点在 debug initialized value 中,以便查看发生了什么并搞清楚问题所在。调用发生在 GameUpdateAndRender ,特别是当 RecomputeGroundChunkOnEXEChange 时。首先,查看了传入的事件,事件看起来是正确的,具有空的块名称(符合预期,因为没有传递该值),行号和线程ID也正确,类型是预期中的“mark the debug value”,但值尚未被赋予。

然后,初始化时,事件的值被赋予了 debug value,此时 debug value 看起来毫无意义,但这是因为它尚未被初始化。经过初始化后,变量得到的名称和其他数据是正确的,包括行号、线程ID以及类型等,所有这些都看起来都是正确的。

检查局部变量时,虽然之前关闭了局部变量显示,但这次开启后可以看到,调试值的 RecomputeGroundChunkOnEXEChange 已经正确赋值,值为 10,这也是配置中的默认值,因此这也是正确的。所有的初始化和设置看起来都没有问题。

最终,虽然一开始没有发现问题,但经过进一步排查,确认这些操作按预期工作,值的设置也是正确的。因此,推测可能问题并非出在初始化阶段。

game_debug.cpp: 在例程开始时测试 Event->Type 是否为 DebugType_MarkDebugValue

问题的根源在于处理流程的位置不对,具体来说,当前代码将处理逻辑放在了 collation frame 处理的过程中。这意味着,如果还没有帧标记,事件就不会被记录,这是不符合预期的。理想的情况是,无论帧是否已打开,都应该记录这些事件。

为了解决这个问题,需要在代码中加入一个特殊的处理逻辑,确保无论如何都能记录这些事件。因此,调整代码结构,增加一个条件判断,确保当事件类型为 mark debug value 时,事件能够被捕获。这个逻辑应该位于处理流程的顶部,而不是嵌套在 else if 中。

修正这个问题后,所有的事件应该能正常记录,而不会仅限于第一帧,因为原本的错误是所有事件只在第一帧中被处理,然后被丢弃,导致它们没有显示出来。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏并查看所有变量

现在,系统可以正确显示事件,但是接下来的问题是为什么会有这么多相同的事件。答案可能与静态变量的使用方式有关,尤其是在多线程环境中。很可能多个线程同时进入并使用这些静态变量,从而导致出现重复的事件。

静态变量通常在多线程环境中需要小心处理,尤其是如果它们是线程本地存储的一部分,可能会导致线程间的冲突和重复。在调试系统中,静态变量的使用并不常见,因此对于如何安全地处理它们并没有过多的关注。这可能是当前问题的原因。

尽管系统可以正常工作,但这个问题表明静态变量在多线程环境中的使用可能并没有完全遵循线程安全的原则。接下来的步骤将是检查静态变量是否存储在线程本地存储中,并探究多线程是否是造成问题的根源。
在这里插入图片描述

win32_game.cpp: 减少使用的线程数

为了调查为什么会出现多个相同的值,首先可以通过禁用多线程来测试是否与线程相关。这样可以更清楚地知道问题是否与线程处理有关,还是仅仅是一个常规的 bug。

为此,可以将线程数量减少到一个,进行测试。通过修改 create thread 和线程计数的设置,将线程数设为 1,然后重新运行程序,观察是否还会出现多个重复的值。如果禁用多线程后问题得到解决,那么很可能是多线程导致了静态变量的重复问题。如果问题依然存在,那么可能是其他的原因,需要进一步排查。
在这里插入图片描述

在这里插入图片描述

运行游戏并查看仍然输出过多的变量副本

在禁用多线程后,发现“ground chunks”中重复的值减少了,但其他部分依然存在大量重复的值。这表明问题可能并不仅仅是多线程导致的,可能是代码的其他部分存在问题,需要进一步调查为什么会有那么多重复的条目。

win32_game.cpp: 恢复使用的线程数

看起来问题并不是出在多线程上,因此接下来需要调查为什么会有多个重复的条目。虽然初始化函数似乎不应该被多次调用,但实际上它却被调用了多次,这可能是问题的根源。因此,需要进一步分析和排查这个初始化函数的调用情况。
在这里插入图片描述

调试器: 进入 DEBUG_IF 并转到反汇编

通过检查调用点,发现初始化函数实际上只在第一次调用时执行。这表明,初始化过程中没有问题,但仍然出现了重复添加条目的情况。进一步检查汇编代码后,发现初始化过程似乎没有异常,所有操作按预期执行。问题可能出在树结构上,可能是处理过程中某个环节出现了循环,导致相同的调试ID多次被添加。整体来看,虽然初始化看似正常,但调试ID重复问题依然困扰,可能与数据结构的管理有关,具体原因仍不明。

game_debug.cpp: 发现 RestartCollation 一直在重新读取事件并添加它们

问题的根源在于重复的事件添加。当调试记录重新整理(collate)时,每次重新开始都会重新读取并添加相同的事件,这导致了多次添加相同的调试信息。因此,问题出在每当重新整理时,事件没有被清除或标记为已处理,造成重复添加。为了避免这种情况,需要在重新整理过程中确保事件不重复添加,可以通过在事件已存在时跳过添加或在每帧处理时清理掉已处理的事件。当前的解决方案是,重新整理时不清除这些事件,这会导致它们被重复处理。

接下来的改进计划是每次处理时只处理一个帧,并清理掉已处理的调试事件,以防止多次添加同一个事件。这个方案将会消除重复的问题,并且可能需要一些额外的工作来确保处理的效率和准确性。
在这里插入图片描述

DEBUG_IF 宏中的变量初始化技巧让我的内部代码质量小很伤心。能否将该初始化移到结构体“方法”中?

问题的关键在于使用 debug if 宏可能会影响到内部代码的质量,造成不必要的复杂性。有人提出是否可以将初始化过程移到结构体的构造方法(struct 的构造函数)中,目的是减少宏对代码质量的影响,同时使得代码更整洁和可维护。

在游戏中,持久化(persisting)和数据存储通常会有特定的机制来确保数据的持久性和一致性,可能需要将一些初始化步骤放到结构体的构造函数中,以确保在对象创建时完成必要的初始化,而不是依赖宏来执行这些操作。

在实时代码加载时保持 DEBUGValue 很棒,我一开始不太明白你在做什么,但通过演示我明白了,真的很酷。我把变量的值和调试变量的值混淆了。这个小补充带来了巨大的好处。

在处理调试值(debug value)和变量值之间的差异时,发现通过实时代码加载功能非常有效。这种方式可以在调试时查看变量的值,并且通过调试的方式调整这些值,从而显著提高调试效率和代码质量。演示展示了这一点,尤其是在调试变量和变量值之间进行精确调整时,能够带来巨大的好处。

此外,对于名字问题,有人提到他们未能修复的部分,尽管不完全理解对方的具体意思,但从上下文来看,可能是在讨论如何清理或改进某些结构或代码,以使其更加简洁和可维护。

你是否因为没有修复 Collation 而感到遗憾?如果没有其他问题,能不能现在看看它,这样你就不会感到遗憾了?

讨论了数据流处理的最后一步,涉及到数据的整合和包装。这一过程需要一定的时间和精力,不可能在短短的10分钟内完成。主要的工作是在系统中如何有效地处理和传递数据流,并确保其在不同环节中被正确地整理和使用。这是一个相对复杂且需要仔细调试的步骤,因此并不打算在短时间内解决,而是需要进行较为详细的工作。

你为什么这么厉害?你是在哪个道场训练成代码战士的?

哈哈,有时候就是觉得自己很棒。至于说“代码战士”嘛,这个词其实有点过时,因为以前有个叫做“CodeWarrior”的程序,实际上并不是特别酷,它是一个Mac的集成开发环境,感觉挺普通的。

我引用道:“与其写 debug_event Name = initiator((AnotherVar = something, something)); 不如创建一个结构体构造函数 debug_event (&AnotherVar, something),来初始化值。”

在讨论时提到,不建议为调试事件(debug event)添加构造函数。添加构造函数的理由不明确,因为这似乎并不会带来什么实质性的不同。若为调试事件添加构造函数,就不能再直接在栈上创建它,而必须调用构造函数,这样还需要再编写另一个构造函数来优先使用新的构造函数。这种方式会增加不必要的复杂性,因此不考虑为调试事件创建构造函数。

此外,还对现有代码的问题表示不确定,无法理解对方试图解决的具体问题。

这样你就不会在宏的函数调用中进行赋值了,这让我感到非常伤心。

在讨论中表达了对在宏中进行赋值操作的反对,认为这会导致不必要的复杂性,尽管从CPU的操作顺序上来看,这些赋值操作不会造成任何不同。特别是在构造函数的例子中,如果使用构造函数,就无法直接看到发生了什么,需要去查看构造函数的实现,增加了理解的难度。

反而,当前的写法更为直观易懂,因为宏中的操作直接可见,不需要跳转到构造函数去查看实现。此外,添加构造函数会改变结构体的行为,导致编译器强制要求在创建结构体时调用构造函数,这增加了代码的复杂性,也可能引发其他潜在问题。因此,认为为结构体添加构造函数的做法是不推荐的,反而会让代码变得更加难以理解和维护。

最后,提到在某些情况下网络连接不稳定导致未能及时看到信息,这也让人有些不满。

你喜欢什么类型的游戏?

我喜欢各种类型的游戏,只要它们有趣。我不喜欢那些看起来和我以前玩过的游戏很相似的游戏,或者只是简单地把它们重新包装一下。我特别不喜欢那些基于电影授权的游戏,比如说《复仇者联盟》之类的游戏,它们往往只是把同样的游戏玩法换个皮肤而已。比如说,玩家按一个按钮来进行攻击,角色变成了绿巨人或者其他什么角色,但本质上它还是和其他游戏一样,完全没有新意,这样的游戏让我感到非常无聊。我更喜欢那些设计上有创意的游戏,能给人带来一些新鲜感或者不同的玩法体验。

相关文章:

游戏引擎学习第214天

总结并为当天的任务做好准备 昨天,我们将所有调试控制代码迁移到使用新的调试接口中,但我们没有机会实际启用这些代码。我们做了很多准备工作,比如规划、将其做成宏、并将其放入调试流中,但实际上我们还没有办法进行测试。 今天…...

算法题型讲解

一.双指针 主要分为俩种类型: 1.左右指针:双指针指向开头,以一定标准移动或交换,对区域进行划分,或找到特殊点的位置 (如:快慢指针判断有无环,移动零) 2.对撞指针&am…...

Qt零散知识点

Qt零散知识点 Qt优点 跨平台接口简单,易上手一定程度上简化了内存的回收 Qt创建新项目 第一个窗口类默认的三个基类 QWidgetQMainWindowQDialog 其中QWidget是QMainWindow和QDialog的基类 一个Qt项目默认创建的文件 main.cpp 入口函数pro文件:工…...

算法导论(递归回溯)——⼆叉树中的深搜

算法思路(129) 前序遍历遵循“根节点、左子树、右子树”的顺序遍历二叉树的所有节点,常用于解决子节点状态依赖于父节点状态的问题。 算法思路: 在前序遍历的过程中,我们可以将信息从节点向左右子树传递&#xff0c…...

UE像素流发布linux并进行容器化部署

一、宿主机软硬件要求 主要需要关注两部分:对像素流的支持、对linux容器的支持。 1、像素流要求: https://dev.epicgames.com/documentation/zh-cn/unreal-engine/unreal-engine-pixel-streaming-reference?application_version5.3 2、linux容器要求…...

arco-design-vue:给<a-table>组件每一行添加data-id属性,并根据id数组是否包含此行id进行样式处理

场景需求: 需要支持框选,框住的行需要更改背景色来标识选中了。如下图所示 【shiftq】表示【加入】,【shiftw】表示【移除】 拆分要实现的功能: 1.框选,选中行数据 2.选中行之后,当前行的样式要有所改变 …...

为什么ChatGPT选择SSE而非WebSocket?

为什么ChatGPT选择SSE而非WebSocket? 一、ChatGPT回答问题的技术逻辑 ChatGPT的响应生成基于Transformer架构和自注意力机制,其核心是通过概率预测逐词生成文本。当用户输入问题后,模型会先解析上下文,再通过预训练的庞大语料库…...

【论文精读与实现】EDC²-RAG:基于动态聚类的文档压缩方法提升检索增强生成RAG性能

🧠 向所有学习者致敬! “学习不是装满一桶水,而是点燃一把火。” —— 叶芝 我的博客主页: https://lizheng.blog.csdn.net 🌐 欢迎点击加入AI人工智能社区! 🚀 让我们一起努力,共创AI未来! 🚀 1. 论文核心思想 这篇由清华大学团队提出的EDC-RAG框架,针对当前…...

SAP S/4HANA Public Cloud的实施特点、项目阶段、资源和工具

1、SAP S/4HANA Public Cloud与OP、PCE部署的区别 近年来,SAP大力推广S/4HANA Public Cloud版本,越来越多的顾问开始接触SAP Public Cloud项目。S/4HANA Public Cloud强调标准化和简化,适合快速实施的企业,在实施方法、技术特点以及项目管理方法上,都与OP版本、PCE版本都…...

Kafka的生产者和消费者的关系

Apache Kafka 是一个分布式流处理平台,生产者和消费者是 Kafka 中两个核心角色,它们之间存在着紧密的关系,以下从多个方面为你详细介绍: 工作模式 生产者:负责将数据发送到 Kafka 的主题(Topic&#xff0…...

DAY01:【pytorch】张量

1、张量的简介 1.1 Variable Variable 是 torch.autograd 中的数据类型,主要用于封装 Tensor,进行自动求导 data:被包装的 Tensorgrad:data 的梯度grad_fn:创建 Tensor 的 Function,是自动求导的关键req…...

如何用VBA编辑器合并Word文档:详细教程

在实际办公中,我们经常需要将多个Word文档合并为一个。我将详细讲解如何通过VBA编辑器实现Word文档的自动合并。 前提:先将主文档另存为“docm宏格式”,将要合并的所有文档放在同一个文件夹内。 一、安装VBA编辑器 VBA编辑器是Word自带的工…...

智能体代理模式(Agent Agentic Patterns)深度解析

一、智能体代理模式的理论演进与核心定义 1.1 从自动化工具到认知代理的范式转变 传统AI系统以 规则驱动型工作流 为核心,依赖预设程序执行确定性任务(如制造业机器人)。而智能体(Agent)通过 大语言模型(…...

若依微服务集成Flowable仿钉钉工作流

项目简介 本项目工作流模块集成在若依项目单独一个模块,可实现单独运行部署, 前端采用微前端,嵌入在若依的前端项目中。因博主是后端开发,对前端不是太属性,没将工作流模块前端代码移到若依前端。下面贴上代码工程结构…...

关于数据结构B树部分的知识点,解题过程以及方法思路

关键点数和节点数的关系...

TOGAF之架构标准规范-技术架构

TOGAF是工业级的企业架构标准规范,本文主要描述技术架构阶段。 如上所示,技术架构(Technology Architecture)在TOGAF标准规范中处于D阶段 技术架构阶段 技术架构阶段的主要内容包括阶段目标、阶段输入、流程步骤、阶段输出、架构…...

经济金融优化:最优消费与投资分配的MATLAB实战

内容摘要 本文聚焦经济金融领域的优化问题,详细介绍最优消费和最优投资分配的理论与实践。 关键词:最优消费;最优投资分配;效用最大化;投资收益;MATLAB 一、引言 在经济金融领域,个体和企业常…...

【Python语言基础】17、继承

文章目录 1. 继承1.1 为什么要用继承1.2 继承的基本语法1.3 方法重写1.4 多重继承 2. supper()2.1 作用2.2 基本语法2.3 注意事项2.4 super() 在多继承中的特点 1. 继承 在 Python 里,继承是一种强大的编程概念,它允许一个类(子类&#xff0…...

基于CNN-GRU的深度Q网络(Deep Q-Network,DQN)求解移动机器人路径规划,MATLAB代码

一、深度Q网络(Deep Q-Network,DQN)介绍 1、背景与动机 深度Q网络(DQN)是深度强化学习领域的里程碑算法,由DeepMind于2013年提出。它首次在 Atari 2600 游戏上实现了超越人类的表现,解决了传统…...

DAY06:【pytorch】图像增强

1、基本概念 数据增强,又称数据增广、数据扩增,是对训练集进行变换,使训练集更丰富,从而让模型更具泛化能力 2、裁剪 — — Crop 2.1 transforms.CenterCrop 功能:从图像中心裁剪图片 size:所需裁剪图…...

K_KMS工具(适用windows和office)

目录 前言 一、下载 二、运行 前言 KMS工具(适用windows和office)。 一、下载 访问下载 💾下载👉工具下载地址:https://pan.quark.cn/s/bfdaa27ea823 二、运行 1、在下载目录中找到压缩包,并解压。 …...

Python Cookbook-5.12 检查序列的成员

任务 你需要对一个列表执行很频繁的成员资格检査。而in操作符的 O(n)时间复杂度对性能的影响很大,你也不能将序列转化为一个字典或者集合,因为你还需要保留原序列的元素顺序。 解决方案 假设需要给列表添加一个在该列表中不存在的元素。一个可行的方法…...

移动端六大语言速记:第13部分 - 网络与通信

移动端六大语言速记:第13部分 - 网络与通信 本文将对比Java、Kotlin、Flutter(Dart)、Python、ArkTS和Swift这六种移动端开发语言在网络与通信方面的特性,帮助开发者理解和掌握各语言的网络编程能力。 13. 网络与通信 13.1 HTTP请求 各语言HTTP请求实…...

kafka生产者partition数量和消费者数量的关系

在 Kafka 中,生产者的分区(Partition)数量和消费者数量之间存在着密切的关系,这种关系对 Kafka 集群的性能、数据处理的并行性以及负载均衡等方面都有着重要影响,以下为你详细介绍: 核心原则 Kafka 中每个…...

数据库主从复制学习笔记

目录 一、Binlog(Binary Log) 核心特性 核心用途 Binlog 格式(3种类型) 二、主从复制 核心原理 主库(Master) 从库(Slave) 配置步骤(以 MySQL 为例) …...

使用xml模板导出excel

下面这种表格如何使用xml导出呢&#xff1f; xml代码 <?xml version"1.0" encoding"UTF-8"?> <tables><styles><style id"h1" font.fontheightinpoints"14" font.fontname"黑体" alignment"c…...

深入解析栈式虚拟机与反向波兰表示法

1.1 什么是虚拟机&#xff1f; 虚拟机&#xff08;Virtual Machine, VM&#xff09;是一种软件实现的计算机系统&#xff0c;提供与物理计算机相类似的环境&#xff0c;但在软件层面运行。虚拟机的存在简化了跨平台兼容性、资源管理以及安全隔离等问题。 1.2 栈式虚拟机的架构…...

软件验收测试方法有哪些?验收测试报告如何获取?

大数据互联网时代&#xff0c;各种软件产品为我们的生活和工作带来了极大的便利&#xff0c;企业为了更好的保障软件产品质量&#xff0c;软件测试工作不可或缺。软件验收测试作为软件测试过程中的最后一个测试工作&#xff0c;也被称之为交付测试。验收测试主要是测试软件系统…...

Flexoo 柔性薄膜加热片技术全解析:从原理到应用优势

FLEXOO柔性薄膜加热片通过创新技术实现高效加热。它的柔性设计能够适配不同形状的表面,满足多种设备的需求。PTC加热技术让加热片具备自我调节功能,自动调整热输出以提升安全性与能效。固定功率加热技术则确保热量稳定输出,适合需要持续加热的场景。你可以依赖它的节能环保特…...

DeepSeek与搜索引擎:AI生成内容如何突破“语义天花板”

一、搜索引擎的“内容饥饿症”与AI的“产能悖论” 2024年&#xff0c;全球每天新增470万篇网络文章&#xff0c;但搜索引擎的索引拒绝率高达68%。这一矛盾的根源在于&#xff1a;算法对“高质量原创”的定义已从“形式独特性”转向“认知增值性”。传统AI生成内容&#xff08;…...

【1】k8s集群管理系列--包应用管理器之helm

一、helm概述 Helm核心是模板&#xff0c;即模板化K8s YAML文件。 通过模板实现Chart高效复用&#xff0c;当部署多个应用时&#xff0c;可以将差异化的字段进行模板化&#xff0c;在部署时使用-f或 者–set动态覆盖默认值&#xff0c;从而适配多个应用 helm工作流程&#xf…...

【零基础玩转多模态AI:Gemma3 27B开源视觉模型本地部署与远程访问】

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

安全岗の夺命连环问:(第壹篇)从XSS到0day的灵魂拷问

终极目录 一、面试官の死亡凝视&#xff1a;"给我手撕一个反射型XSS&#xff01;" 1.1 菜鸟の陨落&#xff1a;那些年我们写过的致命代码 1.2 渗透艺术&#xff1a;如何用XSS实现CSRF联动攻击 1.3 防御矩阵&#xff1a;OWASP ESAPI的十八层净化 二、血泪实战&am…...

IAP Firmware Upload Tools.exe IAP 网络固件升级教程

IAP是In Application Programming的简写&#xff0c;IAP升级可以被视为固件升级的一种形式,它是一种在应用程序运行过程中对固件进行更新的技术手段。允许MCU在运行过程中对MCU User Flash的部分区域进行烧写,目的是为了代替编程器对MCU烧录的依赖。 主程序UI 软件按钮说明&a…...

Redis 7高性能缓存与分布式架构实战

大家好&#xff0c;我是袁庭新。很高兴向大家推荐我的新课《Redis 7高性能缓存与分布式架构实战》。这套课程是我与两位一线大厂的高级开发工程师朋友共同研发的&#xff0c;他们分别来自华为和美团&#xff0c;拥有丰富的实战经验。我将担任课程的主讲&#xff0c;为大家带来全…...

自动驾驶时间同步

主要包含两个大的概念&#xff1a;时间系统间的时间同步与传感器数据间的时间同步 1. 时间系统间的时间同步 概念&#xff1a; 自动驾驶域控一般由多个芯片与多种类型的传感器组成&#xff0c;如&#xff1a;MCU SoC Camera Lidar Radar USS GNSS&#xff0c;其中 MCU…...

CISA关键措施要求解析:提升组织网络安全的实用指南

1. 引言 在当今日益复杂的网络安全环境中,组织面临着前所未有的挑战。美国网络安全与基础设施安全局(CISA)提出的关键措施要求,为组织提供了一个全面的框架来加强其网络安全态势。本文将深入探讨这些措施,并提供实际的实施建议。 2. CISA关键措施概述 CISA关键措施包括以下几…...

java笔记03

基本数据类型 数据值是存储在自己的空间中。 特点&#xff1a;赋值给其他变量&#xff0c;也是赋的真实的值。 引用数据类型 数据值是存储在其他空间中&#xff0c;自己空间中存储的是地址值。 特点&#xff1a;赋值给其他变量&#xff0c;赋的地址值。 综合练习 使用 ctrl…...

【HarmonyOS 5】鸿蒙的装饰器原理和自定义装饰器

【HarmonyOS 5】鸿蒙的装饰器原理和自定义装饰器 一、鸿蒙中的装饰器是什么&#xff1f; 在ArkTS中装饰器&#xff08;Decorator&#xff09;是一种特殊的声明&#xff0c;能够对类、方法、属性等进行标注和修改。 因为ArkTS 是TypeScript 扩展而来的编程语言&#xff0c;Ty…...

【Java学习】AI时代下如何学习Java语言开发

学习 Java 语言开发时&#xff0c;合理借助 AI 工具可以提升效率、深化理解&#xff0c;以下是具体的学习策略和方法&#xff1a; 一、利用 AI 辅助基础学习 1. 智能文档解读与语法解析 工具&#xff1a;ChatGPT、Bing Chat、Google Bard用法&#xff1a; 直接提问基础语法问…...

dd命令刻录CENT OS10 (.iso)光盘镜像文件到U盘

操作系统 | “扇区”、“簇”、“块”、“页”等概念_文件系统的簇和扇区-CSDN博客 Windows下面的DD工具_windows dd工具-CSDN博客 如何用 ISO 镜像制作 U 盘安装盘&#xff08;通用方法、无需 WinPE&#xff09;_isou-CSDN博客 1 到CENT OS 网站download iso光盘镜像文件 ht…...

2025年常见渗透测试面试题- Java考察(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 Java考察 一、Java MVC架构与数据流向 二、Java沙箱安全机制 三、iBATIS参数化查询与注入防御 四、…...

MySQL:事务的理解

一、CURD不加控制&#xff0c;会有什么问题 &#xff08;1&#xff09;因为&#xff0c;MySQL里面存的是数据&#xff0c;所以很有可能会被多个客户访问&#xff0c;所以mysqld可能一次会接受到多个关于CURD的请求。&#xff08;2&#xff09;且mysql内部是采用多线程来完成数…...

开源推荐#5:CloudFlare-ImgBed — 基于 CloudFlare Pages 的开源免费文件托管解决方案

大家好&#xff0c;我是 jonssonyan。 寻找一个稳定、快速、还最好是免费或成本极低的图床服务&#xff0c;一直是许多开发者、博主和内容创作者的痛点。公共图床可能说关就关&#xff0c;付费服务又增加成本。现在&#xff0c;一个名为 CloudFlare-ImgBed 的开源项目&#xf…...

[设计模式]发布订阅者模式解耦业务和UI(以Axios拦截器处理响应状态为例)

当前的代码使用了多个if-else分支来处理不同的状态码,这会导致代码耦合度高,难以维护和扩展。比如,如果未来要新增一个状态码的处理,就需要修改原有的拦截器代码,这违反了开闭原则。发布订阅模式可以将不同状态码的处理逻辑解耦,每个状态码对应一个订阅者,通过中间件来管…...

Redis的过期和内存淘汰策略

文章目录 惰性删除定期删除内存满了&#xff0c;数据淘汰策略 Redis 提供了两种删除策略&#xff1a; 惰性删除 、定期删除 惰性删除 定期删除 两种清除模式: 内存满了&#xff0c;数据淘汰策略 Redis 提供了八种数据淘汰策略&#xff1a; 1. 默认是不淘汰任何的 key&#x…...

每日一题-力扣-2999. 统计强大整数的数目 0410

2999. 统计强大整数的数目 问题分析 题目描述 题目要求统计区间 [start, finish] 内的强大整数数量。强大整数需满足以下条件: 每位数字不超过 limit以字符串 s 作为后缀关键要点理解 强大整数的定义:整数的每一位都不超过 limit,且必须以字符串 s 结尾。区间计数:需要统…...

Flink回撤流详解 代码实例

一、概念介绍 1. 回撤流的定义 在 Flink 中&#xff0c;回撤流主要出现在使用 Table API 或 SQL 进行聚合或更新操作时。对于那些结果并非单纯追加&#xff08;append-only&#xff09;的查询&#xff0c;Flink 会采用“回撤流”模式来表达更新。 回撤流的数据格式&#xff…...

学习笔记四——Rust 函数通俗入门

&#x1f980; Rust 函数通俗入门 &#x1f4d8; Rust 是一门语法精炼但设计严谨的系统级语言。本文围绕函数这一主线&#xff0c;带你真正搞懂 Rust 最关键的语法思想&#xff0c;包括表达式驱动、闭包捕获、Trait 限制、生命周期标注与所有权规则&#xff0c;每遇到一个新概念…...

使用 Spring Boot 和 Uniapp 搭建 NFC 读取系统

目录 一、NFC 技术原理大揭秘1.1 NFC 简介1.2 NFC 工作原理1.3 NFC 应用场景 二、Spring Boot 开发环境搭建2.1 创建 Spring Boot 项目2.2 项目基本配置 三、Spring Boot 读取 NFC 数据3.1 NFC 设备连接与初始化3.2 数据读取逻辑实现3.3 数据处理与存储 四、Uniapp 前端界面开发…...