游戏引擎学习第202天

调试器:启用“跳转到定义/声明”功能

开始了一个完整游戏的开发过程,并分享了一些实用技巧。首先,讨论了如何在 Visual Studio 中使用“跳转到定义”和“跳转到声明”功能,但当前的项目并未启用这些功能,因为缺少浏览信息(browse info)。这种功能依赖于项目在构建时生成浏览信息文件,通常通过 Visual Studio 创建项目时自动设置。

在 Visual Studio 中,如果启用了浏览信息文件,右键点击代码中的内容时,就可以使用“跳转到定义”和“跳转到声明”功能。这对于开发者在探索代码时特别有用,尤其是当你在处理庞大的代码库时。例如,在 Windows 的源代码中,当你想了解某个函数的定义时,通过“跳转到定义”功能,编译器会自动引导你到正确的位置,节省了手动查找的时间。

接着讲解了如何在自己的项目中启用这个功能,虽然没有项目文件时 Visual Studio 可能不会加载这些功能,但开发者仍然可以在编译器设置中开启这些功能。

网络:正在构建浏览信息文件:概述1

讨论了与编译器的浏览信息(browse information)和生成调试信息相关的内容,特别是通过编译器生成用于代码浏览的文件(如 .pdb.bsc 文件)。他们提到了一些编译器的选项,例如 -FR-FR,并解释了这些选项的作用。

  1. 编译器的浏览信息

    • 在使用 Visual Studio 或其他工具时,如果你启用了 “浏览信息”(browse information),编译器会生成一些附加信息文件,这些文件帮助 IDE 或工具进行代码浏览和跳转。
    • 通过使用一些编译器选项,可以生成包含符号信息(symbolic information)的文件,使得在 IDE 中可以进行如 “跳转到定义”(Go To Definition)等操作。
  2. -FR 选项

    • -FR 选项会生成带有完整符号信息的文件,这对于在 IDE 中进行代码浏览非常有用。启用这个选项会生成一个完整的符号表,其中包含了所有的本地变量信息。这是开发者在调试和分析代码时常用的设置。
    • -FR 生成的文件包含比普通的浏览信息更多的符号数据,是“完整的蒙提”(full monty)版本。
  3. 路径设置

    • 当启用这些选项时,开发者可以指定一个路径,告诉编译器将这些浏览信息文件保存到哪里。这样,编译器就可以根据设置的路径生成调试文件。
  4. 尝试设置并测试

    • 讲述者提到,他们会尝试使用这些设置(例如 -FR 选项),看看是否能够生成预期的浏览信息文件,但他们并不会花太多时间在这上面,因为这不是他们当前的主要目标。此时,他们更多的是提醒开发者这项功能是存在的,并且值得了解和使用,特别是对于需要深入代码分析的开发者。

总结:

  • -FR 选项在编译时用于生成带有完整符号信息的浏览文件(例如 .bsc 文件),这些文件使得 IDE 能够提供代码跳转和调试的功能。
  • 开发者可以使用这些选项来提高代码的可浏览性,尤其是在大型项目中。
    在这里插入图片描述

在这里插入图片描述

添加 -FR 标志

在这段对话中,讲述者讨论了如何在编译过程中启用浏览信息(browse information)并生成相关的文件,最终目的是在 IDE 中能够利用跳转功能(如“跳转到定义”)。他们通过调整编译器的标志并尝试生成相关文件,但遇到了一些困难和不确定的地方。

主要内容总结:

  1. 编译标志

    • 讲述者提到,试图通过将 -FR 选项添加到编译器的标志中来生成浏览信息(*.svr 文件)。通过这种方式,他们期望能够生成包含完整符号信息的文件,这样就能在 IDE 中使用“跳转到定义”等功能。
  2. 生成浏览信息

    • 添加了 -FR 标志后,编译器成功生成了 .svr 文件,这证明该操作有效。
    • 然后,讲述者继续提到,接下来需要使用 dsc make 命令来生成 .bac 文件,这是用于显示浏览信息的文件。
  3. 问题和挑战

    • 尽管生成了 .svr 文件,讲述者在尝试使用 dsc make 和查看生成的文件时遇到了一些问题。生成的文件似乎被删除了,或者没有按预期工作。
    • 他们认为,如果没有一个实际的项目(而是只有一个可执行文件),则浏览信息功能可能无法正常启用,至少从他们的经验来看是这样。
    • 因此,他们怀疑要使浏览信息正常工作,可能需要一个实际的项目,而不是单纯的可执行文件。
  4. 解决办法

    • 讲述者提到,可能需要创建一个虚拟项目(dummy project)来解决这个问题,并启用浏览信息功能。
    • 虽然他们并不完全确定,但根据文档和一些配置选项的提示,他们认为这可能是解决当前问题的途径。

总结:

  • 为了启用代码浏览功能,讲述者尝试使用 -FR 标志生成浏览信息文件,后续需要通过 dsc make 命令生成 .bac 文件。但由于没有完整的项目配置,这一过程遇到了一些挑战。
  • 他们的猜测是,只有一个实际的项目(而不是单纯的可执行文件)才能使浏览信息正常工作。

调试器:展示一个带有此浏览信息启用的虚拟 Win32 项目

这是一个关于如何在Windows中快速查找信息的例子。我们通常会保留一个类似“win three to project”的工具,主要用于在操作系统中查找某些定义或信息。这种工具虽然平时不常用,但它始终处于可用状态,以便在需要时可以迅速获取某个术语或概念的准确定义。通过这种方式,可以避免冗长的搜索过程,直接获取需要的内容。这种方法虽然看起来有点特殊,但对于提高工作效率很有帮助。之后,我们会回到常规工作,继续进行其他任务。

移除 -FR 标志并删除 *.bsc 和 *.sbr 文件

我们不需要额外的操作,也不希望编译器做任何不必要的事情,因为希望保持复合时间尽可能快。因此,决定删除其中的 bscsvr 文件,并恢复到之前的状态。这样做是为了避免不必要的额外操作,让系统保持在一个更简洁、更高效的状态,以便继续进行其他任务并确保处理过程流畅。

回顾当前状态

昨天的状态是,所有的内容都保持在运行状态,但只是因为我们关闭了调试代码。实际上,调试代码并没有被完全去除,只是暂时通过一堆条件语句被屏蔽了。如果我们重新启用手动关闭调试功能,让所有代码都重新编译,就会发现我们有大量的错误需要处理。此时我们正处于中途,实际上可能还留下了一些没有完成的部分,我们当时正在尝试不同的方法来决定如何组织调试变量的设置。

在编程过程中,很多人可能不太愿意花时间处理演示代码,因为他们更倾向于做其他的事情,这是可以理解的。不过,对于我来说,如果觉得有某个地方值得深入分析并且能提高生产力,我就觉得有必要花时间去处理这些问题。编程中,耐心是非常重要的美德,尤其是在调试过程中。

我们现在已经做了大约200多个小时的编程,这实际上是相当少的。200小时编程几乎相当于不到10周的时间。因此,尽管有时候我们可能会在调试变量和代码结构上花费更多的时间,但这绝对不会让一个有经验的程序员失去耐心。处理这些问题、不断迭代优化代码是非常重要的,特别是对于一个优秀的程序员来说,他们会愿意花很多时间去解决一个问题,常常是需要许多个小时来思考和调整。

当然,如果时间有限,可能会因为某些紧迫的截止日期或其他更重要的任务而不得不做出取舍,但在当前的情况下,我们没有这些限制。我们的目标是通过教育性质的内容去深入理解和完善代码。为了确保代码结构合理,我们需要花足够的时间去反复测试和优化,而不仅仅是依赖第一种解决方案。

尽管有些时候可能看起来某些问题花费的时间比较长,但实际上这正是编程过程中不可避免的一部分。在低级编程中,处理这些复杂问题和不断优化代码是常态。如果没有耐心去不断调整和实验代码,那可能就不适合从事低级编程工作。大部分编程工作其实就是在不断地调整、重构和试验中前进。所以,耐心和对细节的关注是非常重要的。

为今天设定

我们现在正在处理调试功能,虽然不完全确定最终的结构,但我知道需要它支持一些特定的操作。现在的做法是通过一定的转换步骤,逐步实现这个功能。在这个过程中,会尝试不同的方法,看看哪种方式更容易添加调试变量,看看哪些方法更有效。

我计划先完成这个转换的工作,使其能够正常运行。然后,接下来会做一些使用案例的测试,这样可以了解如何在实际代码中应用这些调试变量。之前,我们只是做了一些简单的测试,主要是检查代码中的调试变量是否可以正常变化,但这些测试非常简单,基本上只是布尔值的切换,调试功能的范围比较小。

接下来的计划是,在这个版本稳定之后,回到实际的代码中,可能会创建一个像实体查看器这样的工具,或者开发一个调试功能来查看更复杂的内容。加上性能分析视图,这样的组合应该能提供一个核心示例,证明我们的调试系统已经足够完善。如果这些功能能够以合理的方式处理,那么调试系统就基本可以认为是完成了。

这是目前的想法,接下来就继续推进这部分工作。

game_debug.h:引入 debug_variable_link

目前正在处理调试变量的存储问题,虽然还没有确定最好的方式,但想要尽可能灵活。之前对于如何管理这些调试变量有一些不确定,尤其是在使用调试变量链表时,还没有找到最合适的解决方案。现在考虑的一种可能方法是使用双向链表来管理调试变量,这样的结构允许我们在任何时候添加或移除元素,保持足够的灵活性。

目前,所有的代码都是推送式的结构,因此引入一个类似调试变量链表的方式可能会更加合适。调试变量将通过一个哨兵(sentinel)机制进行组织,形成一个类似变量组的结构。通过这种方法,可以在需要时动态地添加或删除调试变量,而不需要担心固定的数组或者指针问题。这个方法可能比之前想的更简单,因为它能够有效地处理指针管理的问题,并且确保调试变量的管理保持合理和清晰。

暂时决定就这样先采用这个结构,继续进行调试和优化,直到找到更合适的方式。
在这里插入图片描述

game_debug_variables.h:引入 DEBUGAddVariableToGroup

目前已经基本确定了调试变量的结构和处理方式。接下来要做的就是实现调试变量的添加操作。如果按之前的方式来做,调试变量会直接被创建。当调用调试变量时,系统会将变量添加到调试列表中。如果当前有活动的变量组,还需要将这个变量添加到相应的变量组中,这样就能确保变量被正确地管理和组织。

为了使结构更清晰,还考虑增加一层结构。每个调试变量将关联一个调试变量组,这样会更方便管理每个变量及其所属的组。虽然之前有类似的机制,但现在可能更倾向于实现一个“调试添加变量到组”的方法,这样可以更加简洁明了。通过这种方法,调试系统会接收到调试状态、调试变量以及调试变量组,并将变量正确地添加到组中。

在实现这一过程时,调试变量链接会被创建,然后将其保存并按照预定顺序进行处理。这样做的方式和之前的一致,基本上沿用了相同的逻辑和结构。

此外,如果需要,可以进一步封装这部分代码,创建宏来简化操作。比如,可以将调试变量的处理封装为一个宏,这样每次使用时就能减少重复代码并提高效率。通过这种方式,调试系统的功能可以更加灵活和高效,符合开发过程中对代码简洁和可维护性的要求。

在这里插入图片描述

game.h:#define DLIST_INSERT

链表的操作其实非常简单,当你往链表中添加元素时,整个过程是非常直白和固定的。我们总是知道,添加元素到链表中的过程会是什么样的,所以可以想象,创建一个宏来简化链表操作是很有意义的。通过宏,就不需要每次都手动编写链表的增删操作,直接使用宏就可以完成。

链表操作的基本模式始终是一样的。如果我们有一个哨兵节点和一个元素,只需要按照固定的方式进行处理,不会有任何特别的差异。可以通过一个宏来简化这些操作,比如使用doubly linked list insert这样的方式,只需要提供哨兵节点和元素,宏就会自动处理好链表的插入逻辑。

这种方法的好处是,链表的增删操作变得非常简洁和统一,每次都不需要重复编写复杂的代码,直接用宏就能完成相同的任务。这种方式类似于将链表的处理封装成一个可以复用的工具,极大地提高了效率和代码的可维护性。
在这里插入图片描述

game_debug_variables.h 和 game_debug.cpp:使用 DLIST_INSERT 宏

链表的操作本质上非常简单,但如果想要节省一些思考时间,可以通过宏来简化操作。比如,当你需要插入元素时,可以通过指定长度、组别等信息来调用宏,这样就能省去每次手动编写链表操作的麻烦。知道宏的具体实现后,只需要提供哨兵节点和要插入的元素,就能完成插入操作。

这种方法非常适合不喜欢每次都手动编写链表操作的开发者,特别是对于双向链表这样的常见数据结构。通过使用宏,可以将链表的操作封装起来,使得每次插入操作变得非常简洁。宏会自动完成插入的过程,从而减少了开发者需要处理的细节。

此外,如果对表格不感冒,或者在程序中不喜欢频繁编写链表代码,可以考虑使用宏来处理这些操作。这样可以避免手动编写繁琐的代码,特别是在处理复杂数据结构时,可以显著提高开发效率。这是一种非常有用的小技巧,适合那些希望简化工作流程的开发者。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这两个宏定义用于操作双向循环链表(Doubly Circular Linked List)。我们分别解析它们的作用和原理:


1. DLIST_INSERT(Sentinel, Element)

这个宏用于在头节点(哨兵)后插入一个新元素,即 Element 被插入到 Sentinel 之后。

#define DLIST_INSERT(Sentinel, Element) \
    (Element)->Next = (Sentinel)->Next; \  // ① 新元素的 Next 指向当前哨兵的 Next(即原来的第一个元素)
    (Element)->Prev = (Sentinel);       \  // ② 新元素的 Prev 指向哨兵
    (Element)->Next->Prev = (Element);  \  // ③ 原来的第一个元素的 Prev 指向新元素
    (Element)->Prev->Next = (Element);     // ④ 哨兵的 Next 指向新元素

示意图:
假设原来的链表:

[Sentinel] <-> [A] <-> [B] <-> [C] <-> (循环回Sentinel)

插入 Element 后:

[Sentinel] <-> [Element] <-> [A] <-> [B] <-> [C] <-> (循环回Sentinel)

等价的 C 代码:

void DList_Insert(Node* Sentinel, Node* Element) {
    
    
    Element->Next = Sentinel->Next;
    Element->Prev = Sentinel;
    Sentinel->Next->Prev = Element;
    Sentinel->Next = Element;
}

2. DLIST_INIT(Sentinel)

这个宏用于初始化双向循环链表的哨兵节点

#define DLIST_INIT(Sentinel)       \
    (Sentinel)->Next = (Sentinel); \  // ① 哨兵的 Next 指向自身
    (Sentinel)->Prev = (Sentinel);    // ② 哨兵的 Prev 也指向自身

示意图:

[Sentinel] <-> [Sentinel]  (自身循环)

等价的 C 代码:

void DList_Init(Node* Sentinel) {
    
    
    Sentinel->Next = Sentinel;
    Sentinel->Prev = Sentinel;
}

总结

  • DLIST_INIT(Sentinel):初始化双向循环链表的哨兵,使其 NextPrev 都指向自身。
  • DLIST_INSERT(Sentinel, Element):在 Sentinel 之后插入一个新节点 Element

这两个宏结合起来,就能构建和操作一个带哨兵节点的双向循环链表

game_debug_variables.h:继续清理

首先,DebugAddVariableToGroup 负责将某个元素添加到一个分组中,该操作涉及一个地址,并且是必要的步骤。接下来,需要调试变量,这个变量在当前情况下将会调用DebugAddVariableToGroup,除了将变量添加到分组之外,它还会进行额外的处理。因此,它的作用不仅仅是简单地加入分组,而是结合上下文来执行进一步的操作。

在这个过程中,变量 Context->Group 可能会被使用,但前提是必须确保存在一个可用的分组,否则该操作不会执行。当这些条件满足后,任务就算完成了。

接下来,存在一个DEBUGBeginVariableGroup,它的作用是将某个变量添加到指定类型的分组中。这个分组类型对应的是DebugVariableType_VarGroup。在短期内,VarGroup 这个名称可能与此相关,但具体名称可能已经更改,或者在不同的情况下有不同的称呼。

此外,还有一个与VarGroup类似的名称,可能是VarGroup,但目前尚不确定它的最终命名。
整体来看,这个逻辑是按需执行的,没有过多的额外操作,只完成当前所需的任务。

game.h:#define DLIST_INIT

接下来,我们可以创建一个更大的变量分组,用于持有一个初始化整个分组的变量。这个变量是 DLIST_INIT,我们可以以不同方式来处理它,但本质上这些方式都是一样的。这段逻辑具有高度通用性,适用于几乎所有的双向链表结构。

对于双向链表来说,哨兵节点的初始化方式是固定的,不会因场景不同而发生变化。哨兵节点(sentinel)的 next 指针必须指向它自身,同时它的 prev 指针也必须指向它自身。这个初始化模式在每个双向链表中都是一致的。

此外,我们也可以在这个结构中加入“移除”操作(remove),虽然目前还没实现,但未来是可以加进去的。这一切准备就绪之后,我们的链表结构就构建完成了,能够正常运行。

在链表初始化完成之后,还需要做最后一步,就是将刚刚创建的变量分组设定为当前活动分组(active group)。这样我们在后续操作中才能明确使用的是哪个分组。

总结来说,流程包括:

  1. 创建一个变量并初始化分组;
  2. 设置哨兵节点的 nextprev 都指向自身,完成双向循环链表的初始化;
  3. (可选)未来可以添加移除功能;
  4. 设置当前活动分组为新建的分组,确保后续的操作作用于正确的上下文。

game_debug.h:考虑是否需要为变量存储父指针

目前存在一个略显棘手的问题,那就是我们对于是否继续保留“父节点”这一概念还不够明确,缺乏足够的可见性。在当前阶段,我们拥有两种调试结构:

  • 调试视图(debug views),用于存储我们需要追踪和保留的信息;
  • 调试变量(debug variables),则包含了类似树结构的信息,层级关系已经内嵌在其中。

问题在于,我们是否还需要在变量中保留“父指针(parent pointers)”。这个问题的答案是,虽然变量分组是我们主动创建的结构,但在每一个分组内部,我们所引用的变量可能来源于任何地方。一个变量可能出现在多个视图中,被克隆或者引用。

这就导致:回到上一个变量并非理所当然,因为其可能并不是唯一存在的实例。所以,单靠父指针来回溯是不可靠的。

因此,更合理的做法是:在执行可遍历的结构(比如树或图)时,我们更倾向于使用**栈结构(stack)**来管理遍历状态。

如果不想手动维护栈结构,也可以使用递归函数,由编译器管理程序调用栈,来实现深度优先的遍历逻辑。但是在当前的场景下,我们倾向于明确地使用“压栈-弹栈(push-pop)”的方式来管理上下文状态。

总结要点如下:

  1. 当前缺乏对是否保留变量父指针的清晰判断;
  2. 变量可能被克隆并出现在多个视图中,父指针不能保证唯一性;
  3. 回溯访问上一个变量不能依赖指针,应使用更稳健的结构;
  4. 推荐使用栈或递归方式管理遍历过程,避免依赖父指针;
  5. 可遍历结构更适合“显式栈”操作,便于控制访问路径和上下文管理。

game_debug_variables.h:引入 debug_variable_group_builder 以支持 DEBUGEndVariableGroup 返回父级 VarGroup

当我们执行“debug 大型变量分组”的时候,会引发一个新的问题:在结束一个变量分组时,无法直接执行某些操作,主要是因为我们失去了与父级分组的连接。不过这个问题本质上很简单,我们完全可以借助编译器的调用栈来解决。

实际情况是,在结束一个变量分组的时候,我们真正需要的是获取并返回它的父分组。所以,我们可以将“变量分组”和“它的父级”一起打包成一个结构,类似于一个变量分组构建器(builder)。这个结构在创建过程中,既包含当前的分组,也包含它对应的父分组。

通过这种方式,我们就规避了无法追踪父分组的问题。可以简单理解为,我们有一个 debug_variable_group_builder,它的返回值中就包含了上下文中的根分组,我们只需要存储这个返回结果。

这样,在执行 DEBUGBeginVariableGroup 的时候,就能够顺利地追踪和维护层级结构。尽管这样做稍微麻烦了一些,但从结构设计的角度看是可控且清晰的。

具体来说:

  1. 每次创建新的变量分组时,记录下当前的父分组;
  2. 在结束变量分组时,从“构建器”中拿到父分组,并恢复上下文;
  3. 这种方式通过构建器封装了上下文状态,避免了硬编码父指针的问题;
  4. 稍微增加了一些复杂性,但可维护性更强,也更灵活;
  5. 如果希望更简单,也可以用递归和栈方式,不过选择哪种方式取决于具体要优化的目标(性能 vs 可读性)。

总结:我们引入了一个包含当前分组和其父分组的中间结构(类似 builder),使得在“结束分组”操作时可以自然地返回到原来的上下文。这个方案虽然操作上稍显繁琐,但逻辑清晰,适用于需要频繁嵌套分组并保持上下文一致的场景。
在这里插入图片描述

game_debug_variables.h:向 debug_variable_definition_context 添加 GroupDepth 和 *GroupStack 数组,展示另一种解决方案

在这种特定的情况下,为了更直观地展示变量分组的构建与管理方式,我们还可以采用另一种实现方式,那就是引入一个**栈结构(stack)**来管理分组的上下文。这种方式结构清晰,逻辑上也非常直观。

我们会定义两个关键变量:

  1. groupDepth:表示当前分组嵌套的深度;
  2. groupStack:是一个固定长度的数组(例如最大 64 层嵌套),用来存放每一层的变量分组指针。

分组嵌套的基本原理如下:

  • 在任意时刻,如果 groupDepth > 0,就说明我们当前处于一个有效的变量分组上下文中;
  • 我们可以通过 groupStack[groupDepth - 1] 获取当前处于顶层的分组,并将新的变量添加到这个分组中;
  • 如果想简化边界判断,也可以人为地在栈底放一个“虚拟根分组”,这样即使 groupDepth == 0,仍然有一个合法的父级指针,避免空值判断。

实现细节:

  1. 在执行 beginVariableGroup 时,我们会先断言 groupDepth 没有超过最大容量(比如 64 层);
  2. 然后将当前新建的分组加入 groupStack[groupDepth],并将 groupDepth 自增;
  3. 这个分组成为当前的顶层,后续的变量都会添加到这个分组中。

需要注意的一点是:如果直接在入栈时写入 groupStack[groupDepth++],在判断数组越界时必须预先检查是否 小于最大容量 - 1,否则有可能写入超出数组边界的位置。因此,先检查后写入,逻辑更安全。

在执行 endVariableGroup 时:

  1. 首先断言 groupDepth > 0,确保我们当前确实处于一个分组中;
  2. 然后将 groupDepth 减 1,即表示当前分组结束,回到上一个分组上下文;
  3. 这样,在任意时刻,groupStack[groupDepth - 1] 都是当前有效的父分组,能正确维护嵌套关系。

总结关键流程:

  • 使用栈结构来追踪变量分组的嵌套层次;
  • 每次创建新分组就将其压入栈顶;
  • 每次结束分组就将其从栈顶弹出;
  • 使用数组模拟栈,限制最大嵌套层级,避免无限嵌套;
  • 提前加入虚拟根节点可避免对 groupDepth == 0 的特殊判断;
  • 每次添加变量时都引用栈顶的分组指针,确保上下文一致。

通过这种方式,我们避免了构造复杂的结构,也不依赖编译器栈,能够手动管理分组上下文,更加灵活、安全且便于调试。
在这里插入图片描述

game_debug.cpp:初始化 debug_variable_definition_context 中的 GroupStack

当我们初始化上下文时,必须确保变量分组的栈(group stack)已经被正确设置并处于预期状态。特别是在使用变量定义上下文(variable definition context)时,初始化过程必须更严格,以避免后续逻辑中出现分组状态错误或栈越界。

初始化上下文时的关键步骤如下:

  1. 初始化栈深度 groupDepth 为 0,意味着此时没有进入任何变量分组;
  2. 初始化 groupStack[0] 为空指针(null),即栈底是一个“虚拟的空分组”。这样做的好处是,即使当前未进入任何分组,也能安全地引用栈底,避免出现空指针异常;
  3. 从这个空分组开始,之后通过 beginVariableGroup 启动实际的根分组,然后再继续嵌套其他分组。

虽然这种“空栈底”的处理看起来有点绕,但实际上是为了保证代码的健壮性,在任意深度下都能获取到有效的父分组。

针对当前结构的补充说明:

  • 初始化时虽然有一段默认流程可能会触发“根分组”的初始化,但我们并不依赖它,而是手动设置 groupStack[0] = null 并从此开始逻辑流程;
  • 然后,调用 beginVariableGroup 来进入实际的根级分组并按需继续构造;
  • 之所以对这个“根分组”存在疑问,是因为之前的流程中出现了一些不太符合预期的行为,但即便如此,当前方式可以绕开这些问题,确保整个上下文初始化稳定、安全。

调试与防御式编程:

为了避免疏忽遗漏导致的栈状态错误,我们在最后添加了一个 断言(assert)

  • 在所有变量分组结束后(执行完所有 endVariableGroup 操作之后),断言 groupDepth == 0
  • 这样能有效检查是否存在“未关闭的变量分组”;
  • 虽然程序在实际运行时仍可能“正常工作”,但这类逻辑疏漏是危险且不规范的,必须主动检测并避免。

总结:

  • 初始化上下文时明确设置 groupDepth = 0
  • 设置 groupStack[0] = null 作为安全栈底;
  • 显式调用 beginVariableGroup 构造真正的根分组;
  • 最后使用断言确保所有分组都被正常关闭,防止遗留未关闭状态;
  • 整体过程体现了防御式编程的思路,确保变量分组系统在构建与释放过程中保持绝对的一致性与可控性。
    在这里插入图片描述

game_debug_variables.h:继续清理编译错误

接下来我们继续推进调试相关的代码逻辑。

首先发现 DebugCameraRef 这一部分现在已经不再起作用,基本上不清楚它当前的存在意义,因此我们决定将其移除以简化结构,去除冗余代码。

随后我们进入了调试相关的逻辑部分,具体来说是变量树(DebugVariableTree)的处理。在这里,变量系统已经基本完善,运行也相对正常,具备一定的组织性。

关于变量树的处理:

  • 现在变量系统已经基本成型,能够以分组形式组织变量,形成一棵树状结构;
  • 接下来的目标是清理已有逻辑,并为变量树的创建与展示准备好基础;
  • 如果要添加一个调试树(debug tree),我们就应该使用一个统一的名称,比如称为 debugTree
  • 分配并初始化这个结构体时,参数的设置其实与原先的逻辑大致相同,只是现在所引用的对象是变量而非之前的特殊类型;
  • 因此,核心只需要知道当前操作的变量属于哪个分组,并据此建立树结构。

关于变量分组与显示的简述:

  • varGroups 中可能存在一些占位逻辑(placeholder),主要是为了保持结构上的完整性;
  • BitmapDisplayInVarGroup 这类函数存在的意义在于确保我们将某些变量正确地分类显示,尤其是将其转化为文本形式;
  • 这些展示逻辑并不是系统运行的必要部分,但它们能够清晰地表明当前系统对变量结构的理解是完整的;
  • 暂时不需要对这些内容做太多变动,后续可以考虑更深入的交互优化,但当前保持原样即可。

结语:

当前调试系统整体框架已经搭建完成,变量分组、变量树、显示管理三者之间的关系也逐渐清晰,接下来的步骤会更加关注在交互优化、功能扩展等方向,比如改进显示方式、支持更多的变量类型转换、拓展分组树的动态更新等。我们会逐步展开。
在这里插入图片描述

在这里插入图片描述

黑板:使用指针和栈遍历树

在当前的调试变量结构设计中,我们不再使用变量的父节点指针,这就意味着在遍历变量树时,不能再依赖传统的“向上回溯”方式来确定回退路径。这一变化带来了一个关键问题:我们无法知道在遍历完成一个分支后该如何返回上一层节点,因为变量可能存在于多个树中,失去了唯一的父节点引用。因此,我们必须引入栈结构(stack)来记录遍历路径,以便于正确地返回。

遍历变量树时栈结构的必要性

我们可以设想一下树结构的遍历过程:

  • 原本的树具有双向指针(子节点指向父节点,父节点指向子节点),所以可以通过指针轻松向上或向下移动;
  • 现在取消了父指针,意味着不能再直接“向上爬”;
  • 栈的作用就如同在山崖上攀爬时的绳索——每向下一个节点就压入当前路径节点,回退时从栈中弹出上一个节点;
  • 遍历过程就能准确恢复上下文路径,从而支持完整的深度优先遍历(或其他类型的遍历)逻辑。

实现方法上的两种选择

我们有两种方式来实现这种遍历:

方法一:显式栈结构
  • 使用一个结构体,比如 DebugVariableStack,其中包含当前深度和一个栈数组;
  • 每当遍历一个新节点时,将当前节点压栈;
  • 当分支结束、需返回上级时,从栈中弹出前一个节点;
  • 这种方式较清晰,便于控制,但需要维护栈状态。
方法二:递归方式
  • 使用函数自身的调用栈来模拟栈结构;
  • 每次进入子节点时递归调用自身;
  • 返回时自然退回上一层;
  • 实现简洁,但对于非常深的树结构可能导致栈溢出,且调试难度略高。

不过我们更倾向于使用显式栈的方式,因为递归函数虽然简单直观,但在某些场景下不易调试,而且可能影响性能或栈空间。

栈结构的基本定义草案:

struct DebugVariableStackEntry {
    
    
    DebugVariable* variable;
    int depth;
};

struct DebugVariableStack {
    
    
    DebugVariableStackEntry entries[MAX_DEPTH];
    int count;
};
  • MAX_DEPTH 可以设置为一个足够大的常数,比如 64;
  • count 表示当前栈的深度;
  • 每次遍历都根据 count 来管理栈的入栈和出栈。

应用场景与下一步

这种结构不仅适用于遍历调试变量树,还可以推广到多个视图的变量同步、变量组结构展示等方面。后续我们可以根据实际需求扩展栈的用途,例如保存额外上下文信息、处理不同树分支的操作行为等。

综上,我们已经建立了一个更灵活、更通用的调试变量系统结构,脱离了对双向指针的依赖,也为后续功能扩展打下了坚实基础。下一步可以专注于具体的栈封装、遍历接口定义,以及更智能的变量组织逻辑。

game_debug.cpp:在 WritegameConfig 中添加 *Stack

栈结构将数据按顺序存储,可以通过“推送”和“弹出”操作来访问数据。该结构通常用于处理嵌套的元素,像树形结构中的节点。考虑到树的深度,特别是涉及64层深度时,可能会导致问题,因此应该避免过多的深度递归操作。

在开始时,栈的深度被初始化为0,表示开始时没有数据或树的根节点。然后需要向栈中推送树的根节点,这个根节点就是树的起点。从根节点开始,接下来将依次处理树的各个层级。

程序使用一个while循环来遍历栈,直到栈为空。每次从栈中弹出一个元素,并对这个元素进行处理。在这个过程中,如果当前元素是一个“组”,那么处理完当前元素后,应该将该元素的子节点(如果有的话)再次推送到栈中。

此外,还涉及到遍历兄弟节点的操作。具体来说,栈中的每个元素不仅可能有子节点,还可能有兄弟节点,因此需要遍历兄弟节点。兄弟节点可以通过元素之间的链接关系获取,通常使用一个for循环来遍历这些兄弟节点。

处理流程大致如下:

  1. 初始化栈,推送根节点。
  2. 进入while循环,检查栈是否为空。只要栈不为空,继续处理。
  3. 弹出栈顶元素。
  4. 如果该元素是一个组,且有子节点,则将子节点推送到栈中。
  5. 对于当前元素的兄弟节点,使用for循环遍历并处理每个兄弟节点。

整个过程会持续进行,直到栈为空,即所有节点都已经被处理完。

在这里插入图片描述

在这里插入图片描述

黑板:栈的自然顺序

在这个讨论中,提到栈的自然顺序和树形结构遍历的关系。栈按顺序处理元素,但这种顺序并不符合树的结构,尤其是如果我们希望按照树的深度优先或广度优先顺序来遍历。

  1. 栈的自然顺序:栈是一种后进先出(LIFO)的数据结构。它处理的顺序并不符合树形结构的深度优先或广度优先规则。在栈中,每次推送的数据按照先后顺序存入,但当从栈中弹出数据时,它会按相反的顺序处理。对于树形结构,如果按栈的自然顺序遍历,可能会先处理树的所有兄弟节点,然后再处理子节点。这种方式相当于先处理树的一层,再处理下一层的节点。

  2. 树的深度优先遍历:在递归函数中,通常采用的是深度优先遍历(DFS)。递归函数会在处理当前节点时,递归地先处理它的子节点。递归会将一个节点及其所有子节点的状态推送到栈中,直到没有更多子节点可以处理。递归函数会先深入树的每一条路径,直到遇到叶子节点,然后返回并处理其他路径。

  3. 栈的显式使用与递归的区别:在显式栈的处理方式中,栈只存储数据,而递归函数则同时存储代码和数据。递归函数的栈不仅保存当前节点的状态,还保存函数的调用信息,因此每次递归会把当前节点的状态和接下来的执行步骤(代码位置)一起压入栈中,这使得递归能够按深度优先顺序遍历树。

  4. 广度优先遍历与栈的关系:如果使用显式栈来进行树的遍历,栈会按照层次处理树的节点,即广度优先遍历(BFS)。这意味着会首先处理完当前层的所有节点,才会开始处理下一层节点。而递归的方式通常是深度优先遍历(DFS),递归会深入每一条分支,直到遇到叶子节点才回溯。

  5. 栈与递归的效率问题:虽然栈可以显式地处理遍历的状态,但在树的遍历过程中,如果想要按照深度优先或者广度优先的顺序来处理节点,使用栈显式地操作可能会变得不太高效。比如,在广度优先遍历中,需要处理每一层的节点,然后将下层节点推入栈中。这种方式可能会增加额外的复杂度,因为每次都需要手动管理栈的操作和节点的顺序。

  6. 递归函数的优势:递归函数的优势在于它隐式地管理了栈的状态。递归会将当前节点和执行路径的信息一同压入栈中,然后处理子节点,最终返回父节点。通过这种方式,递归能够按深度优先顺序遍历树,而不需要显式地管理栈的顺序。

总结来说,显式栈的使用可能会让我们在遍历树形结构时遇到一定的挑战,特别是在保持遍历顺序方面。递归函数由于能同时管理代码执行状态和数据,通常能更容易实现深度优先或广度优先的遍历。在显式栈遍历树时,需要特别注意如何控制节点的顺序和回溯过程。

game_debug.cpp:引入 debug_variable_iterator 来增强栈

讨论的核心是如何手动管理栈以控制树的遍历顺序,尤其是在需要精确控制遍历顺序时,如何避免直接使用递归。传统上,递归函数通过隐式的栈管理遍历状态,但在手动管理栈时,需要注意如何存储和恢复栈的状态,以确保按照预定的顺序遍历树形结构。

  1. 栈的状态存储
    通过增强栈的结构,可以存储每次遍历时需要的额外状态信息。例如,栈中可以存储一个调试变量(debug variable),它包含了当前遍历的位置、终止条件(例如一个哨兵值sentinel)以及当前节点的链接(link)。这种做法类似于存储函数的局部变量,用来表示当前遍历的状态。

  2. 状态存储和栈的增强
    每当需要开始遍历一个新节点时,首先将当前节点的状态(如var group next)和终止条件(如sentinel)推送到栈中。然后,当遍历过程继续时,栈的状态会被更新,并确保按正确的顺序进行遍历。如果当前节点是一个组(group),则在遍历它之前,必须先保存当前的遍历状态,以便在处理完该组后,能够恢复并继续遍历栈中的其他元素。

  3. 栈的遍历与迭代
    在栈中存储了当前节点的遍历状态后,下一步是遍历这个节点并推进。使用类似迭代器的方式,可以通过增加遍历指针(如integrator link)来控制遍历进度。每当节点遍历完一层时,检查是否需要处理该节点的子节点。如果是一个组,当前的遍历状态需要保留下来,以便在处理完该组的所有子节点后,能够恢复并继续遍历栈中的其他节点。

  4. 遍历深度的管理
    如果当前节点是一个组,并且需要递归地处理该组的子节点,则在处理时,必须更新栈的深度信息(depth)。这意味着在每次进入一个新的组时,栈中的深度需要增加,并推送新的遍历状态。这时,可以使用VarGroup.Next和目标变量(target)作为新的栈元素,继续进行树的遍历。

  5. 栈的弹出与恢复
    在每一轮遍历完成后,需要弹出栈中的元素来恢复到上一级的状态。当栈中的元素遍历完一层后,可以通过简单地调整指针或状态来控制从哪里开始继续遍历。这种方式避免了递归的函数调用堆栈,而是通过显式管理栈来控制遍历顺序。

  6. 避免递归,控制遍历顺序
    如果想避免递归,手动管理栈可以使遍历过程更加灵活,特别是在需要控制遍历顺序时。在这种显式栈的处理方式中,我们不需要依赖递归的隐式栈,而是完全通过栈来控制遍历的进度和顺序。这对于大规模的树形结构遍历尤为重要,因为通过手动控制栈,可以避免递归带来的深度限制或性能问题。

  7. 总结
    总体来说,手动管理栈的目的是为了获得对遍历过程的精确控制,尤其是在需要按照特定顺序访问树的节点时。通过在栈中存储遍历状态信息,可以有效地模拟递归过程,同时避免递归可能带来的问题。通过增强栈的结构,能够在遍历过程中灵活地控制每个节点的状态和深度,确保按照预定的顺序进行遍历。这种方法适用于那些需要手动控制遍历顺序的场景,尤其是在栈的管理和状态恢复方面。
    在这里插入图片描述

game_debug.cpp:继续清理编译错误

在这里插入图片描述

game_debug.cpp:展开栈迭代循环

如果我们进入这个流程,并将初始值设置为某个特定的“初始值”时,我们可以做出以下操作。首先,假设我们从栈中取出元素时,可以检查它是否等于一个特定的“哨兵值”。如果是哨兵值,我们就要从“Depth”值中减去1。如果不是哨兵值,我们就处理这个元素,就像我们现在在做的那样。处理完后,我们将其移到下一个值。

基本的思路是,持续地从栈中取出元素。如果取出的元素是我们需要的,那么继续处理;如果已经完成了迭代,我们就将其从栈中弹出。如果还没有结束,我们就获取下一个元素并推进到下一个组。这一过程中,我们会处理当前的变量,并检查是否需要将新的元素推入栈中。

这个过程听起来非常直接,只需稍微包装一下,就可以很顺利地实现。包装的部分可能只是一些小细节上的调整,可能会在接下来的工作中稍微调整一下。也许会将这些细节留到下周一再处理。
在这里插入图片描述

game_debug.cpp:在 DEBUGDrawMainMenu 中使用我们的栈实现

在流程中,首先到达合适的位置后,准备开始执行接下来的操作。我们将使用当前的变量,按照它原本的方式进行操作。接着,处理变量树部分时,实际上只是将其名称改为更简单的常规树形结构,其他部分看起来没有变化,应该和之前的实现一样。

然后,我们继续进行反转操作,这部分逻辑与之前一样,完成后就继续执行。后续的操作和处理流程没有变化,保持原样。最终,在流程的结束阶段,我们进行迭代操作,并触发相关的动作。整体上,这个流程基本上与之前的实现保持一致,没有太大的不同。
在这里插入图片描述

game_debug.cpp:继续清理编译错误

首先,问题出在需要处理查找操作,而目前还没有实现。这意味着必须开始对每个变量进行处理,当显示一个变量时,需要获取它的调试视图。每个变量都应该有一个调试视图,包含类似维度等信息,可能包括内联块、大小等细节。因此,每个变量都需要具备这些信息。

在实现上,可以通过设置一个默认的调试视图值,然后在获取视图时,确保能够知道这个变量实际需要什么类型的视图。可能需要为不同的变量类型设置不同的调试视图类型。在这种情况下,可以通过一个方法来获取该变量的调试视图,传递变量并返回适当的视图。

这个过程中,每次使用变量时,都会根据变量的类型获取对应的调试视图,确保显示的是正确的视图。这个设计思路听起来很合理,也可以扩展更多功能,提供多种可能性,虽然还没有完全决定要怎么实现,但这些思路可以为未来的开发提供很大的灵活性。

接下来,涉及到的一些调整主要是将原先的处理方式改为使用视图,因为调试视图包含了变量的维度和布局信息。为了避免错误,必须确保在调用过程中,使用正确的视图类型和结构。有时,编译器会提示某些成员未定义,像在调试过程中出现了“非调试视图成员”这种错误时,需要修改相关代码,确保变量和视图之间的对应关系正确无误。

总的来说,当前的调整主要是与调试视图的实现相关,确保每个变量都能在合适的上下文中显示相应的视图,而这些修改并不会涉及过多的结构性变化。
在这里插入图片描述

在这里插入图片描述

game_debug.cpp:引入一个虚拟的 GetDebugViewFor

要创建一个虚拟的示例,这样可以帮助我们验证当前的代码是否能正常运行。这个操作应该能够有效地触发错误,因为一些功能尚未实现,导致程序在执行时崩溃。此时,错误信息表明某些必要的功能还没有被实现或处理,这可能是当前开发过程中遗留的问题。
在这里插入图片描述

game_debug.cpp:完成编译错误清理

正在开发变量引用的功能,并且这部分实现已经非常接近完成。首先,决定暂时跳过一些变量的处理,特别是涉及到拆解值的部分,因为这个部分将会进行主要的更改。接下来,将修改现有代码,以支持更加丰富和直观的拖放功能,并实施新的系统来提升操作的表达能力。

在进行这些更改时,需要为每个变量添加调试视图。每当提取到某个变量时,系统将会根据变量的类型返回相应的调试视图。为了处理不同类型的视图,决定在处理过程中假设总是能返回一个有效的调试视图,无论它实际是什么样的。

接下来,处理涉及可折叠视图的部分,确保这些元素能够正确显示并进行交互。如果视图不可折叠,则设置相关参数,保证布局和样式按预期工作。

在过程中,还需要解决一些小问题,尤其是与上下文和成员定义有关的错误,确保每个变量的上下文和调试信息都能正确对应。如果出现错误或未定义的成员,需要及时修正。

最后,为了确保代码继续顺利运行,将暂时跳过一些细节,比如某些不必要的属性和维度,专注于完成主要的实现部分,逐步解决遇到的问题,最终在完成这些修改后,确保系统能够正常运行并支持新的功能。
在这里插入图片描述

在这里插入图片描述

这工作了吗?

目前遇到的问题是,某些功能无法正常工作,特别是与GetDebugViewFor相关的部分。由于这部分功能还没有实现,所以它会返回零,并且此时无法使用该功能。虽然可以尝试创建一个类似的功能来测试,但在没有实现该功能之前,任何相关操作都不会正常工作。

同时,回顾上一次的工作,发现一个问题没有被解决,那就是最后一个变量组没有正确关闭。在调试过程中,这个问题非常明显,因为组的开头和结束不匹配,导致一个组没有被正确关闭。虽然从功能上来看,这个问题并不会导致系统崩溃,因为在当前的系统中,关闭组并不会产生任何实际的影响。即使留下多个未关闭的组,系统也能继续运行,但从代码规范和清洁度角度来说,最好能够在编译时就捕获到这个错误,避免不小心留下悬挂的组。

虽然没有一个明确的理由表明为什么这个问题必须解决,但从程序员的严谨性和代码的整洁性角度出发,最好能够在编译时进行检查,确保所有的组都正确关闭。

最终,尽管加入这些修改可能会导致崩溃,但在接下来的工作中,这些问题仍需要解决。关于重试和添加功能的部分,虽然有一些进展,但目前还没有到达能够继续推进的阶段,需要在下周一的工作中进一步处理这些问题。在此之前,也没有对某些修改进行彻底的检查和验证,因此还需要对之前的修改进行回顾和分析。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

你提到曾经花了10周时间解决一个问题。那个问题是什么?

在之前的讨论中提到,曾经花费了很长时间解决一个问题,可能超过了十周。虽然不确定是否具体花费了十周时间,但在处理一个名为“目击者碰撞检测”(witnesses collision detector)的问题时,确实投入了大量的时间和精力。这个问题涉及到多个方面,其中只有一部分与最接近的向量有关,但整个过程可能总共花费了接近十周的时间,甚至可能超过了十周。

除此之外,还提到了在解决“曲线求解器”(Curve Solver)问题时,花费了超过十周的时间。这项工作当时是全新的领域,几乎没有人涉足过类似的任务,因此需要从已有的文献中找灵感,借鉴那些已经在图形和动画领域中做过类似曲线求解的人们的工作。这个过程非常具有挑战性,需要自己摸索和思考如何将这些方法应用于动画领域。

总的来说,面对这些复杂的技术问题时,常常需要花费很多时间和精力,甚至长达数周或数月,才能找到解决方案。在这些过程中,尝试不同的思路和方法是很常见的。

你有没有考虑过使用类似 Linux 内核中那样的内联链表,使用 offsetof()?

在讨论是否使用侵入式链表时,提到了一般情况下确实会使用侵入式链表,特别是在能够使用的情况下。侵入式链表是一个常见的、在许多系统中都能找到的有效结构,例如Linux中使用的链表。然而,在当前的情境下,不能使用侵入式链表,因为有一些元素需要出现在多个链表中,这种情况下就无法使用侵入式链表了。

如果元素不需要出现在多个链表中,那么可以回到原来的做法,使用侵入式链表。在之前的实现中,确实使用了侵入式链表。侵入式链表的特点是每个元素都会直接包含指向下一个元素的指针,因此不需要额外的内存或结构来存储这些指针。

但是,在当前的需求中,元素需要在多个链表中出现,这使得必须使用外部链表。外部链表允许元素在多个链表之间共享,而不需要修改每个元素本身的结构,因此无法使用侵入式链表。

不过,还是有一个侵入式链表在使用,那就是树结构。树结构本身就是一种侵入式链表,因为每个节点都会包含指向其子节点的指针。因此,尽管某些地方不能使用侵入式链表,但在树的实现中,仍然采用了这种结构。

游戏引擎的可移植性如何?能用于制作其他游戏吗?

关于游戏引擎的可移植性问题,首先,提到这个引擎会确保能够在多个平台上运行,实际上已经有人将其移植到 STL 等平台上。所以在可移植性方面,它是非常有潜力的。

然而,至于是否能用来制作其他类型的游戏,答案是引擎并没有被设计成支持多种游戏。它的目标是为特定的游戏提供支持,而不是成为一个可以制作多种游戏的引擎。因此,是否能用于其他游戏并不在这个项目的目标范围内,所以并不关心这一点。

总的来说,虽然引擎本身具有一定的可移植性,能够运行在多个平台上,但它并不打算被用来支持多个游戏的开发,其主要目的是支持特定的一个游戏。

曲线求解器?

“曲线求解器”这个术语其实就是用来描述一种“曲线拟合”的过程。它的功能是处理一些原本不是曲线的数据,将这些数据转化成曲线。具体来说,它会接收一组数据点,并在这些点之间生成一条平滑的曲线,或者通过某种方法拟合出一条最合适的曲线。因此,曲线求解器的核心作用就是将离散的数据点连接起来,形成一条连续的曲线。

我们会看到内存块调试可视化吗?

关于内存调试保留(memory chunk debug reservation)的问题,提到将来肯定会做这项工作,但目前还不确定具体如何实现它的可视化。内存调试的可视化比较困难,因为内存中有很多细小的元素,因此很难做出直观的表现。虽然无法保证可视化效果会非常好,但可能会采用一种方式,例如用一条线表示内存,并在上面标出一些小块来表示不同的内存区域。尽管如此,这项工作还是会在未来进行的。

顺便说一下,我认为 Linux 内核的实现允许一个元素出现在多个列表中

关于侵入式链表的问题,指出了侵入式链表的一个限制,即它不能让同一个元素同时出现在多个链表中。虽然可以通过链表将元素连接起来,但每个元素只能属于一个链表,不能在多个链表中共享。这意味着,尽管多个线程可以通过同一个链表连接,但同一项数据不能同时出现在多个链表中。

黑板:链表

在讨论侵入式链表时,指出了一个关键点:一个元素在任何时候只能属于一个链表。具体来说,链表中的每个元素通常会有“下一个”和“上一个”指针,用于连接链表中的其他元素。一个元素不能同时存在于两个相同类型的链表中。然而,如果为每个链表类型使用不同的变量集,则同一个元素可以出现在两个不同类型的链表中。也就是说,元素可以在不同的链表中出现,但每个链表类型的元素只能属于其中一个。

这一规则并不是与实现相关的细节,而是与基本的类别理论相关,它表明了元素在同一类型的链表中只能出现一次。这是一个概念性的限制,无法绕过。

能再给我快速回顾一下你是如何用栈替代调试视图实例的吗?

请求再次简要说明如何将堆栈替换为调试视图的实例,因为目前似乎还没有完全理解这一点。实际上,问题的核心是如何将堆栈中的数据替换为调试视图实例,以便在调试过程中进行更好的数据查看和操作。

你通常使用什么“格式化语言”?

通常不会使用格式化语言,自己也很少这样做。这种做法对自己来说并不常见。一般来说,更多的是以语义化标记来处理内容,比如标注某些词语为专有名词或者通过链接来引用相关内容,但并不会像某些程序那样指定某部分内容为斜体或进行其他格式化。自己写的程序通常不需要这些格式化的细节,因此并没有遇到过需要这种功能的情况。并不是说这种做法不好,只是自己没有遇到过实际需要它的场景。

这就是你在其他程序中做 UI 的方式吗?

有些方面是自己已经做过的,有些则是全新的尝试。现在正专注于以一种特定的方式来处理用户界面(UI),希望展示如何设计一个优秀的UI结构。故意选择了一种与以往不同的方式进行设计,希望通过这种新的尝试,能够找到一些新的、有益的做法,或者从中学习到一些有价值的经验。通常,在自己做这类事情时,也会故意尝试不同的方法,看看是否能通过改变之前的做法获得一些新的收获。

猜你喜欢

转载自blog.csdn.net/TM1695648164/article/details/146996593
今日推荐