游戏引擎学习第252天:允许编辑调试值
回顾并为今天的工作设定目标
我们处理了调试值(debug value)的编辑功能。我们希望实现可以在调试界面中编辑某些值,为此还需要做一些额外的工作。
我们的问题在于:当某个调试值被编辑时,我们需要把这个“编辑”的操作记录下来,以便调试系统后续在处理这些值时,知道它们已经被修改过,并可以将这些修改后的值写回去。
目前我们调试系统的界面已经运行起来了,所有的调试数据也能正确记录并展示出来,包括一个还没有实现的 profile 窗口。同时,一些控件也可以点击,但现在还不能编辑这些控件的值,点击之后没有任何反应,也不会高亮显示。
问题在于:虽然我们已经有了一整套调试数据输出系统,但是这些值只是某个帧(frame)中写出的数据的副本,而不是原始变量的引用。所以当我们想修改这些值时,调试系统并不知道应该把修改写回哪里,也就无法真正改变游戏内部的状态。
所以我们要做的是,在用户修改这些值时,我们要保存这个修改的状态,并确保当同一个值在之后的帧中再次被处理时,可以应用这个改动。
这就需要我们在调试系统中增加一个机制,让它知道每个调试值来自哪里,并能在需要时写回去。这个过程有点复杂,因为它涉及调试面板上的一个网格布局,这些调试值是以特定结构排列并显示的。
考虑如何实现调试值的可编辑性
我们需要做的是,使用调试界面上的网格系统进行查找,判断某个值是否存在待编辑状态。
我们已经开始着手处理这个问题,并在相关位置添加了一个 TODO
作为占位,表示后续需要处理。我们初步写了一段逻辑,用来表示接下来需要通过调用调试系统,将编辑请求传达出去,并判断是否当前正是执行编辑的适当时机。
在当前代码中,有一段叫做 DebugHandleValueEdit
的函数,它传递的是该值的地址。通过传递地址,我们可以在该值被输出到调试界面的那一刻,直接写入该值的新内容。
具体过程是这样的:当值被记录时,调试系统首先会记录当前值的状态,然后在合适的时机判断是否存在修改请求,如果有,就将修改写入该地址,从而完成实时覆盖。这样既能记录,又能动态修改。
不过我们现在写的方式可能还不够合理。因为如果要为每种可能的数据类型都写一个编辑函数,那调试系统的接口就会变得非常繁杂。可能会有更优雅的方式来处理这个问题。
考虑更通用的解决方法,比如使用一个包含各种数据类型的联合体(union),这样调试系统就可以只暴露一个统一的编辑接口。通过这个接口返回的联合体,我们可以将其中正确的值复制回目标变量,从而实现修改。
总之,我们在朝着一个更统一、结构更清晰的调试编辑系统方向前进,希望在保持功能完整的前提下,尽量减少接口的复杂性。
在 game_debug_interface.h 中,为 DEBUG_VALUE 添加 DEBUGEditEvent,使其能够设置新的事件值
为了更简洁地处理编辑事件,可以考虑使用一种方法来简化调用。在这种方法中,可以只定义一个统一的接口,例如 update_event
,这个接口接受一个调试名称作为参数,并且可以根据名称查找并获取相应的事件数据。
具体来说,可以在调试窗口中通过使用(GUID)来查找调试值。如果能够找到匹配的事件,就可以继续操作。这样,代码会更加清晰,避免多次调用不同的编辑函数。
我们可以通过调用 update_event
来更新调试事件。更新事件时,会根据已经传入的事件来覆盖原来的数据。这一过程无需为每种类型的值编写专门的编辑函数,而是通过统一的事件处理接口进行。这样,事件处理函数会根据事件本身进行必要的内部操作。
对于如何处理数据的具体类型,可以在事件处理函数内部进行适当的转换和写入。这意味着,编辑操作并不需要特定化每个类型的数据,而是通过统一的事件机制来处理。
此外,在事件更新过程中,还需要确保能够正确地将数据从合适的联合体(union)部分复制回来,确保数据能够被正确设置。这是必要的,因为我们需要将编辑的值在实际应用时恢复到正确的地方。
总结来说,创建一个统一的更新接口,将调试事件的编辑集中在一个地方处理,可以避免接口过于复杂,同时保证系统在编辑调试值时的简洁性和可维护性。
在 game_debug_interface.h 中,为 DEBUG_VALUE 添加 DEBUGValueGetEventData,并为所有需要的类型创建函数原型
在实现调试事件的编辑时,需要考虑如何高效地更新和处理事件的值。首先,在记录事件的值并将其直接写入调试缓冲区之后,接下来需要有一种方法来将事件的值设置为新的值。为了能够把这个值传回程序,需要写一行代码,其中包含正确的类型,并且确保这种操作不会增加不必要的复杂性。
为了实现这一目标,最有效的方式是重载调试事件的处理函数,创建一个新的函数来进行事件的设置操作。这个新的函数将允许将事件的值设置为新的值,且需要传递事件的地址,确保事件的内容被正确更新。这样一来,可以通过统一的函数来处理不同类型的事件,避免重复编写大量的冗余代码。
具体来说,可以通过一个类似 set_event_value
或 get_event_data
的函数来处理事件的值。通过传递事件的地址,可以让这个函数正确地修改事件的内容。虽然这种做法看起来可能会增加一些冗余代码,但它是确保能够灵活、统一地更新事件值的有效方式。
简而言之,创建一个统一的接口来更新事件的值,虽然会增加一些代码,但能避免重复的复杂操作,确保处理过程的简洁性和高效性。
在 game_debug_interface.h 中重新引入 DEBUGValueGetEventData,编译时出现预期中的错误,然后重写这些函数原型
在调试系统中,首先需要记录调试事件并设置事件数据。通过调用 record_debug_event
和 set_value_event_data
函数,可以开始处理事件数据。接下来,想要对事件进行修改,计划通过调用类似 debug_value_event_data
的函数,并将事件的值地址传递进去。然而,直接这样做并不能实际发生编辑操作,只是简单地重新分配值,但此时并没有进行实际的修改。
经过测试,发现这部分操作并未起到预期的效果,因此需要进行进一步的调整。例如,确保调试系统能够正确处理每个事件,并检查这些值是否有效。在调试过程中,发现一个问题:当打开多个缓冲区时,系统可能会出现混淆,导致无法确定正在编辑的内容。特别是当同一个文件被打开两次时,可能会覆盖之前的内容,这可能导致数据丢失。
为了解决这个问题,需要确保文件只会有一个副本被打开。如果尝试打开已打开的文件,系统应该采取额外的措施来防止这种情况发生,避免无意中覆盖工作内容。这意味着需要在代码中实现一个保护机制,以确保文件不会被错误地重新打开。
此外,还存在另一个 bug:在某些情况下,编辑操作未能完全替换值。这种情况只发生在特定的条件下(例如在处理特定类型的矩形时),并且其他场景下表现正常。这可能是由于字符串处理中的某个边缘情况所导致的错误,尽管在其他场景中一切都运作正常。
最后,在处理实体类型时,缺少了必要的转换步骤。此时需要补充这些遗漏的转换,以确保类型能够正确地处理和更新。
注意到 Entity->Type 无法转换,因为它是枚举类型
在处理 entity_type
类型的调试值时,遇到了类型转换的问题。编译器无法将某些类型正确地转换为可以编辑的形式。问题的根源在于 entity_type
是一个枚举类型(enum),而在调试系统中试图通过地址传递该值进行修改,但由于枚举类型本身的特性,这种方式并不被编译器接受。
之前之所以没有出问题,是因为传值和传地址的方式不同。在调试系统的初期实现中,是通过值传递的方式调用 debug_value
,这种方式在处理枚举类型时没有问题。但现在需要通过传地址的方式进行修改,以便在运行时更新值,这时编译器就无法自动推导枚举类型的地址转换。
尝试通过将参数设为引用(reference)来规避这个问题并不可行,因为 C++ 并不会更智能地处理枚举的引用类型传递,这并不会解决问题。
因此,这个问题本质上是由 C++ 对类型系统的严格处理导致的,特别是在需要通过地址来识别和修改调试值时。目前还没有特别理想的解决方案,除非为每个具体类型(包括枚举)专门写一个函数分支,或者显式地提供某种方式来将这些枚举类型包装为可以被调试系统识别和操作的类型。
此外,调试系统中的其他部分(例如渲染器、数据转换器)本身已经比较复杂和庞大,增加对枚举类型的特殊支持可能会引入额外的问题或不必要的复杂性。因此在设计时需要权衡是否值得为这些特殊类型添加额外处理逻辑。当前没有特别好的通用绕过方式,可能最终仍需要为 entity_type
这类类型单独实现一个专用的处理函数来支持调试系统中的编辑操作。
在 game_debug_interface.h 中修改 DEBUGValueGetEventData 以接受一个忽略的值
在当前处理过程中,面对 C++ 类型系统带来的限制,采取了一种略显荒谬但可行的解决方案,来应对调试系统中对于不同数据类型编辑能力的需求。具体情况如下:
尝试通过传值与传址结合的方式解决问题——即在函数调用中同时传递一个值(作为来源)和一个地址(作为目标),但函数内部实际上忽略值参数,仅通过目标地址完成数据的写入。这种方式虽然语义上并不优雅,甚至显得有些可笑,但在 C++ 语言现有机制下却是一种实用且高效的权宜之计。
为了实现这一方案,代码中使用了占位参数名(如 ignored
)来标明这些参数并不会被真正使用,仅作为形式参数存在。实际的数据写入操作通过强制类型转换(如 (int32 *)
)将目标地址转换为需要的类型指针,并进行解引用赋值。
这种方式绕开了类型推导和模板系统对类型一致性的严格检查,允许在需要时对类型不匹配但结构兼容的变量进行处理。尽管这种做法违反了类型安全性原则,但在调试系统这种工具性代码中是可以接受的。
在实现过程中也对多个具体类型进行了适配,比如 int32
、vector2
、rectangle2
、sound_id
等等,分别做了类似的类型处理,并确保编译通过。
虽然从结构上看这是一种不优雅的 hack,但确实解决了核心问题:如何在调试系统中安全地修改各种不同类型的数据值,尤其是当这些类型在编译期并不能轻易统一处理时。最终的实现结果令人满意,也展现了在 C++ 复杂类型系统限制下灵活应对问题的能力。
此外还调侃了一下 rectangle2
类型的处理方式,表现出对于冗长调试过程中的一丝幽默感,即使过程枯燥,仍保有自我调节的空间。整体而言,虽然过程技术上曲折,但达到了调试系统编辑值的初步目标。
在 game_debug_interface.h 中为 DEBUG_VALUE 和 DEBUG_B32 添加 DEBUGEditEventData,用于更新事件值
我们现在已经实现了调试数据的读取和写入功能,接下来只需要在这两者之间插入关键的调用逻辑,也就是我们说的“分叉”处理部分。这一调用就是 DebugEditEventData
,它接收当前的调试名称(也就是网格 ID 或 hash 值)以及对应的调试事件,并在我们将值写回内存之前提供一个机会来更新这个事件。
这个过程的逻辑非常简单清晰:
- 我们在读取事件值之后,写回值之前调用
DebugEditEventData
。 - 传入当前事件的名称(通过 hash 得到),和事件结构本身。
- 如果有编辑请求,此函数将处理更新;如果没有,则什么也不做。
- 所以对于像布尔值这样的情形也一样:
- 在读取布尔值之后、
- 写入之前,
- 调用
DebugEditEventData
。
这意味着调用逻辑在不同类型的数据中是统一的,只要参数格式一致就行。
这样做之后,唯一剩下的工作就是在 debug.cpp
中真正去定义 DebugEditEventData
函数。该函数的职责就是处理 hash 查询,即通过给定的名字查找当前调试事件是否存在修改记录:
- 首先会使用类似
GetElementFor
的方法来查找当前名称对应的事件。 - 如果查到有更新记录,就将其拷贝到当前事件中(实现“编辑”效果)。
- 如果查不到,就什么都不做。
这部分逻辑其实就是前面实现 GetElementFor
那套 hash 查询逻辑的延伸。
因此:
- 插入
DebugEditEventData
调用的位置已明确。 - 参数是 hash 名称(调试名)和事件结构。
- 函数本体只需在 debug 系统中使用已有哈希表结构完成一次查询和一次值拷贝。
总结就是,我们的调试系统现在具备了完整的链路:读取 → 可选编辑 → 写入,通过对调试事件进行集中式拦截和编辑,实现运行时交互修改的能力。剩下的任务就是具体实现该编辑函数的逻辑并挂接到现有 hash 系统上。
在 game_debug.cpp 中思考是否让 GetElementFromEvent 写入指向 debug 元素的指针
我们现在的需求是在调试系统中编辑事件数据,而真正关键的部分是通过事件名称解析出其对应的哈希值(也就是 GetElementFromEvent
的逻辑),因为这个哈希值就是我们查找、识别调试元素的核心依据。
既然在获取调试元素的时候已经完成了名称解析、哈希计算和事件查找的全部步骤,那我们在随后的调试阶段就不必重复这一操作。也就是说,如果我们已经拿到了元素指针或者哈希值,就可以把它缓存下来、写入,避免再次计算。这带来了两个思路:
-
提前存储哈希值或元素指针
- 如果我们已经通过名称拿到了对应元素(如通过 hash 查表),我们就可以只保存该元素的指针或哈希值。
- 这意味着之后进行写入或编辑时,不再需要名称解析步骤,从而提高效率。
- 例如,我们可以在第一次事件处理时就将哈希值保存起来,后续使用它快速查找。
-
为何还需要再次进行查找?
- 因为在更新事件值之前,我们必须知道是否存在用户修改过的值。
- 这一判断的前提仍然是通过哈希或名称查找当前事件。
- 所以不管怎样,在某个阶段必须完成一次表查找。既然如此,最好复用已完成的查找结果。
然而,也存在一些权衡和技术限制:
- 如果我们选择存储的是指针,那这些指针就必须是稳定的,也就是说指向的调试元素不能被释放或重分配。
- 这对于临时对象或生命周期不明确的调试值是危险的,比如某些结构体字段,或者某些动态事件。
- 不稳定的指针可能导致崩溃或未定义行为。
考虑到这些限制,我们可以采用以下策略:
- 只允许编辑那些能保证稳定地址的数据(如静态布尔变量或全局状态),这样指针缓存是安全的。
- 对于非稳定值,可以选择不支持编辑,或者继续采用哈希+名称方式重新查找。
另外一点考虑是性能:
- 所有调试编辑行为都是为开发服务的,在产品发布或性能敏感的路径中我们可以关闭调试系统。
- 比如可以让数据块决定是否启用事件记录,如果禁用就不写入调试值。
因此我们的最终结论是:
- 可以缓存调试元素的哈希值甚至指针,以提高效率。
- 但缓存指针的前提是这些值具有稳定地址,否则必须继续通过哈希查找。
- 调试系统开销可以通过条件开关控制,不影响最终产品性能。
- 在实际实现时,应该让数据结构判断是否需要记录事件,以避免不必要的成本和混乱。
在 game_debug.cpp 中实现 DEBUGEditEventData
我们决定尝试实现这个功能,看看性能是否真的有问题。考虑到调用次数不多,理论上影响不大,但仍旧令人担忧。我们更倾向于调试系统只是简单地将信息写入缓冲区,而不是执行大量逻辑计算。频繁地做复杂操作,尤其是在调试阶段,是不太理想的。
我们的目标是实现一个 DebugEditEventData
函数,它接收调试(GUID)和事件(Event),然后尝试在已存在的事件数据中进行编辑操作。这个函数的核心是获取事件的名称哈希值,然后通过哈希值查找对应的调试元素。
我们提取了部分已有代码的逻辑,将哈希计算部分抽出来以便复用,并实现了一个使用哈希值来直接查找调试元素的逻辑。此方式可以避免重复解析字符串,提高效率。这个函数只在我们已经获得名称哈希的前提下调用。
在调用过程中需要获取调试系统的全局状态,然而并非所有时候都能保证这个状态存在,例如程序刚启动时。因此我们必须处理好可能为 null
的情况,不能假设调试状态总是存在。
令人担忧的一点是,当前这个查找操作仍然比较昂贵:我们不仅要查找调试状态,还要对比字符串、解析名称、比对哈希值。这些操作非常消耗性能。虽然我们已经优化了部分流程,但仍旧不是理想状态。
从某种角度来看,这将一个本可以无忧调用的调试系统,变成了一个必须小心使用的组件,这种转变令人不安。我们也曾考虑过在事件里直接存储指针,这样就可以避免查找开销,但指针可能在某些情况下变得无效,因此这不是一个稳定方案。
最理想的方式是能够在事件中记录永久性的、可直接索引的数据,就像我们之前对计数器那样。但由于存在多个翻译单元(translation unit)的问题,我们无法实现完全硬编码的方式。因此,我们只能在 C++ 的限制下尽力而为,做出“最不坏”的设计决策。
接下来,如果成功找到调试元素,我们就可以安全地替换事件中对应的值,比如一个布尔变量,并记录“值已被编辑”的状态。为了避免多次覆盖,我们在检测到一次编辑后,就阻止其他地方再次尝试修改该值。这可以通过一个“值已编辑”的标志来实现,只有在该标志为 true
且存在最近一次事件时才进行替换。
这段逻辑的另一个挑战在于调试接口的调用关系:平台层想要记录调试值,却无法直接调用 DebugEditEventData
,因为这个函数定义在游戏层。平台层可以写缓冲区,却无法访问游戏层函数。这是架构设计中的一个限制,虽然可以通过一些补丁方式绕过,但这种依赖仍让人不满。
我们最终认识到,在现有 C++ 条件下,很难实现完全理想的设计方案。我们只能尽力保证功能完整、性能可控,并通过合理的接口封装、条件判断等方式,尽量降低调用成本和错误风险。这就是我们当前调试系统设计的折中方案。
在 game_debug.cpp 中移除 DEBUGEditEventData,并简化系统逻辑,仅检查 GUID 指针是否匹配
我们在重构调试系统的编辑流程,目标是极大地简化调试值的设置和管理逻辑,并提升其性能和灵活性。
首先,我们意识到调试系统实际使用中,大多数时候只会编辑一个值,几乎不会同时修改多个值。即便存在多个值的情况,也完全可以只针对其中一个进行操作。因此我们不再需要为每个可能被编辑的值分别存储状态信息,也不需要在事件哈希表或其他结构中查找、匹配等繁琐过程。我们只需设立一个全局指针,指向当前正在编辑的调试事件即可。
这个全局指针(称为debug_global_edit_event
)是唯一且共享的,用于记录正在被编辑的事件。我们只需在处理时对比事件指针本身是否相等,不再需要比对字符串或哈希值,从而极大地简化逻辑与提升效率。
在这种机制下,我们可以做到以下几点:
- 不再需要单独的“设置编辑数据”函数,而是统一使用一个如
debug_value_set_event_data
的函数来进行赋值操作; - 在进行数据设置时,只需判断当前事件是否为正在被编辑的事件,如果是,则从事件中读取用户输入的值并覆盖到目标变量;
- 否则就从原始数据源写入到事件中(以供用户查看);
- 编辑状态仅在当前帧内有效,不需要在帧之间保留,因此值可以是临时存在的,比如模拟实体内部临时展开的数据结构,也可以被编辑;
- 编辑值的类型处理也被统一封装,减少了重复代码,所有类型(如
u32
、r32
、b32
等)都通过统一的宏来注册其处理逻辑; - 由于只有一个正在被编辑的事件指针,所以所有设置函数都可以宏展开,大大减少冗余代码。
另外,为了实现这个机制,我们还将debug_global_edit_event
的定义放入调试系统的共享区域中,使平台层可以通过设置该指针来告诉调试系统当前正在编辑哪个事件。这与global_debug_table
的处理方式一致,因此可以顺利地将其集成在已有结构中,比如直接在debug_table
中添加该指针字段,作为首项。
通过以上设计,我们实现了一个几乎无成本的调试编辑机制,能够灵活支持临时值和即时编辑,逻辑清晰,结构简洁。这种方式虽然一开始看起来比较“疯狂”,但实际上非常高效且优雅。我们不再需要为调试编辑维护复杂的数据结构、状态同步或生命周期管理,只依靠一个简单的全局指针,即可完成全部调试编辑逻辑。
「这有点像指针跳转」
我们在设计中引入了一个全局指针,它本身虽然是一个间接引用(pointer trace),但并不会带来性能问题,原因是:
这个全局指针只是跳转一次,指向正在被编辑的事件结构体。而从事件结构体中再获取我们需要的数据(比如event->value
)时,这一步是直接通过结构体的字段访问完成的,也就是所谓的“硬引用”(hard reference),它不会再经过额外的指针跳转。
所以整个流程中只有一次指针跳转,是完全可以接受的。在大多数架构中,这样的访问方式并不会引起缓存失效或显著性能下降。
而且这个指针是挂在global_debug_table
里的,而这个表在所有系统中本来就已经被管理得很好,生命周期、可见性、访问权限等都已经有完整的机制,不需要我们重新构建一套系统去管理编辑状态。
通过这种方式,我们既保留了结构上的简洁,又避免了频繁指针解引用带来的复杂性,同时借助已有的global_debug_table
实现了全局一致性,不会产生额外维护负担。整体来看,这个设计在可控的范围内引入了最小的复杂度,却带来了最大程度的灵活性与效率。
运行游戏,注意值还不能编辑,但整体进展良好
我们目前的系统状态已经相当令人满意。现在没有任何值会被修改,是因为当前还没有设置任何对应的事件,这也正是我们预期的行为。系统已经处于一个良好的基础结构上——只要设置一个事件,编辑操作就会自动发生,这种机制的建立意味着我们已经不再需要处理那些冗余的修改路径。
接下来的关键就是在合适的时机设置这个事件。一旦设置了,就可以触发实际的值修改,从而实现动态调试与交互式控制的功能。
例如我们现在如果进入 game.cpp
(或名为类似“game”的调试入口点),只需要调用一个设置事件的操作,那么编辑逻辑就会如预期地启动。
下一步是寻找具体代码中哪个位置负责变量的“自动修改”逻辑,比如 auto_modify_variable
相关的部分。通过进入对应函数,我们可以直接插入或验证事件的设置流程,进一步验证这个编辑系统是否生效。
我们通过这种方式完成了调试系统的核心功能:只需设置一次事件,全局即可感知、判断并应用对应的值修改。整体结构简化、维护成本降低、调试能力增强,所有这些都汇聚到一个高效的设计理念上。
在 game_debug.cpp 中复制 *Event 的 DEBUGEndInteract 并引入 OriginalGUID 的概念
我们目前正在调整调试系统的变量修改逻辑,目标是通过设置一个全局的 edit_event
来实现对调试值的直接编辑。
在 modify_variable
或类似函数中,例如 toggle_value
,它直接修改事件结构体中的值。现在,我们的想法是:只要将当前事件赋值给 global_debug_table->edit_event
,就可以实现对这个值的编辑操作。
然而,这里有一个关键的注意事项:我们必须保留事件原始的字符串指针(GUID
),用于后续的比较操作。因为调试系统的其它部分不会重新复制或保存这个字符串,而是一直依赖于它保持不变。这个 GUID
对象是整个调试系统识别和定位事件的关键,尤其是在判断是否当前正在编辑某个变量时,是通过指针比对来实现的。
为了解决这个问题,我们在调试元素(debug_element
)中新增了一个字段 OriginalGUID
,用于保存事件的原始 GUID
字符串指针。这个字段不会被打印或显示,因为我们不能保证它指向的内存一定是有效的,特别是在 DLL 被重新加载后,这些原始指针可能会变得无效。
因此,每次我们处理新的调试事件,尤其是在从事件获取元素的阶段,就要将当前事件中的 GUID
复制或赋值给对应元素的 OriginalGUID
。换句话说,只要有人调用了 get_element_from_event
,我们就需要更新一次这个 OriginalGUID
字段,以便后续能够将其恢复给 edit_event
。
整个逻辑流程如下:
- 每当我们触发一个变量修改(如通过 UI 或控制台),只需设置
global_debug_table->edit_event = event
。 - 为保证正确比较,我们将原始
GUID
保存在元素中,以便之后重新赋值回事件。 - 每次从事件获取调试元素时(例如调用
get_element_from_event
),都要更新OriginalGUID
。 - 系统后续只需比较指针是否相等来判断是否处于编辑状态,无需进行额外的字符串解析或比较。
通过这种方式,我们保持了编辑机制的简洁性和效率,同时确保系统在跨帧、跨 DLL 载入的情形下仍能保持稳定运行。这种机制对调试时编辑值、保持事件唯一性以及系统一致性起到了决定性作用。
在 game_debug.cpp 中让 GetElementFromEvent 存储 OriginalGUID
每次调用 get_element_from_event
时,我们需要覆盖元素的 OriginalGUID
字段,将其赋值为当前事件的 GUID
。这样可以确保每次处理事件时,都能保存原始的 GUID
指针,保持对事件的稳定引用。
为了便于管理和减少重复,计划将这个逻辑上移到更高的层级,因为其他地方也可能需要使用这个原始 GUID
指针。这样做之后,可以简化当前的调试系统,尤其是去除一些冗余的字段和结构。例如,现在 debug_ids
等字段显得多余,因为它们是系统历史中多次尝试的产物,已经没有存在的必要。
通过这种方式,原本可能不必要的部分将被清理掉,使调试系统变得更加简洁和高效。进一步地,这个变动能减少代码的复杂性,避免维护多个冗余路径,从而确保系统更稳定、更易于扩展。
在调试器中给 DEBUGEndInteract 设置断点,确认确实命中了
bool32 认为是int32所以DEBUGValueSetEventData后面在赋值类型
保持GUID不变,不修改GUID解析名字
在当前的调试过程中,需要确认是否完成了所有必要的工作。由于不确定编辑相关的代码是否正确触发,首先设置了一个断点来检查是否进入了相应的编辑流程。在调试中,观察到事件的值已经成功被设置为0,且调试系统的相关事件也确实被覆盖,但问题出现在没有正确更新 GUID
值。
具体来说,发现系统并没有成功覆盖全局调试表中的 GUID
,尽管在调试日志中看到事件的 GUID
和解析后的 GUID
被记录下来,但它们并不是预期中的正确值。可能是因为在代码的处理流程中,事件本身的 GUID
并未被正确更新或覆盖。
于是,计划进一步调试和修正这个问题,推测可能需要手动更新或设置 original_GUID
值,以确保覆盖过程中能正确更新。这一步骤可能是必须的,以便确保在更新和编辑过程中,相关的 GUID
值能够稳定有效地反映在调试系统中。接下来需要检查并修正 GUID
值的更新过程,以解决当前的覆盖失败问题。
在调试器中调查为何在编辑调试元素后 OriginalGUID 没有被保留
目前我们希望在今天完成调试系统的这一部分功能,因此暂停其他排队任务以集中处理这个问题。当前系统的行为已经非常接近预期,只差最后一步的修复。
我们正在检查 get_element_from_event
函数的行为,以确认其是否按照设想工作。检查发现,该函数确实成功设置了我们期望的 OriginalGUID
,这是个好迹象,说明我们获取事件元素时能够正确保留原始网格信息。
但问题出现在后续某个阶段,当我们尝试访问调试元素时,实际获取到的 GUID并不是我们期望中保留的那个原始值。因此我们进一步追踪,想要搞清楚为什么调试元素中并没有正确地保留
OriginalGUID` 的值。
也就是说,虽然我们已经在获取事件元素时设定好了 OriginalGUID
,但在之后用于调试或编辑时,并没有成功地从该值中取出正确的内容。这种不一致性可能说明在调试元素创建或引用过程中,某些地方仍然使用了错误或旧的指针。
我们现在的目标是查明为什么调试元素没有拿到更新后的 OriginalGUID
,可能的原因包括指针没有正确传递、被覆盖、或者某段逻辑绕开了我们设置的值。接下来的工作就是聚焦于这个流程的具体实现,找到这段断裂的链路,并修复它,确保调试系统能可靠地使用最新的网格数据。
恍然大悟:GetElementFromEvent 会自己重写 GUID
目前问题的关键在于对 OriginalGUID
的赋值时机出现了不恰当的覆盖。我们意识到,get_element_from_event
函数自身会设置或覆盖 OriginalGUID
,但这并不总是我们想要的行为。
因为我们只希望在真正从协作系统(coalition)中获取元素时才覆盖 OriginalGUID
,而不是每次调用 get_element_from_event
时都做这件事。当前实现中,这个函数会在某些路径中自动执行覆盖操作,而我们仅在特定情况下(例如通过协作系统获取时)才需要进行这样的赋值。
进一步分析发现,这种覆盖操作仅出现在某两个具体分支逻辑中,而这两个分支本身并不支持编辑功能,因此问题出在调用路径与赋值逻辑之间的误配。当前对 OriginalGUID
的设置是在不支持编辑的路径中执行的,因此当真正支持编辑的路径走过来时,值可能已被错误的内容覆盖或未能按时设定。
接下来我们要做的是:
- 调整逻辑,不让
get_element_from_event
在所有路径中都设置OriginalGUID
。 - 仅在真正从协作系统中取得可编辑内容时,再去设置这个值。
- 保证之后用于调试或编辑的数据结构始终保留正确的原始网格指针,避免误引用或空引用。
这样,我们就能确保调试系统的编辑路径只在合适时机设定 OriginalGUID
,不会被其他流程提前覆盖,从而让调试操作的数据引用保持一致性和正确性。
在 game_debug.cpp 的 CollateDebugRecords 中设置 OriginalGUID
我们目前采用了一种更直接的处理方式:暂时将 OriginalGUID
设置为事件中提供的 liquid
值,以便调试记录在存储事件时,能够将该信息正确地存入调试元素中。换句话说,现在是在执行 store_event
时,将事件中的 liquid
设置为元素的 OriginalGUID
。
这可能不是最终最完善的解决方案,但目前看起来逻辑上是合理的,因此我们尝试继续验证这一机制。
然而,在测试时发现,尽管进行了上述设置,我们依然没有成功获得正确的 GUID 值。当前的 OriginalGUID
仍未按预期设置成功。我们推测这可能与调用顺序、赋值时机,或编译未及时生效有关。
下一步是等待重新编译完成后再次尝试验证 OriginalGUID
的值是否被正确赋予。如果依然失败,我们可能需要进一步深入查看数据流与生命周期是否匹配,以及是否存在赋值被覆盖或遗漏的逻辑缺陷。
目前核心逻辑如下:
- 初始阶段临时将
OriginalGUID = event.OriginalGUID
- 真正设置发生在
store_event
函数中,将其传入调试元素 - 后续验证是否能顺利传递并保持该值
此策略目标是确保调试元素在记录和编辑过程中始终能保持正确且稳定的原始网格指针。我们将在编译完成后再次测试其有效性。
目前在执行运行和切换操作时,我们尝试从事件中获取对应的调试元素。然而系统未能正确找到目标,原因是当前无法得知原始值。这本身并不构成严重问题,主要是由于可用于处理问题的时间有限,而当前系统较为复杂,在这种受限时间下推进难度较高。
我们现在只剩下一个关键点需要整理完成,即需要同时追踪两个信息:
- 该元素的“通用名称”;
- 该元素的“特殊标识形式”。
这两个信息都可能用于唯一确定调试元素,目前的问题在于,我们需要通过这两种方式之一确定要编辑的具体元素。
考虑到这一点,我们提出两种可能的解决策略:
- 一种是避免从事件中查找调试元素,改为在执行交互操作(interaction)时直接将调试元素本体传入,而不是事件引用。
- 另一种更理想的方式是继续在事件中保留对原始 GUID 的指针。因为无论如何,这个原始 GUID 信息在后续逻辑中是有用且必要的,能够帮助我们回溯或验证当前编辑对象的准确性。
因此,倾向于保留这种从事件中获取 original_GUID
的机制,不通过额外查找逻辑进行追踪,而是确保事件中总能保留和传递该 GUID 信息,这样更简洁也更可靠。
我们下一步将致力于在不破坏调试系统其他部分结构的前提下,优化这一逻辑,使其既能精确定位调试目标,又便于维护、可读和拓展。
在 game_debug.cpp 中停止在 StoreEvent 中重写 GUID
我们目前在处理 store event
时的逻辑是,在存储事件的过程中,会将 GUID 的值重写为 event.get_name()
的结果。现在我们决定不再继续使用这种做法,而是选择将其推迟到“展示阶段”或其他类似的操作时再处理。
也就是说,在真正执行展示或交互操作之前,不再预先重写 GUID 内容,而是保留事件中的原始 GUID 值。这样做的好处是:
- 在之后需要查找调试元素时,能够准确获取到其原始值;
- 能够避免因为重写而失去对调试数据源的真实映射;
- 有利于保持事件数据结构的完整性和一致性,减少调试时的不确定因素。
简而言之,我们不再提前干扰 GUID,而是在真正需要展示或对比时才进行转换或使用,这样在查找值时才能正确获取目标,正如当前这个例子中希望达到的效果一样。
我们接下来会按照这个思路去处理整个调试系统的事件存储和展示逻辑。
运行游戏,发现现在可以正常编辑这些值了
现在这个方案已经可以正常运行,我们已经能够对这些值进行编辑。
目前的编辑功能只支持布尔类型的值,对于非布尔类型的值,我们还没有实现对应的编辑器。因此,那些非布尔类型的值在界面上不会被识别为布尔值,也就无法进行编辑。实际上我们目前还没有实现对其他类型(比如整数、字符串等)数值的编辑功能。
也就是说,当前系统已经实现了布尔值的可编辑功能,并且行为正确;但其他类型的值目前还不支持编辑,接下来需要扩展编辑器功能以覆盖更多数据类型,才能实现完整的调试交互体验。
运行游戏,发现部分纹理加载失败,但所有布尔值现在都可以编辑了
出现了一个问题:图形驱动在这次运行中对我们的纹理处理表现得不太稳定,可能引发了渲染故障或程序崩溃。从当前情况来看,这种结果并不令人意外,因为我们目前所采用的纹理处理方式,本身就存在一些安全性尚未验证的问题。
虽然这种做法还没有经过充分测试,也未被证明在所有情况下都是安全可靠的,但我们目前仍然使用了这种方式。考虑到这些风险,出现问题也属正常。
总的来说,我们对这种结果是可以接受的,当前阶段的目标并不是彻底解决图形稳定性问题,而是优先完成主要逻辑功能,后续再对图形部分做更稳妥的改进。
触发粒子系统
我们最初进行了一些临时处理,但一开始确实忽略了粒子系统的逻辑。具体来说,粒子系统是通过角色的头部朝向来确定粒子的朝向的,这就意味着粒子会根据角色面朝的方向来进行偏转或发射,这一机制本身就比较特殊,也容易引起一些意想不到的结果。
不过目前看来这种行为是可以接受的,没什么严重问题。
在这个基础上,我们理论上已经能够对所有这些调试项进行编辑了。只要我们愿意为它们编写相应的编辑器逻辑,就可以实现各种类型的交互和修改。这在功能层面上是一个非常大的进展,虽然听起来有些疯狂,但实际上已经具备了构建完整编辑系统的能力。只需扩展更多类型的编辑器即可实现更复杂的操作。
在 win32_game.cpp 中切换渲染类型
我们尝试切换渲染类型时,发现了一些问题与限制。当前我们只能在两个 OpenGL 渲染类型之间切换,这是因为 globalRenderType
并不是一个布尔值。由于这个原因,若只将其处理为布尔类型,就无法支持第三种渲染类型,例如软件渲染模式。因此这是我们后续需要解决的一个问题。
目前,通过切换,我们可以正确渲染为软件模式,并且也可以成功切换回原有模式。这表明系统大致已经运作正常。虽然整体流程还没有最终完成,但进展令人满意。
在这个过程中还发现另一个问题,当切换到软件渲染时,图像色彩出现偏差,呈现出一种“泛白”效果。这说明我们在切换渲染后,尚未正确处理 sRGB 色彩空间相关的内容,这是接下来需要修复的地方。
另外还观察到一个现象:界面上小暂停图标(little pause)似乎出现了异常,看起来状态不对劲。可能是全局暂停(global pause)状态没有正确设置或响应,推测可能和键盘上的某个快捷键有关,比如 “P” 键设置了切换,但我们当前没有对这个键的处理逻辑进行更新或调试。
总体而言,目前我们已经能够通过调试系统切换不同的渲染路径,并且具备编辑和控制相关参数的基础能力。不过仍有几个方面需要完善:
- 支持多值(非布尔型)调试项的编辑器;
- 正确处理不同渲染路径下的 sRGB 输出;
- 修复暂停状态指示的逻辑;
- 清理并统一渲染状态的表示方式。
虽然还有不少工作待完成,但整体系统架构正在逐步成型。
软件渲染奇怪 窗口没对上的原因
这个问题之前遗留的问题
根据窗口大小个GlobalBackbuffer赋值
在 game_debug.cpp 中确保开始调试时清除 debug EditEvent
目前有一个比较奇怪的行为是,调试时的编辑状态会保持(sticky),也就是说,如果不手动清除,某次编辑的状态可能会在之后的调试帧中继续生效。这种行为显然不符合预期,因此需要在每次进入调试时,主动清除当前的调试事件。
理想的处理方式是在每一帧的调试循环中,特别是在调试开始时(即 debugStart
或 debugFrame
的时候),就将当前的调试事件重置,防止某次编辑被意外持续应用。每次调用全局调试表并附加事件(globalDebugTableAddEvent
)时,也应当确保重置编辑状态。
目前这个问题暂未引发明显的 bug,但考虑到后续逻辑的复杂性,为了稳定性还是应当尽早修复这一隐患。
同时还有另一个异常是,按下全局暂停键(如 “P”)时,调试系统并没有如预期那样正确响应。其他相关功能看起来一切正常,只有这个全局暂停状态有问题。具体表现是:设置了暂停状态后,系统会立即自动将其切换回非暂停状态。
初步判断这个问题可能和调试编辑系统的集成有关,尤其是我们刚刚引入并启用了对事件编辑的支持,可能在某处逻辑上干扰到了全局暂停的正确设定。具体原因还不明确,需要进一步调查。
除此之外,还有以下几点值得注意和后续处理:
- 在
debugFrame
中添加逻辑以清除遗留的编辑状态; - 检查全局暂停状态与编辑系统的交互逻辑,确定是否存在状态覆盖或更新顺序的问题;
- 观察全局
pause
控制是否和局部状态有逻辑冲突,是否存在未同步的控制路径; - 计划将剩余功能推迟到明天继续处理,目前先暂停其他新增内容。
整体而言,编辑与调试系统已经初步成型,基础功能基本可用,但还存在一些边缘问题需要打磨,尤其是状态同步与清理机制。
暂停会出现触发断言
原因是GlobalPause 范围太大 导致事件没有被处理
DEBUGGameFrameEnd 没有呗执行导致的
在 game_world_mode.cpp 中添加切换 GroundChunks 的能力
当前在处理地面区块(ground chunks)渲染逻辑时,发现原本所在的位置是地面区块的更新部分,而不是实际执行渲染的部分。虽然在最初的思路中以为更新和渲染是前后分离的,实际上在代码中更新可能发生在渲染之后,这有点出乎意料,但确认下来确实是目前的结构。
为了便于控制这些地面区块的行为,尝试使用某种全局开关变量(例如 groundChunksOn
)来控制渲染流程的启用与否。但在实现中遇到了一些问题:
- 由于某个变量(如
groundChunksOn
)尚未声明,出现了“未声明的标识符”错误; - 此外,在编辑操作中误触发了“加载文件”(
load file
)操作,导致意外地新建了一个缓冲区,进而破坏了编辑流程; - 对于这种误触行为,后续计划通过配置编辑器禁止某些键位触发,以防止类似操作重复发生;
- 当前代码段中,虽然正确设定了地面区块的状态控制逻辑,并准备将其接入主循环,但最终遗漏了对状态值的实际存储或使用,导致运行时没有效果。
此外,还有关于 alpha 混合值的思考:
- 代码中设置了不同的全局 alpha 值(如
globalAlpha
),可能是因为不同渲染流程中设置的透明度不一致,需要统一控制; - 这一部分也需要检查:设置 alpha 的时机、位置是否合理,是否干扰了渲染顺序或渲染层级。
总体上,这一段工作正在将地面区块更新与渲染逻辑整合,旨在让其更模块化、受控,并借助配置开关实现灵活调试和渲染切换。同时也意识到 UI 编辑器行为对工作流的影响,计划进行个性化配置避免误操作。下一步需要补全遗漏的变量声明与状态管理,使控制逻辑真正生效。
在 game.cpp 中把 GroundChunksOn 放入菜单中
当前问题出现在菜单相关的逻辑中:
我们原本应该把某个功能或选项加入旧有菜单(old menu)中,但忘记这么做。现在尝试添加时却遇到了一个权限相关的错误,具体表现为:
- 系统提示“进程无法访问文件,因为该文件已被另一个进程使用”;
- 错误类型为“权限被拒绝”。
这种情况通常表明某个资源文件(可能是配置文件、数据文件或临时文件)当前正被另一个进程锁定或占用,导致无法写入或修改。
目前的判断和处理方式如下:
- 需要确认该文件是否在其他地方被打开或正在被其他进程使用;
- 有可能是编辑器、构建系统或其他后台工具未正确释放文件资源;
- 应该在进行文件写操作之前确保文件没有被其他进程占用;
- 考虑加上文件访问冲突的处理逻辑,或者使用操作系统提供的独占访问模式进行处理;
- 临时解决方式可以尝试关闭相关程序或重启进程,释放占用。
总体上,这是一个典型的文件访问冲突问题,需要从进程或文件句柄管理的角度去排查和修复。同时也要检查是否在代码逻辑中有资源未关闭或未正确同步的地方,避免未来再次触发类似错误。
运行游戏并测试 GroundChunks 的开关功能
现在系统已经可以正常控制渲染行为,例如:
- 可以在当前界面中开启或关闭某些功能;
- 可以在菜单中选择切换到软件渲染模式,切换后能够正确显示;
- 可以自由切换这些状态(例如启用、禁用某些渲染项),并且表现如预期;
- 虽然系统还有不少工作待完成,但目前已经趋于收敛,基础部分稳定可用;
- 接下来需要清理系统中冗余的调试 ID 等遗留逻辑;
目前的逻辑是所有编辑操作都已经统一到特定的网格(grid)结构上,因此:
- 不再需要保留多个路径来处理调试编辑;
- 可以删除多余的“调试 ID”等原本用于标识不同编辑目标的中间机制;
- 系统将更简单、统一和稳定;
总体而言,已经构建出了一个更为明确、集中化的渲染和调试编辑框架,虽然后续还有优化空间,但方向清晰,结构正在变得简洁可靠。