为当天的活动做铺垫
正在进行游戏开发中的调试和视图功能开发。目标是增加一些新功能,使得在开发过程中能够有效地检查游戏行为。今天的重点是推进用户界面(UI)的开发,并且尝试在调试变量的管理上找到一个折中的解决方案。计划探索是否能够在运行时既实现完全的动态编译,又能即时更新调试变量,尽可能做到既有灵活性又能快速反应。虽然这个方法还不完全确定,但已考虑出一个可能的思路,今天将进行尝试。
目前的工作计划主要集中在继续完善UI功能上。这是今天的任务目标。
回顾我们上次的进展
我们之前实现了一个功能,就是通过一个简单的、看起来不太美观的径向菜单(radial menu),在游戏中能够进行调试和子集的切换,并且能在运行时动态地重新编译游戏。这意味着在当前的开发环境中,可以实时更改游戏的配置,而无需重启整个游戏。例如,现在可以看到,我们使用了一个小的渲染面板作为整个流的呈现。如果我们希望更改某些设置,比如调整调试选项(比如更改缓冲区大小),我们可以通过菜单选择该选项,点击后,游戏会立即重新编译并生效。更改后,游戏甚至不会丢失当前的状态,整个过程几乎是无缝的。
这种动态编译的能力很酷,通常人们会借助脚本语言来实现这种功能,但现在我们在游戏开发中实现了类似的效果,并且完全没有离开C语言。我们可以根据需要完全优化代码,同时还能够在不影响性能的情况下享受脚本语言的便利性。这种灵活性让我们可以在开发中轻松选择合适的工具和方法,提升了开发效率和便利性。
请求一些功能
如前所述,当前的径向菜单非常丑陋且不太实用,且功能也有限。如果将其扩展到一百个调试选项,不仅菜单会变得极其混乱,而且查找特定选项也会变得非常困难。因此,至少在这种情况下,我们可能需要考虑采用分层的设计,即使我们仍然坚持使用径向菜单,也应该允许先选择一个类别,再从中挑选具体选项。
个人而言,我更倾向于创建一个类似“按住右键”弹出的UI,像我之前提到的那样。这种设计可以让我快速访问调试选项,而且这些选项不会占用太多空间,始终保持在视图之外,只有在需要时才可以快速调出。这就是我选择使用径向菜单的原因——它能够快速提供我需要的功能,尤其是那些可能需要频繁开启和关闭的选项,比如性能分析信息等。
但是在进一步优化径向菜单之前,我打算先创建一个更加全面的UI系统,能够处理更大范围的调试选项和分类。我的设想是做一个带有滚动功能的层级树状视图,类似常见的树形菜单。这个界面可以通过右键点击激活,并且当前右键点击后,菜单只是瞬间出现并消失,我想把这个行为改成点击一次右键后菜单保持打开状态,直到用户交互完成。另外,当用户按住右键并松开时,可以通过特定的手势来选择菜单中的选项,从而实现既有快捷操作又不丢失功能性。
这种方式可以让我们既能享受高效、快速的调试选项切换,又能避免界面过于混乱,给调试带来更多便利。
创造一个新术语
目前,正在考虑一种新的方式来优化调试工具的功能,特别是通过调整界面和交互方式来提升使用体验。首先,我们希望实现一个功能,使得模拟器不再追踪主角或者某些物体的动作。这是为了避免一些不必要的追踪行为,使得系统运行更加灵活。
接下来,也希望优化地面块的处理方式,避免它们变得过于庞大或者占用太多资源。同时,为了更高效地进行调试,可能需要开启地面块的重新构建功能,这样可以在代码修改时即时刷新,确保调试数据的准确性。
在这些基础上,目标是设计一种新的用户界面,能让调试选项更加有序和易于访问。具体的想法是,通过在界面的一侧添加一个列表,该列表可以包含多个层次化的选项。比如,列表中可以有“渲染”、“模拟”等类别,这样就能够方便地根据类别快速找到需要的选项。即使选项很多,用户也能通过嵌套结构逐步筛选,减少信息量的混乱。
这种层级化的列表可以让用户在面对成千上万的调试选项时,能够快速而准确地找到需要调整的部分。接下来的步骤是设计一个类似于径向菜单的界面,用于实现这种新的层级式结构,使得调试过程变得更加高效、直观。
总体来说,目的是让调试工具更加功能全面,同时保持足够的灵活性,使得用户能够轻松应对大量的选项和设置,从而提升开发和测试的效率。
查看当前的调试代码
目前,调试菜单中正在使用一个径向菜单来显示所有调试变量,这个菜单会遍历所有在调试系统中的变量。这些调试变量是列在一个数组中的,目前该菜单会遍历并显示所有的调试变量。然而,这并不是我们希望的行为。
实际上,我们希望径向菜单能够只显示某些特定的调试变量,而不是显示所有的变量。因此,必须修改菜单的实现方式,让它不再遍历整个调试变量数组,而是只显示某些特定的调试选项。
调试变量列表本身并不需要是一个固定的数组,它可以是任何结构,只要能够在运行时被构造和使用。这些调试变量并不会由程序中的代码直接修改,而是通过一个配置文件来进行读取和写入。这个配置文件包含了程序运行时所需要的变量定义,它实际上是用来配置调试功能的。
目前,配置文件需要遵循一定的格式以便程序能够读取和写入,但是从程序实现角度来说,配置文件的内容可以更加灵活。可以考虑将其构造为一个动态生成的列表,而不仅仅是一个固定的数组。这样,调试变量就不再是静态的,而是可以根据运行时的需求进行动态构建。
例如,可以在程序启动时动态生成一个调试变量的层次结构,而不是仅仅创建一个平铺的列表。通过这种方式,可以实现更加灵活的调试界面,例如将调试变量以树形结构展示,这样就能更加方便地分类和管理大量的调试选项。
此外,C/C++等编程语言在数据输入和构建方面存在一些限制,特别是处理动态数据时可能会遇到困难。因此,考虑在运行时动态构建调试变量列表,能够避开这些限制,并提供更加灵活和高效的调试体验。
总之,目标是通过动态生成一个层次化的调试变量列表,提升调试界面的可用性和效率,避免目前径向菜单中将所有变量列出的问题。
game_debug_variables.h:引入DEBUGCreateVariables
为了改进调试变量的管理方式,计划创建一个新的函数来构建调试变量的层次结构,并将这些变量以更加结构化的方式组织起来。该函数将被命名为类似于“创建变量层次结构”的名字,目的是通过传入一个内存分配区域(如调试专用内存区域),来动态地创建和组织调试变量,而不是依赖于固定的静态数组。
具体来说,函数将使用传入的内存区域来动态分配内存,并构建调试变量的列表。通过这种方式,调试变量将不再是静态的,而是可以根据实际需求动态生成,便于管理和查找。
同时,调试变量的添加将不再通过宏来直接扩展到固定的数据定义,而是通过调用一个函数来添加调试变量。在这个新方法中,调试变量的添加将变得更加灵活,函数会接收变量的名称(可以是字符串)和实际的变量值,并将它们添加到调试系统中。这样做可以使调试变量的管理更加模块化和可扩展。
这种方法的优势在于,它允许调试变量的管理集中在一个地方,易于维护和扩展,也能够更加灵活地应对不同的调试需求。通过动态生成调试变量列表,可以方便地实现更复杂的调试界面,如层次化的树形结构展示调试信息,使得用户在处理大量调试变量时能更加高效地进行分类和查找。
#pragma once
#include "game_platform.h"
#include "game_asset.h"
enum debug_variable_type {
DebugVariableType_Boolean,
};
struct debug_variable {
debug_variable_type Type;
const char* Name;
bool32 Value;
};
debug_variable DebugVariableList[] = {};
#define DEBUG_VARIABLE_LISTING(Name) DEBUGAddVariable(#Name, Name)
internal void DEBUGCreateVariables(memory_arena* Arena) {
DEBUG_VARIABLE_LISTING(DEBUGUI_UseDebugCamera),
DEBUG_VARIABLE_LISTING(DEBUGUI_GroundChunkOutlines),
DEBUG_VARIABLE_LISTING(DEBUGUI_ParticleTest), DEBUG_VARIABLE_LISTING(DEBUGUI_ParticleGrid),
DEBUG_VARIABLE_LISTING(DEBUGUI_UseSpacesOutlines),
DEBUG_VARIABLE_LISTING(DEBUGUI_GroundChunkCheckerboards),
DEBUG_VARIABLE_LISTING(DEBUGUI_RecomputeGroundChunkOnEXEChange),
DEBUG_VARIABLE_LISTING(DEBUGUI_TestWeirdRrawBufferSize),
DEBUG_VARIABLE_LISTING(DEBUGUI_FamiliarFollowsHero),
DEBUG_VARIABLE_LISTING(DEBUGUI_ShowLightingSamples),
DEBUG_VARIABLE_LISTING(DEBUGUI_UseRoomBaseCamera),
}
game_debug_variables.h:实现创建变量组的功能
为了改进调试变量的管理系统,计划通过引入“开始分组”和“结束分组”的方式来组织变量。这将允许将调试变量按照类别进行分组,从而使得管理和查找变得更加方便。例如,可以创建一个分组来管理与摄像机相关的变量,另一个分组用于粒子效果,或者像地面块的计算等其他分组。这种方法将帮助避免在一个巨大的调试选项列表中搜索每个独立的变量,取而代之的是通过点击分组来快速定位到相关的调试选项。
每个分组都可以有多个调试变量,并且这些分组也可以是层次化的,允许将一个组放置在另一个组内部。这样可以创建一个更加复杂的层次结构,支持多级分组,使得用户可以在多层级中逐步找到所需要的调试变量。比如,在一个更复杂的层次结构中,可能会有“地面块”这一大类,然后在其中细分为“检查板”、“渲染计算”等子项。
此外,为了更好地管理这些变量,计划对调试变量进行动态处理,不再依赖固定的宏定义,而是通过调用函数来添加变量。通过这种方式,调试系统的扩展性将变得更强,并且能够适应更多的动态需求。每个变量的名称和对应的值都将被作为参数传入函数中,而不是硬编码在程序中。
为了实现这一目标,还计划引入更多的上下文管理。每次创建新分组或调试变量时,都需要记录这些变量所处的上下文信息,例如所属的分组、内存区域、变量的类型等。上下文信息对于跟踪调试变量的状态以及支持层次化分组至关重要。因此,在代码实现中,除了调试变量本身,还需要一个专门的调试状态和变量定义结构来管理这些信息。
总体来说,这一改进方案的核心目的是通过分组和层次结构的方式,提供一种更为清晰和高效的调试系统,能够支持更复杂的调试场景,并提供更好的可扩展性。
game_debug_variables.h:引入debug_variable_group和debug_group
为了实现调试变量分组的功能,需要设计一个数据结构来存储这些变量和分组。首先,定义了一个调试变量的类型,并且每个变量都包含了相关的类型信息。接下来,需要为这些变量提供一个组织结构,这就是“调试变量组”的概念。每个调试变量组可以包含多个子组,子组之间可以形成层次结构,允许变量和组之间相互嵌套。
调试变量组的设计需要能够支持多个层次的嵌套,意味着一个组可以包含多个子组,子组中还可以包含更多的子组。每个组需要包含以下几个信息:名称、子组、以及一个指向该组中第一个子项的指针。此外,调试变量组还可能需要跟踪其他状态信息,例如该组是否被展开,是否可以进行折叠等。这些状态信息将帮助调试工具在界面中以更直观的方式展示和管理这些组。
为了实现这个结构,调试变量和调试变量组的设计需要一个共通的元素,即每个变量或组都应该包含一个指针,指向下一个元素,形成链表结构。这种链表结构允许通过遍历的方式来访问和管理每个元素。
具体来说,每个调试变量可能会有一个指向下一个变量或组的指针,而每个调试变量组则可能会有一个指向第一个子项的指针。这样,能够实现遍历整个数据结构,从而便于对调试变量进行分类和展示。
此外,调试变量组可能还需要额外的信息,例如是否已被展开,当前的显示状态等。这些信息将帮助调试工具在图形界面上动态展示这些变量和组的结构,用户可以通过点击展开和收起组。
总结来说,核心目的是设计一个能够支持多层次分组的结构,其中每个组可以包含多个变量和其他子组。通过这种方式,可以方便地组织和管理大量的调试变量,使得调试过程更加高效和直观。
game_debug_variables.h:编写DEBUGBeginVariableGroup和DEBUGAddVariable
为了实现变量分组功能,首先需要在内存池(arena)上分配存储空间,并推入一个新的调试变量(debug variable)。由于所有变量类型(无论是布尔、浮点数还是分组)都会以调试变量的形式存储,因此创建变量的逻辑需要统一处理。具体而言,需要一个通用的函数来添加变量,这个函数应该接受变量名称、类型等参数,并在内存池中创建相应的变量对象,初始化其默认值,并返回该变量对象。
在创建变量分组(variable group)时,需要确保它被正确地组织到当前正在操作的变量组中。为了实现这一点,需要维护一个指向当前变量组的上下文(context),以便新变量可以被正确地添加到当前活跃的分组中。每次添加变量时,都应该检查当前是否有打开的变量组,如果有,则将变量添加到该组内。
然而,由于采用了单向链表结构(single linked list),默认情况下新添加的变量会被插入到链表头部,这样最终变量的顺序会与预期的顺序相反。因此,为了保持插入顺序,链表结构需要额外维护一个指向最后一个子节点的指针(last child pointer)。这样,每次添加新变量时,都会将其追加到列表的末尾,而不是插入到头部,从而保证变量的顺序与插入顺序一致。
具体实现逻辑如下:
-
变量的添加(Add Variable)
- 确保变量存储在当前的变量组中。
- 如果当前组为空(即没有任何子变量),则新变量既是第一个也是最后一个子节点。
- 如果当前组已存在子变量,则新变量应该追加到末尾,并更新最后一个子节点的指针,使其指向新变量,同时更新最后一个子节点的指针指向当前新变量。
- 这样可以确保变量的添加顺序不会颠倒。
-
维护链表结构
- 变量组维护一个
first_child
指针指向第一个子变量,同时维护一个last_child
指针指向最后一个子变量。 - 每次新变量加入时,都将
last_child->next
指向新变量,同时更新last_child
指向新变量。 - 这样可以保证变量组内部的变量顺序与插入顺序一致。
- 变量组维护一个
-
变量组的初始化(Begin Variable Group)
- 变量组本质上也是一个变量,因此它也通过
Add Variable
进行创建。 - 变量组默认设置为展开(expanded),并初始化
first_child
和last_child
为NULL
,表示该组还没有子变量。 - 当变量组被创建后,它会成为当前上下文中的活动变量组,后续添加的变量都会自动归入该组。
- 变量组本质上也是一个变量,因此它也通过
-
布尔变量的添加(Add Boolean Variable)
- 与普通变量类似,布尔变量也通过
Add Variable
进行创建,只是它的存储值是一个bool
类型(存储在value_bool32
字段中)。 - 添加完成后,布尔变量会被自动归入当前变量组,并按照正确顺序存储。
- 与普通变量类似,布尔变量也通过
这一结构允许轻松地创建层次化的调试变量组,并确保变量按照预期顺序存储,同时提供了灵活的扩展能力,例如未来可以添加更多变量类型或新的组织方式。通过维护 first_child
和 last_child
指针,能够高效地维护变量列表,避免顺序颠倒问题。
game_debug_variables.h:编写DEBUGEndVariableGroup
在结束变量组(End Group)时,需要执行以下操作:
-
确保当前存在一个有效的变量组
- 由于结束变量组的操作只适用于已经存在的组,因此必须先检查当前上下文中的
group
是否为NULL
。 - 通过
assert
语句确保group
不为空,防止错误操作,例如尝试结束一个不存在的组。
- 由于结束变量组的操作只适用于已经存在的组,因此必须先检查当前上下文中的
-
恢复上一级变量组
- 结束当前变量组的本质操作是**“弹出”**当前组,使其上一级的变量组成为新的当前组。
- 具体做法是将当前组的
parent
指针赋值给context->group
,这样变量组的层次关系就能正确恢复。 - 这一操作相当于“回溯”到上一级变量组,使后续的变量添加操作不会错误地归入已结束的组。
-
形成完整的层次化结构
- 通过这个逻辑,可以轻松构建一个层次化的变量组织系统,每个变量组都可以嵌套其他变量组或变量。
- 由于每个变量组在创建时都会存储
parent
指针,因此在结束组时可以顺利地回溯到正确的上一级组。 - 这一操作保证了整个调试变量系统的完整性,并且在代码上实现起来非常简单,仅需大约 30 行代码。
整个过程的核心思想是利用链式结构和上下文管理,在 begin group
时向下进入一个新的组,在 end group
时向上回溯到父级组,从而保持完整的层级关系。通过 first_child
和 last_child
指针维护变量的顺序,同时利用 parent
指针确保组的正确嵌套,最终形成稳定的变量管理系统。
game_debug_variables.h:创建debug_variable_definition_context
在创建调试变量系统的上下文(Context)时,需要进行以下几个关键步骤,以便顺利启动整个进程:
1. 初始化调试变量上下文(Context)
- 调试变量上下文是整个系统的核心数据结构,负责管理变量的层次关系、存储区域等。
- 需要初始化上下文中的各个关键变量,包括:
- 变量存储区域(Arena):用于动态分配和存储调试变量。
- 当前状态(State):管理调试变量的当前状态,以便追踪变量组的层级关系。
- 根变量组(Root Group):作为所有变量的起点,确保整个系统有一个统一的入口。
2. 创建根变量组(Root Group)
- 在系统初始化时,创建一个根变量组(Root Group),作为所有变量的父级容器。
- 这一做法的优势在于:
- 统一管理所有变量,避免零散分布的问题。
- 便于后续的层级管理,所有子变量组都会挂载到根变量组下,从而形成清晰的树状结构。
- 创建根变量组的方式是调用
BeginGroup
,并为其命名,例如root_group
,使其成为所有变量的起点。
3. 将根变量组设置为当前上下文的默认组
- 由于根变量组是最顶层的变量组,因此在初始化时,需要将
context->group
设为root_group
,以确保后续添加的变量都能正确归入其中。 - 这样可以保证:
- 当新变量被创建时,它们都会自动归属于当前活跃的变量组,而不会丢失或错位。
- 在需要管理多个层级的变量时,可以直接从根变量组开始遍历,无需额外维护复杂的索引。
4. 使用存储区域(Arena)进行变量分配
- 变量存储区域
Arena
负责分配调试变量,确保它们能够在整个调试过程中持续有效。 - 在代码实现中,每当创建新的调试变量时,都需要从
Arena
申请内存,以便存储变量的元数据和值。 - 通过
context->arena
进行分配,使得所有变量都能统一存储在可管理的区域中,提高内存管理的效率和可维护性。
5. 优化代码逻辑以减少重复代码
- 由于所有变量都会挂载到某个变量组中,因此可以统一调用
BeginGroup
来处理变量的添加,而不必为每种变量单独编写初始化逻辑。 - 这样可以:
- 避免冗余代码,提高代码的可读性和可维护性。
- 通过统一的 API 进行变量管理,确保所有变量都能按照既定规则进行组织。
- 例如,可以在
CreateDebugVariables
过程中直接调用BeginGroup
,以简化初始化流程。
6. 最终形成的完整结构
- 在完成上述步骤后,调试变量系统的基本结构如下:
- 根变量组(Root Group) 作为所有变量的起点。
- 变量存储区域(Arena) 负责动态分配变量内存。
- 变量上下文(Context) 维护当前变量的层级关系。
- 所有变量都会自动归入当前变量组,并通过
parent
指针形成树状结构。
- 这样就构建出了一个完整的调试变量管理系统,可以轻松支持多层嵌套的变量组,并提供高效的内存管理能力。
game_debug.cpp:调用DEBUGCreateVariables
在 game_debug
内部,当我们初始化 Arena
之后,紧接着就可以创建调试状态(DebugState
)及其相关的变量组和变量。以下是整个流程的详细说明:
1. 在 Arena
初始化后创建调试状态
Arena
负责分配和管理调试变量的存储空间,因此在Arena
创建完毕后,立即创建调试状态是合理的做法。- 通过
DebugState
统一管理所有的调试变量和变量组,确保它们能够正确地存储和检索。 - 需要包含
DebugState
相关的头文件,以便能够正确使用其中的定义。
2. 调整 debug_variable_group
的定义
- 发现
debug_variable_group
在代码中的引用出现了一些问题,主要是命名上的不一致。例如:- 变量组的名称在不同地方使用了不同的拼写,如
debug_variable
和debug_variable_group
,需要统一。
- 变量组的名称在不同地方使用了不同的拼写,如
- 解决方式:
- 统一变量组的名称,确保所有代码引用的都是同一个定义。
- 在
DebugState
头文件中正确包含debug_variable_group
的声明,以便所有相关代码都可以访问它。
3. 调整变量组的结构
- 由于
debug_variable
使用union
结构来存储不同类型的数据,需要确保:- 在访问变量组时,正确地引用
group
成员,而不是访问debug_variable
本身。 - 变量组的存储结构能够正确地管理变量的层级关系。
- 在访问变量组时,正确地引用
- 这种调整的主要目的是:
- 减少编译错误,确保变量组的定义能够正确地被访问和使用。
- 保持代码清晰,避免混淆
debug_variable
和debug_variable_group
的用途。
4. 改进函数的返回值
- 目前的
AddDebugVariable
、BeginVariableGroup
等函数并没有返回创建的变量或变量组。 - 但是,为了提高代码的可扩展性,可以改进这些函数,使它们返回创建的对象。这样:
- 调试工具可以在创建变量后,立即修改它们的属性,例如默认值、可视化选项等。
- 未来如果需要扩展
debug_variable
的功能,能够更灵活地管理变量对象。
- 解决方式:
- 调整
AddDebugVariable
,让它返回创建的变量对象。 - 调整
BeginVariableGroup
,让它返回创建的变量组。
- 调整
5. 修正语法错误
- 代码中存在一些拼写和语法错误,例如:
- 分号(
;
)缺失,导致编译错误。 - 字符串引号错误,导致解析问题。
- 括号匹配错误,影响代码逻辑。
- 分号(
- 解决方式:
- 逐步检查代码,并修正所有拼写和语法错误,确保编译通过。
6. 实现变量遍历以验证数据结构
- 由于
debug_variable
采用层级结构存储,因此在WriteGameConfig
时,需要遍历整个变量树,将其以正确的层级关系写入配置文件。 - 这部分代码的作用是:
- 验证变量创建逻辑是否正确。
- 确保所有变量都正确地归属于各自的变量组。
- 输出结构化的数据,以便后续调试。
- 具体做法:
- 递归遍历
debug_variable_group
,写出每个变量及其子变量组的数据。 - 在
WriteGameConfig
函数内实现遍历逻辑,并输出测试结果,以检查变量是否正确构建。
- 递归遍历
7. 最终测试和验证
- 在代码的最后部分,计划利用
WriteGameConfig
来测试调试变量的创建逻辑。 - 该测试的核心目的是:
- 确认所有变量都能正确存储。
- 确保层级结构正确构建。
- 通过遍历和写入,验证调试系统是否正常工作。
- 这部分测试非常关键,因为它可以直接反映
debug_variable
结构是否按照预期构建。
总结
整个过程的核心目标是构建完整的调试变量管理系统,并确保其能够正确地存储、组织和访问变量。具体步骤包括:
- 初始化
Arena
后立即创建DebugState
,并确保所有变量都归属于debug_variable_group
。 - 修正
debug_variable_group
的命名问题,统一变量管理的结构。 - 调整
debug_variable
结构,使其能正确处理变量和变量组的数据。 - 改进
AddDebugVariable
和BeginVariableGroup
的返回值,使其更加灵活可用。 - 修正语法错误,确保代码正确编译。
- 实现
WriteGameConfig
,遍历变量树并输出数据,以测试结构是否正确。 - 最终执行测试,确认所有变量和变量组的层级关系正确。
整个系统的代码量并不多,但结构非常清晰,能够很好地支持调试变量的管理和扩展。
game_debug.h:将debug_variable *RootGroup添加到debug_state
为了完成整个调试变量系统的结构,需要实现一个方法来获取根变量组(Root Group)。具体实现步骤如下:
1. 设计获取 Root Group
的方法
- 在调试系统中,所有变量都会组织在一个层级结构下,而最顶层的变量组就是 Root Group。
- 需要提供一种方法,让代码能够访问
Root Group
,以便进行遍历、管理和输出调试变量。 - 解决方案:
- 在
DebugState
结构中添加Root Group
的引用,确保所有变量都能从这个根节点开始访问。 - 在
DebugCreateVariables
函数内,确保Root Group
被正确地初始化并存储。
- 在
game_debug.cpp:引入WritegameConfig
调试变量系统的非递归遍历逻辑
为了优化变量的遍历方式,我们设计了一种 非递归 的方法来遍历整个调试变量树。相比传统的递归方式,这种方法减少了函数调用的开销,并且更易控制遍历逻辑。以下是具体实现步骤:
1. 目标:实现非递归遍历
- 传统递归遍历通常采用 深度优先遍历(DFS),每进入一个变量组(Group)就递归进入子变量组。
- 但我们希望改为 非递归遍历,避免深度递归带来的栈溢出问题。
- 通过 父指针(Parent Pointer) 和 兄弟指针(Sibling Pointer),可以模拟递归的回溯过程,从而实现无栈的遍历。
2. 遍历逻辑
(1) 从根变量组开始
- 从
RootGroup
开始遍历,进入它的第一个子变量。 - 如果变量是组(Group),进入其子变量。
- 如果变量不是组(Group),直接处理并进入下一个变量。
(2) 处理完子变量后,寻找下一个遍历目标
- 如果当前变量有兄弟变量(Next),则转向该兄弟变量。
- 如果没有兄弟变量,向上回溯到父变量,并寻找下一个兄弟变量。
- 如果回溯到
RootGroup
且没有兄弟变量,遍历结束。
3. 具体代码实现
debug_variable *Var = DebugState->RootGroup->Group.FirstChild;
while (Var) {
if (Var->Type == DebugVariableType_Boolean) {
At += _snprintf_s(At, (uint32)(End - At), (uint32)(End - At),
"#define %s %d //bool32\n", Var->Name, Var->Bool32);
}
debug_variable *Next = 0;
if (Var->Type == DebugVariableType_Group) {
Next = Var->Group.FirstChild;
} else {
Next = Var;
for (;;) {
if (Var->Next) {
Var = Var->Next;
break;
} else {
Var = Var->Parent;
}
Next = Var->Next;
}
}
}
4. 关键逻辑解析
(1) while (Var)
控制整个遍历过程
- 只要
Var
仍然指向一个有效变量,就继续遍历。
(2) if (Var->Type == VariableType_Group && Var->Group.FirstChild)
- 如果当前变量是 组(Group),并且存在子变量,则进入其子变量。
(3) Var = Var->Parent;
- 如果当前变量 没有兄弟变量:
- 回溯到父变量,寻找父变量的兄弟变量(
Parent->Next
)。
- 回溯到父变量,寻找父变量的兄弟变量(
(4) if (Var)
- 如果成功找到兄弟变量,则前往该兄弟变量继续遍历。
6. 可能的改进
- 如果在
ProcessVariable(Var);
过程中需要修改变量,可以增加状态标志位,标记哪些变量已经访问过,避免重复处理。 - 如果调试变量需要以特定顺序写入,可以增加额外的排序逻辑,确保变量按期望顺序输出。
7. 结论
该非递归遍历方法 高效且易维护,避免了递归调用导致的栈溢出问题,同时确保变量按正确的层级结构被遍历、处理。
先理解一下之前的代码现在有点蒙圈了
root
├── Chunks
│ ├── GroundChunkOutlines (Boolean)
│ ├── GroundChunkCheckerboards (Boolean)
│ └── RecomputeGroundChunkOnEXEChange (Boolean)
├── Particles
│ ├── ParticleTest (Boolean)
│ └── ParticleGrid (Boolean)
├── Renderer
│ ├── TestWeirdRrawBufferSize (Boolean)
│ ├── ShowLightingSamples (Boolean)
│ └── Camera
│ ├── UseDebugCamera (Boolean)
│ └── UseRoomBaseCamera (Boolean)
├── FamiliarFollowsHero (Boolean)
└── UseSpacesOutlines (Boolean)
game_debug.cpp:调用WritegameConfig
调用 WriteGameConfig
进行测试
为了验证整个 调试变量层级结构 是否正确构建并能正确遍历,我们决定直接调用 WriteGameConfig
并观察其输出结果。具体流程如下:
1. 直接调用 WriteGameConfig
- 通过 直接调用
WriteGameConfig
,我们可以测试遍历逻辑是否正常运作,以及最终输出的配置是否符合预期。 - 在调用前,我们会先 设置一个断点,确保能够逐步检查数据。
2. 具体测试步骤
(1) 设置断点
- 在
WriteGameConfig
入口处 手动设置断点,这样可以确保程序在执行该函数时暂停,方便我们检查变量的状态。
(2) 观察变量层级结构
- 检查变量层级,确认
RootGroup
是否正确初始化。 - 遍历变量树,确认
FirstChild
和Next
指针是否正确链接。
(3) 运行并分析结果
- 运行代码,并观察
WriteGameConfig
的输出是否符合预期。 - 如果输出不正确,则需要 检查变量的初始化和遍历逻辑。
3. 代码实现
void TestWriteGameConfig()
{
// 设置断点,确保可以检查 RootGroup
DebugVariable *RootGroup = GetDebugRootGroup();
if (RootGroup)
{
WriteGameConfig(RootGroup);
}
}
4. 预期输出
- 如果
RootGroup
初始化正确,则WriteGameConfig
应该能够顺利遍历变量并写入配置。 - 如果出现异常,可能是:
RootGroup
未正确设置。FirstChild
或Next
指针未正确链接。WriteGameConfig
内部逻辑有问题,导致输出错误。
5. 结论
通过直接调用 WriteGameConfig
并设置断点,可以方便地 验证变量层级结构是否正确,并确保最终的遍历和写入逻辑无误。这是调试整个变量系统的重要一步。
调试器:进入WritegameConfig
死循环了
测试变量层级结构的遍历和文件写入
我们现在进行 调试变量层级结构 的遍历测试,并观察其最终的 字符串累积 和 文件写入 结果。具体步骤如下:
1. 逐步遍历变量层级
我们从 根组(Root Group) 开始遍历:
- 根组本身不打印任何内容,因为它只是一个容器。
- 进入 第一个子节点,该节点是
GrandChunkOutlines
,类型是 布尔值,因此将其打印。 - 检查是否有 下一个兄弟节点,如果有,则继续遍历并打印。
2. 遍历过程
(1) 遍历第一个变量
GrandChunkOutlines
是布尔变量,被正确打印到字符串中。- 继续检查 下一个兄弟节点 并前进。
(2) 遍历第二个变量
Gunshot
是布尔变量,同样被正确打印。
(3) 遍历第三个变量
Experimental
是布尔变量,打印后 发现没有下一个兄弟节点。
(4) 回溯到父节点
- 因为当前节点没有
Next
指针,所以我们 回溯到父组。 - 检查父级是否有下一个兄弟组,如果有,则进入该组并遍历其子节点。
(5) 遍历第二个组
- 进入 新的一组(Group),发现它本身 不是可打印变量,因此不输出。
- 进入 该组的第一个子节点,如果是布尔变量,则打印。
3. 最终写入文件
(1) 生成的字符串
在遍历过程中,我们不断地 累积字符串,最终的输出如下:
GrandChunkOutlines = true
Gunshot = false
Experimental = true
...
(2) 生成文件
- 遍历完成后,系统将最终的 字符串内容写入文件。
- 打开文件检查写入结果,确认 变量层级被正确解析并输出。
4. 预期输出
文件最终应该包含所有 可打印变量,层级结构正确,内容如下:
GrandChunkOutlines = true
Gunshot = false
Experimental = true
...
如果文件格式正确,说明 遍历逻辑和文件写入逻辑均已正确实现。
5. 结论
- 层级遍历:正确地 递归/非递归遍历 变量树,并按照正确的顺序访问所有变量。
- 文件写入:成功 将遍历结果输出到文件,格式符合预期。
- 下一步优化:如果需要,可以 增加日志记录 或 可视化层级结构,以便更直观地查看解析结果。
整个调试过程验证了 变量系统的正确性,确保遍历和写入逻辑无误。
game_config.h:查看我们写出的文件
没写完
修正变量名称前缀缺失问题
在当前调试过程中,我们发现 变量名称 存在 前缀丢失 的问题。具体情况如下:
1. 问题描述
在遍历变量层级结构并写入文件时:
- 变量名称缺少
DebugUI_
前缀。 - 这部分在之前的处理逻辑中被 移除了,但在写回
GameConfig
时,需要 重新添加 该前缀。
2. 解决方案
(1) 修改 WriteGameConfig
逻辑
在写入 GameConfig
文件时:
- 重新添加
DebugUI_
前缀,确保变量名格式正确。 - 这样,所有变量在
GameConfig
文件中的格式将恢复为:DebugUI_GrandChunkOutlines = true DebugUI_Gunshot = false DebugUI_Experimental = true
(2) 具体实现
- 在 遍历变量 时,拼接
DebugUI_
到变量名称前。 - 确保 组(Group) 的层级结构 不受影响,仅针对变量名称进行调整。
- 测试文件写入结果,确保格式恢复正确。
3. 预期优化
- 变量名完整性:恢复
DebugUI_
前缀,确保变量名称符合GameConfig
解析要求。 - 无额外改动:不影响变量层级结构,仅修改名称输出逻辑。
- 可扩展性:如果未来需要调整 前缀规则,可以 增加配置选项,而不是硬编码
DebugUI_
。
4. 结论
- 已识别问题:变量名称缺少前缀。
- 已修正逻辑:在
WriteGameConfig
过程中 重新拼接前缀。 - 最终结果:变量正确写入
GameConfig
,并保留层级结构,保证调试系统的完整性。
game_debug.cpp:重新插入DEBUGUI_前缀到调试变量中
修正变量名称并优化层级结构
在当前的优化过程中,我们 重新插入 了 DebugUI_
前缀,并对整个变量组织方式进行了调整,使其更加清晰和易用。
1. 变量名称修正
之前,变量名称在处理时 去掉了 DebugUI_
前缀,导致最终写入 GameConfig
文件时格式不正确。
修正方法:
- 在 写入文件前 重新 拼接
DebugUI_
前缀,确保变量名符合预期格式。 - 这样,最终的
GameConfig
文件结构正确:DebugUI_GrandChunkOutlines = true DebugUI_Gunshot = false DebugUI_Experimental = true
- 这确保了变量名在 调试 UI 和
GameConfig
解析 时都可以正确识别。
2. 组织方式优化
(1) 层级结构调整
- 变量层级现在 更加有序,按照 组(Group) 进行归类。
- 这让数据 更清晰易读,也方便 后续拓展。
(2) 结构示例
目前变量组织方式:
Root
├── DebugUI_GrandChunkOutlines
├── DebugUI_Gunshot
├── DebugUI_Experimental
├── Group_1
│ ├── DebugUI_ShadowRendering
│ ├── DebugUI_AmbientOcclusion
├── Group_2
├── DebugUI_Lighting
├── DebugUI_Physics
- 层级清晰:所有变量都有明确的归属。
- 便于管理:未来如果需要增加新变量,只需放入相应的 组(Group) 中。
3. 代码简化
这部分的代码逻辑 非常简单:
- 遍历变量树结构
- 根据变量类型 进行不同处理:
- 组(Group):继续递归
- 普通变量:拼接
DebugUI_
前缀后写入文件
- 不影响核心逻辑,仅优化组织方式
4. 结果与优化
✅ 变量名称修正,恢复 DebugUI_
前缀
✅ 层级结构优化,数据更加清晰有序
✅ 代码简化,处理逻辑保持简单高效
✅ 更好的拓展性,未来可以轻松增加新变量或组
最终,我们的调试变量系统现在更加清晰、易用,并且便于扩展。
调试器:继续单步调试
在这部分,我们已经完成了所有的写入操作,并且确认一切都按预期工作。接下来要做的就是将调试相关的内容 从当前流程中移除,然后转向 下一步的操作。
总结流程:
- 写入操作完成:所有数据已经正确写入,不再需要进一步的修改。
- 移除调试信息:从调试的环境中移除相关的调试输出。
- 进入下一阶段:完成调试后,开始进入接下来的任务,确保流程顺利推进。
总的来说,现在已经完成了调试和数据写入部分,接下来可以继续进行其他的开发工作。
game_debug.cpp:回顾我们所做的工作
在这一部分,主要讨论了如何实现遍历操作,并提到了几种实现方式的选择。
核心思路和实现:
-
递归 vs 非递归:
- 传统上,遍历树结构时常使用递归,但在这次实现中选择了非递归方式。这是因为,在遍历树时,如果不使用栈来保存状态,就需要一个反向的指针来追溯路径。
- 由于已经有了父指针,实际操作中无需额外使用栈就能完成树的遍历。每当到达一个节点的末尾时,利用父指针可以回溯到上层,继续遍历其他节点,这样做既节省空间,又能简化代码。
-
父指针的优势:
- 父指针允许在没有栈空间的情况下依靠已知的当前节点继续遍历。如果当前节点没有下一个节点,那么可以通过父指针返回上一层,继续处理其他节点。
-
代码优化:
- 通过这种方式,避免了栈的使用,代码结构更加简洁,且不需要额外的存储空间,这使得代码实现更加高效且易于理解。
-
未来计划:
- 明天的工作计划是实现代码,允许打印出当前的树状结构,并且支持列表的展开与折叠,进一步优化对数据结构的展示。
总结来说,整个设计和实现采用了简洁的非递归方法,通过父指针来帮助树结构的遍历,避免了不必要的栈使用,这样不仅提高了效率,也简化了代码。接下来,计划进一步扩展功能,实现更直观的树状展示功能。
我们能否在调试菜单中添加一个选项,切换优化和调试版本?
目前,虽然无法立刻在调试菜单中实现切换优化和调试构建的功能,但这是可以轻松实现的。以下是几种可能的实现方式:
-
切换回单一翻译单元(Single Translation Unit):
- 目前的构建脚本没有重新编写,但可以回到使用单一翻译单元的方式,并通过
pragma optimize off
来关闭优化。这是目前想法中最合适的方式,因为之前的构建问题是因为没有正确处理pragma optimize
和内联函数(inline functions)周围的优化指令。通过这种方式,如果选择关闭优化,就能启用调试模式。 - 具体操作是,在调试模式下使用
pragma optimize off
,然后根据是否启用调试定义(debug define)来控制是否进行优化。这样做的好处是代码更清晰,且容易管理。
- 目前的构建脚本没有重新编写,但可以回到使用单一翻译单元的方式,并通过
-
使用构建包含文件(Build Include File):
- 另外一种方法是不改变当前的组织方式,而是通过设置环境变量的构建包含文件来实现。这种文件会定义一些额外的命令行选项,然后根据这些选项来选择是否开启调试模式或者优化模式。
- 通过这种方法,可以将编译的开关集中在一个地方,方便管理并且不会影响代码的结构。
最终,推荐的方式是回到使用单一翻译单元构建,因为这种方法对多个方面都有好处,包括简化构建过程和确保代码正确性。
如果在配置文件中根据组的深度进行缩进,视觉上会不会更好?这样我们就能知道哪些变量属于哪个组?(例如,根组中的变量没有缩进,深度为1的组有一层缩进,依此类推。)
对于是否将配置文件中的内容按组的深度进行缩进,以便更清晰地识别哪些变量属于哪个组,存在一些思考。
首先,通常情况下,我们不太关注是否需要以这种方式来显示文件内容,因为这并不会影响程序的执行或逻辑。如果确实想要做这种格式化输出,其实实现起来非常简单。我们只需要每次处理一个组时,增加一个“深度”值,深度值决定了缩进的层级。具体来说,可以在写入时根据当前的深度插入相应的缩进。
如果想要按深度调整缩进,具体方法是:每次进入一个新组时,深度增加;每次打印一个变量时,检查当前深度并打印相应数量的空格或制表符。这样就能确保变量在其所属的组下,缩进层级正确。
对于是否在每个组前加上注释,这也是可以选择做的事。每次遇到一个组时,可以打印该组的名称作为注释,帮助查看时清晰了解当前的组结构。比如在写入时加入一行注释,说明当前正处于哪个组的定义部分。
在实际操作中,还需要考虑是否在每次打印时都需要强制插入缩进,因为如果某些组不需要输出内容(例如仅作为结构存在),可能不需要缩进。如果选择始终打印缩进,那么代码的可读性会更强,但如果希望文件更简洁,则可以选择根据是否有内容来决定是否插入缩进。
总体来说,按组深度缩进并加上注释,会使得生成的配置文件更具可读性,尤其是在处理较复杂的结构时。
你使用的emacs有插件吗?是类似spacemacs的东西吗?
没有使用任何附加的功能或者插件,完全是原生的Amex(American Express)。没有做任何额外的自定义或扩展,只使用了基本的Amex功能,没有涉及像"space max"之类的特殊操作。
切换调试模式和优化模式时,不需要重新启动整个游戏吧?
在不同的优化模式之间切换时,应该不需要重启整个游戏。目前来看,没有明显的理由需要进行重启。从逻辑上讲,这种切换应该是可以在运行时完成的,而不会影响游戏的整体运行流程。
game_platform.h:实现来自论坛的建议,使SDL game能够在Linux和OS X上运行
有人在论坛上发布了关于在 Linux 上运行代码的问题和解决方案,其中涉及对 game_platform.h
文件中的某些方法进行实现,以使代码能够在 Linux 上正确编译。我们查看了相关的代码修改,并分析了所需的更改。
首先,我们发现了一些需要平台特定实现的原子操作,例如 AtomicCompareExchange
和 AtomicAdd
。在 Linux 平台上,这些操作可以通过 __sync_lock_test_and_set
和 __sync_fetch_and_add
来实现,而不需要额外的类型转换。此外,GetThreadID
的实现也依赖于不同的架构,例如 macOS、x86 和 x86_64,因此需要使用 #if defined
进行条件编译。
在 macOS 上,线程 ID 存储在 GS 段的基地址中,因此可以直接使用内联汇编(GAS 语法)来获取。而在 Linux 上,其存储位置有所不同,但依然可以通过汇编代码访问。此外,由于不同架构的调用约定不同,因此需要根据 CPU 类型选择正确的方式来获取线程 ID。
在修改的过程中,我们还发现 #if defined(APPLE)
需要与 #if defined(__x86_64__)
结合使用,否则会错误地匹配到 iOS 设备,而这些设备实际上不支持相关操作。因此,添加了额外的架构检查,以避免错误触发不支持的架构错误。
此外,还有一个关于 snprintf
的建议,即使用 snprintf
取代旧的 sprintf
,以避免潜在的安全漏洞。不过,目前仍然依赖 C 运行时库,因此后续可能需要进一步移除该依赖,以减少平台兼容性问题。
最后,在 Windows、Linux 和 macOS 上编译代码的最后一个调整是 time
函数需要进行 const
转换,以符合不同平台的编译要求。做完这些调整后,代码应该可以在多个平台上正确编译并运行。
你是否考虑过通过编译时加上“if(0)”来实现compile_switch / variable_switch速度优化?
讨论了编译器在切换变量时的行为,涉及编译优化的策略。其中提到,使用 Isidro 进行编译是其中的一部分,虽然方式可能稍显复杂,但整体思路是直观的,并不会涉及太过特殊或不寻常的做法。这个方法是自然发展的产物,而不是刻意设计的复杂方案,因此符合预期的逻辑流程。
为什么不使用Lua,难道你傻吗?
讨论了为何当前项目未使用 Lua 作为脚本语言,主要以讽刺的语气强调 Lua 的一些缺点,例如 难以调试、执行效率低,以及 缺少丰富的语言特性,这些因素会影响开发效率。如果按照这些标准来选择的话,理论上整个游戏都应该用 Lua 编写,因为这样能确保代码运行缓慢、难以排查问题,并且编写过程也不够流畅。
实际上,尝试使用 Lua 时遇到了困难,最终未能成功集成,因而放弃了这个选项,转而采用了其他方式。不过,若是更聪明的开发者,或许会选择完全用 Lua 来构建整个游戏。
你如何处理多线程问题?
多线程处理方式有多种,具体实现可以回顾之前的相关内容。整体而言,方法相对简单,没有采用过于复杂的方案。
你启发我放弃了多年的商业经验,去实现我自己的IMGUI,感谢你,我又开始喜欢编码了!
有人受到启发,决定放弃之前在商业领域的编程经验,转而投身游戏开发,并重新找回了对编程的热爱。这是非常令人高兴的消息。
你使用的是_snprintf还是sprintf?定义中提到“将snprintf改为_snprintf”。
讨论了在使用 printf
时的问题,发现可能无法使用 printf
,而是可以使用带有下划线的版本,比如 _printf
。但这似乎并没有对其他人有太大帮助,所以决定暂时不解决这个问题,表示可能会在下周再处理这个问题,因为这并不需要太多的工作。
你在这个项目中使用任何类型的文档或设计吗?你已经制作了哪些文档,计划制作哪些文档?
在这个项目中,关于文档的使用,提出了不喜欢用“文档”这个词来描述与游戏相关的内容,因为这个词常常让人联想到一些质量较差的游戏。尽管如此,确实有一些笔记本用于规划游戏设计方面的内容,但并不是那种会做成正式文档的人,也不是游戏设计师,所以不太适合深入讨论这类文档的制作。