游戏引擎学习第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,这意味着它们属于"临时"内存,每帧会被擦除;
  • 然而,一些变量(比如调试值)不应该每帧丢失,它们应该是长期存在的;
  • 我们还没有实现"每帧处理机制",所以暂时无法明确每帧该保留什么,清除什么;
  • 所以目前整个系统在"数据生命周期管理"上是残缺的。

临时解决方案

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

c 复制代码
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的操作顺序上来看,这些赋值操作不会造成任何不同。特别是在构造函数的例子中,如果使用构造函数,就无法直接看到发生了什么,需要去查看构造函数的实现,增加了理解的难度。

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

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

你喜欢什么类型的游戏?

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

相关推荐
FAREWELL00075几秒前
C#核心学习(十五)面向对象--关联知识点(1)命名空间
学习·c#·命名空间
IT猿手37 分钟前
动态多目标优化:基于可学习预测的动态多目标进化算法(DIP-DMOEA)求解CEC2018(DF1-DF14),提供MATLAB代码
学习·算法·matlab·动态多目标优化·动态多目标进化算法
Y1anoohh1 小时前
驱动学习专栏--写在前面
学习
小学生搞程序1 小时前
学习Python的优势体现在哪些方面?
开发语言·python·学习
李匠20242 小时前
C++学习之工厂模式-套接字通信
c++·学习
A林玖2 小时前
【学习笔记】服务器上使用 nbconvert 将 Jupyter Notebook 转换为 PDF
服务器·笔记·学习
ling__wx2 小时前
go学习记录(第一天)
学习·go
前端熊猫2 小时前
React Native (RN)的学习上手教程
学习·react native·react.js
M_chen_M3 小时前
es6学习02-let命令和const命令
前端·学习·es6
M_chen_M3 小时前
JS6(ES6)学习01-babel转码器
前端·学习·es6