游戏引擎学习第218天

发布于:2025-04-12 ⋅ 阅读:(34) ⋅ 点赞:(0)

构建并运行,注意一下在调试系统关闭前人物的移动速度

现在我准备开始构建项目。如果我没记错的话,我们之前关闭了调试系统,主要是为了避免大家在运行过程中遇到问题。现在调试系统没有开启,一切运行得很顺利,看到那个角色快速移动的效果,真是挺不错的。

但是,如果我们想要调试功能正常运行,我们需要改变计划,把调试系统重新打开。现在,我们回到了昨天的工作状态,之前我们就在进行一些修改,所以接下来我们继续回到这些修改上,继续前进。

game_debug_interface.h:回顾GUID概念

现在我们回到了之前做的工作,我们正在处理调试记录的部分。我们希望能够确保无论在调试记录的哪个阶段,每一个记录的事件都有一个唯一的标识符,这样我们就能够清晰地知道文件中的某个位置,并且能把从这个位置来的所有事件整合在一起,作为一个整体来查看。

不过,我们没有真正改变调试事件的定义。现在我们有了一个机会来处理这个问题,因为我们去掉了行号,现在只剩下液体(可能是指某种数据或状态),而“块名称”仍然存在,这跟之前的处理不同,我不确定是否要保留块名称,未来可能会考虑把它去掉。

接下来,我们需要检查是否缺少了反斜杠,可能在某个地方遗漏了,不过现在看起来应该已经加上了。接下来就只需要加一个分号,应该就可以了。

game_debug_interface.h:传入GUID并让其生效

在这里,我们传入了GUID和其他相关内容,我们希望做的事情是,实际上把一个独特的标识符传入,而不是原先的内容。为了实现这一点,我们需要调整代码,确保它能正常工作。

接下来,我们需要获取这个“GUID”(可能指某种数据或状态)。要实现这一点,我们需要修改之前的代码,使其能够适应新的方式。我们正在尝试进行一些改变,看看是否能通过某种方式使它可行。我不确定这样做是否能成功,但还是决定尝试一下。

如果我们这样做,那么每次调用“调试初始化值”时,显然它将变得不正确。我们必须确保这一点能够正确运行,并且如你所见,正在出现一些错误提示——调试初始化值时出现问题。我们接着去找出问题所在,发现问题仅出现在两个地方,所以解决起来其实比较简单。

在这些地方,我们传递了文件名和行号,但现在我们需要做的是去掉这些行号,只保留文件路径,因为文件路径是需要保留的。接下来,我们要把本应存放“标识符”的位置替换成独特的标识符字符串,希望这样做能起作用,虽然我不确定是否能成功,但我还是决定这么试试看。

在这里插入图片描述

在这里插入图片描述

对宏处理器的思考ε

宏的使用过程总是有点像一场冒险,因为我们从来记不清楚宏处理器具体是怎么工作的,只记得它通常就是一大堆乱糟糟的东西,总是带来各种混乱。

现在我们正在处理“添加变量到分组”这部分内容。这意味着我们大致已经解决了之前由于唯一标识符引起的错误问题,现在开始回到调试系统的结构性修改上来。这些结构性部分需要根据前一天做出的设计决定进行相应的调整。

我们接着准备编译一次,这是个提醒,同时也让我们想到一件事,是早上洗澡时脑海里突然冒出来的。就像平时一样,大脑时不时会跳回到我们正在做的某个编程任务上,忽然想到一些新的想法或是之前忽略的问题。于是我们决定趁热打铁,把这个念头也一起整理实现。

思考内存分配

突然脑子不自觉地飘到了内存分配上。忽然意识到之前写的那部分代码其实是错的。尽管目前还没真正依赖那段逻辑运行,但问题已经存在了。我们写了一个判断逻辑,检查内存区域是否还有空间,比如 ArenaHasRoomFor,但这种做法本质上就是不对的。

问题出在我们对内存的使用策略上。我们的设计并不是要真正“释放”内存,而是打算对旧的内存进行回收和重用。所以在这种模型下,不管释放多少帧,内存区域技术上永远不会“有空余空间”,因为我们不是让内存回到系统,而是自己循环使用。

这就导致了当前的判断方式失效,根本无法真实反映当前是否能安全分配新事件数据。我们真正应该做的是改写分配器逻辑,在尝试分配事件数据时,先看看有没有旧的数据可以回收复用。如果有,就用旧的;如果没有,就尝试“释放”一些老的帧以腾出空间。这样才符合我们的内存管理思路。

所以,这段逻辑是错的,不但写得太早,也写得太保守了,必须重新设计,才能真正契合我们的需求。

这是个典型的的问题,属于那种特定时间特定场景下必须处理的事情。更像是一种日常维护性质的操作

我们面对的问题就有点像这种,不是特别重大的结构性更改,也不是早期启动阶段就能立刻解决的,而是开发过程中中段出现的,需要专门抽时间处理的。虽然它不复杂,但也不能忽略,否则整个系统的内存处理会变得混乱。
在这里插入图片描述

game_debug.cpp:使CollateDebugRecords根据来源将元素放入合适的位置

继续推进调试系统的结构修复,当前的重点是实现一个机制,使所有进入系统的事件都能按照它们在代码中出现的位置进行组织和归类。我们希望每当出现一个事件,收集器都能自动将其放入一个与其来源位置对应的分类中,从而实现统一管理和后续查看。

为此,我们设计了一个名为 GUID 的标识符,它是根据事件发生的代码位置在编译期生成的唯一字符串。这个字符串通过拼接路径、文件名、行号等信息,在编译期间就被写入可执行文件的字符串表中。由于每个这样的拼接字符串在内存中都有唯一的地址,因此我们可以利用这个地址本身作为键值,不必再执行传统的字符串哈希计算。这样做的优点是显著提升了性能,因为省去了遍历字符串计算哈希的过程。

因此,所谓的“字符串哈希”在这里并非传统意义上的哈希操作,而是直接使用这个唯一字符串在程序中对应的指针作为哈希键。因为这些字符串都是唯一的,指针也一定唯一,不会发生碰撞,因此这种做法既安全又高效。

接下来我们要做的是在 CollateDebugRecords 这个函数中实现归类逻辑:遍历所有调试事件,每次从事件中提取 GUID,然后使用这个指针去查找或创建一个对应的调试元素。可以理解为我们为每个事件来源位置创建了一个桶,所有来自这个位置的事件都会被放入同一个桶中。

为了实现这个过程,我们需要一个函数,比如 GetElementFromEvent,该函数接收一个事件作为参数,从中提取 GUID,并返回与该位置对应的调试元素。这样一来,我们就可以在整理事件记录时,快速而准确地将它们分类,从而实现结构化的调试信息展示。

总结来说,这一设计既高效又整洁,利用编译期字符串唯一性的特点,实现了无需运行时复杂计算的事件归类逻辑,为整个调试系统的稳定运行打下了坚实基础。接下来只需将这些逻辑整合到系统结构中,就能完成核心功能的构建。
在这里插入图片描述

game_debug.cpp:引入GetElementFromEvent

当前的目标是从调试事件中提取唯一标识 GUID,然后根据这个标识从哈希表中找到对应的调试元素。整体过程非常直接,即:遍历事件,提取 GUID 指针,基于该指针计算哈希值,然后在哈希表中查找或插入对应的元素。

在实现上,使用 GUID 字符串的内存地址作为哈希基础,因为这些字符串在编译期间就已经生成并保存在可执行文件的字符串表中,它们的指针地址是唯一的,所以不需要执行字符串内容的对比。这种方式不仅简单,而且极为高效。

不过在处理指针转整数用于哈希计算时,需要注意编译位数的差异。指针在 64 位模式下是 64 位整数,在 32 位下则是 32 位。因此,在统一哈希逻辑时,直接使用指针的低位可能不够分散,而高位可能因地址集中也缺乏区分度。

解决方式是:将指针值先转换为整数,再右移若干位,以跳过那些低位的对齐部分,因为字符串在内存中的地址通常会是对齐的,低几位不具区分性。最终通过位移后的整数值作为哈希值,并对哈希表的长度取模,确定该 GUID 应该插入的哈希槽索引。

之后,通过该索引找到哈希槽内的链表头部,开始遍历。遍历过程中,不是用字符串比较,而是直接比较 GUID 的指针地址,因为这些指针地址本身就是唯一标识。

总结来说,整个哈希流程包括以下几个关键步骤:

  1. 从事件中取出 GUID 指针;
  2. 将该指针转为整数后做右移,舍弃低位无用部分;
  3. 对哈希表长度取模,得到索引;
  4. 遍历该索引处的链表,比较 GUID 指针,找到已有元素或插入新元素。

这样设计的哈希系统充分利用了编译期字符串唯一性,大大减少了运行时开销,也避免了复杂的字符串比较逻辑。系统在结构上更为简洁清晰,性能也更有保障。接下来可以继续在此基础上构建完整的调试信息归档与展示流程。
在这里插入图片描述

在这里插入图片描述

game_debug.cpp:继续实现GetElementFromEvent

当前的逻辑是完成调试事件的哈希表插入流程,即从事件中提取唯一标识(GUID 指针),通过哈希算法查找已有元素,若未找到则新建一个并插入哈希表。

首先,遍历哈希表中某个桶位的链表,看是否已经存在目标元素。如果存在,则直接使用已有元素作为结果返回。虽然中途直接返回是可行的,但为了结构清晰,保持函数有明确的退出点,因此选择使用一个结果变量记录查找结果,再在结尾统一返回。

如果在链表中未找到已有元素,则说明需要创建新的调试元素。这时,使用常规的结构体内存分配器(暂不考虑之前构建的特殊分配器),从调试状态的 arena 中分配一个 DebugElement 结构体。

新分配的元素插入哈希链表的方式很常见:设置新元素的 NextInHash 指针指向该位置原有的第一个元素(可能为空),然后将哈希表当前位置指向新元素,完成链表头插入操作。

在新元素创建后,需要进行初始化,包括:

  • NextInHash 的链接设置;
  • OldestEventMostRecentEvent 清零;
  • 记录 GUID 指针;
  • 并插入断言,确保 GUID 不为 null。

这条断言非常重要,因为整个系统是基于 GUID 指针的唯一性来建立索引逻辑的,如果某个元素没有 GUID,那它在哈希表中的表现会和未初始化一样,可能导致严重问题。该断言就是为了及时捕捉这类缺失,以避免系统出现潜在错误。

此外,由于这些 GUID 指针在整个程序中是唯一并不释放的字符串,所以不会有指针复用或指向非法内存的风险。

最后,完成这些操作后,编译整个模块,确保流程的正确性。从逻辑来看,这套哈希查找和初始化流程是稳定、清晰、有效的,下一步即可在这个基础上继续构建事件整理与分析系统。
在这里插入图片描述

在这里插入图片描述

这个比喻就像是有人帮我们把午餐送来了,听起来是一件很棒的事,因为我们不用自己动手,但实际情况可能没那么理想。就像是收到了一份来自陌生餐厅的午餐,虽然省去了准备的麻烦,但内容未知,可能会令人失望。

比如,有可能送来的是那种味道不讨喜、外观黏糊糊、甚至带着胶状酱汁的冷冻肉饼之类的东西。如果我们真的很饿,也许还是会吃下去——毕竟饥不择食。但如果我们本身有能力做饭,或者有条件点一份更好的外卖,那我们大概率是不会选那种质量低劣的食物的。

这就意味着,虽然自动生成的结果省事,但仍需要保持警惕和判断力。最终的选择,还是取决于我们是否愿意接受眼前的这个“现成午餐”,还是更愿意投入一些精力去获取更好的结果。这个比喻也侧面反映出一个态度:工具虽然方便,但质量还需自己把控,不能盲目接受。

game_debug.cpp:#if 0 创建变量,添加变量到组,创建变量组和释放变量组

我们现在还有一些收尾工作要做,就像之前提到的,需要对当前的结构做一些清理。特别是关于“创建变量组”的这部分,目前其实并不是当下真正需要关注的内容。

当前这一部分的逻辑,在现在这个阶段来说并不关键,暂时也没有立即的用途。因此,我们在思考是不是可以先把这部分“清零”或者暂时搁置掉。这样做的原因是,我们目前的重点并不在这里。

实际接下来要做的事情,是围绕新的结构逻辑展开的。也就是说,接下来系统的行为将会按照新的调试结构进行,而不是现在这些临时或者旧的变量组逻辑。

总结来说,就是现阶段这些变量组的内容可以暂时搁置或者清理掉,把注意力集中到即将生效的逻辑结构上,这样能够避免当前工作被不相关的代码干扰,也能让后续的改动更加清晰、聚焦。
在这里插入图片描述

game_debug.cpp:引入StoreEvent

我们接下来要处理的是在特定位置,把传入的事件插入到对应的调试元素结构中。

在这一段逻辑中,我们已经完成了查找对应元素的操作,因此接下来的工作其实就是将这个事件插入到对应的“事件流”中。换句话说,每当有新事件到来,我们就会把它丢进它所属于的那个调试元素结构里。这个调试元素的哈希是我们之前通过定位已经找到了的。

所以,现在我们只需要将事件追加到那个当前激活的元素结构中就可以了。逻辑上非常简单直观,尽管整体系统还有很多要完善的地方,但目前这一部分开始逐步形成较为清晰的结构。

此外,还有一件之前做得不太合理的地方也要修正。之前我们为了分配每一个事件,强行让所有数据都通过一个统一的分配函数走了一遍分配逻辑。这个做法在某些场景下显得有些笨拙或不够聪明,实际上并不是每一类数据都需要如此对待。

比如说,我们这里只需要为“存储事件”进行专门处理。因为这些“存储事件”是整个调试结构中唯一一类会被反复分配和释放的数据,其它结构都是常驻的,不需要反复处理。而这些事件在系统运行时会持续积累,直到超过容量之后才会回收旧的事件。因此,我们决定为它单独设置一个“事件存储接口”。

具体来说,我们会新建一个函数,比如叫做 StoreEvent,专门处理调试事件的存储。调用这个函数时传入调试状态,它会内部完成事件的分配工作。

完成分配后,我们还需要对新分配出来的事件结构进行初始化:

  • next 指针的设置(用于构建链表结构)
  • 当前帧索引的设置(这个信息在我们当前上下文中是已知的,可以直接填入)
  • 事件数据的复制(这部分最简单,直接从输入的事件中拷贝数据)

通过这种方式,我们的事件处理变得更加清晰、集中,同时也避免了不必要的复杂操作,提升了系统的效率和可维护性。整体上来说,这种设计在系统结构日益清晰的同时,也为后续的扩展和调试打下了基础。

在这里插入图片描述

"轻松简单像挤柠檬汁一样"κ

这段内容提到的是一种轻松的氛围,类似于一种茶文化中的习惯。讲到的是,当你和别人一起喝茶时,有一种常见的玩笑话或者习惯,那就是大家围坐在一起时,会拿起一片柠檬,做一个夸张的动作,说“easy peasy lemon squeezy”,然后把柠檬挤进茶里。不过,这个做法并不适合所有种类的茶。如果是加了牛奶的茶,就不适合放柠檬,因为柠檬会让牛奶发生凝固反应,影响口感。所以,喝加牛奶的茶时,不建议加入柠檬,避免这种不合适的搭配。

这一段主要是在讲解一种习惯的趣味性,带有一些幽默的表达。

game_debug.cpp:继续处理StoreEvent

这一段内容讲述了调试元素(debug elements)在处理事件时如何通过链式结构来组织事件。在这个结构中,调试元素会按顺序将事件堆叠起来。具体来说,每个调试元素的“下一个”事件(next)初始时为空(即为零),因为事件是按顺序链接在一起的。

如果一个调试元素还没有任何事件(即“最早事件”和“最近事件”都为零),那么就把当前事件作为这个调试元素的唯一事件,同时它会是这个列表中的“最早事件”和“最近事件”。如果调试元素已经有事件了,那么当前事件就会被插入到事件链中,成为“最最近事件”,并且它的“下一个事件”指针会指向之前的事件。

这种处理方式有些独特,带有一些非传统的思路,但这种方式也许有其独到的地方,能够在特定场景下简化处理或增加趣味性。
在这里插入图片描述

game_debug.cpp:删除所有的Region内容,#if 0 删除AddVariableToGroup

这段内容描述了在当前情况下,对调试过程中某些功能的简化和调整。首先,决定暂时不使用与“区域”(region)相关的功能,可能是因为这些功能当前不需要或者会增加不必要的复杂性。接下来,提到要禁用一些功能的代码段,表明这些功能暂时不需要。

对于标记调试值的功能(mark debug value),决定保留目前的做法,继续执行它的当前功能。接着,提到可能需要将“存储事件”(store event)功能提取出来,作为一个工具函数(utility function),这样可以在多个地方调用,简化代码并提高可复用性。这是为了让代码更加模块化,减少重复,并提升整体结构的清晰度和效率。
在这里插入图片描述

game_debug.cpp:实现StoreEvent

这段内容描述了在代码中的调试事件处理部分。首先,提到创建了一个 store event,并且在检查了调试状态、不同元素以及调试事件之后,认为这个部分的工作几乎完成。接下来,讨论了是否需要做一些额外的修改,最后决定不再进行太多更改。

然后,关注点转移到了如何获取实际的存储事件(stored event)。虽然当前还不知道如何获取这些事件,但明确表示这是接下来的目标。之后,提到出现了一个编译错误,似乎框架中的 debug element 没有成员 frame,这引发了困惑。最后提到在代码中查看了相关的字段,如 oldest eventmost recent event,并怀疑是否有误或存在错误。
在这里插入图片描述

game_debug.cpp:修复编译错误

首先,提到在代码中还涉及到add region的部分,但这部分仍然在处理与区域相关的内容,因此决定暂时将其去除。接着,由于操作时不小心过于激进地使用了快捷键,导致创建了两个版本的编译,修正了这一点并恢复了代码。

然后,讨论到创建变量组的部分,认为这一部分目前并不重要,所以决定暂时不关注它。同样,matching block eventsDebugType_OpenDataBlock DebugType_MarkDebugValue等内容也被认为暂时不需要关注,因此也没有进一步处理。接着,确认了store events已经处理完毕,代码达到了一个较为清晰的状态。

之后,提到自己猜测随着代码逐渐整理和优化,可能会清理掉一些冗余的代码,但这一工作会留到后面做。接着明确表示不再关注与值组和树结构相关的内容,重新回到了代码的核心部分。

在这里插入图片描述

在这里插入图片描述

game_debug.cpp:销毁PushSizeWithDeallocation

首先,决定完全抛开之前的错误操作,因为那些操作根本不应该发生,完全不符合预期,因此选择“抹去”这些操作,忽略它们的存在,假装它们从未发生过。接着,决定将所有相关内容恢复到原来的状态,认为那只是一次简单的失误,并且坦诚承认这个错误。

此后,强调不需要再去讨论这个错误,已经承认并且道歉过了,不想再继续纠结于此。明确表示大家应该放下这个问题,向前看,继续推进工作。
在这里插入图片描述

在这里插入图片描述

game_debug.cpp:确保必要时释放StoredEvents

现在理论上,所有目标已经基本达成,除了一个问题,就是事件处理方面的情况。接下来需要处理的,就是如何管理调试框架和存储事件。具体来说,需要确保每当新的帧和事件进入时,系统能够根据需要释放帧空间,以便存储新的事件。

关键的操作是,当新帧或存储事件需要分配新的内存时,如果当前没有足够的内存,可以释放一个旧的帧空间,腾出空间来为新的事件或帧分配内存。

所以,首先需要处理的是分配内存的问题。每次要分配内存时,系统要先尝试从空闲列表中获取所需内存块。如果能成功获取,就直接分配;如果不能,则需要释放一个帧空间,腾出内存。这个过程类似于循环,反复尝试从空闲帧中分配内存,直到成功为止。

另外,代码中提到的分配和释放内存操作,应该通过宏来实现,以保证代码的简洁性和一致性。同时,可能需要在某些地方做一些细节优化,比如简化某些判断或操作。

最后,系统将首先尝试从空闲列表中获取内存块,如果获取成功,则直接使用;如果没有足够的内存,就会触发释放帧的过程,直到有足够的内存可用。

在这里插入图片描述

game_debug.cpp:引入FreeOldestFrame

在这个过程中,需要特别注意释放帧的操作。对于需要释放的最旧帧,首先要确认当前是否有可以释放的帧。如果有,我们就执行释放操作。具体来说,释放最旧帧的步骤是:首先检查是否存在最旧的帧,如果存在,进行释放。为了确保在释放之前已经正确处理了帧的指针,释放后还需要更新指向下一个帧的指针。此时,还需要特别注意调试状态中最新的帧指针,若最新的帧就是被释放的帧,则需要将其指针设为零,表示没有最新帧。

此外,为了避免出现异常情况,在执行释放最旧帧的操作时,可以加入一个断言,确保当没有帧可释放时,函数不应该被调用,这样可以防止程序进入不一致的状态。

在处理新的帧时,首先尝试分配内存。如果内存不足,则会调用释放操作来腾出空间。这个过程与帧的处理非常相似,只不过这里是针对存储事件。与帧处理类似,存储事件也会先尝试从内存中获取,如果没有足够的内存,就需要释放最旧的存储事件以腾出空间。

总的来说,框架和事件的内存管理逻辑非常相似,关键的区别在于操作的是帧和事件这两种不同的数据结构,其他处理流程基本一致。
在这里插入图片描述

编译,修复编译错误,确保其他功能正常

首先,确认了 free frame 操作的正确性,确保它在释放帧时能够正常工作。接下来,进行了一些清理工作,以确保所有相关代码都处于合理的状态。

接着,检查了代码中的其他部分,确认它们也都执行了合适的操作,确保整个系统的逻辑是连贯和一致的。特别是需要确保 free frame 函数能够正确地释放帧,并进行必要的更新,以便在没有帧可释放时不会发生错误。

最后,进行了进一步的清理工作,以确保所有的代码部分都整洁且逻辑清晰,为接下来的开发或者调试打下基础。
在这里插入图片描述

在这里插入图片描述

game_debug.cpp:在NewFrame中添加if(ArenaHasRoomFor)

首先,回顾了代码的逻辑,意识到有一些部分的代码设计不够周全,特别是在处理帧的释放时,最初认为每次都需要释放帧,但实际上只有在内存不足的情况下才需要释放。因此,修改了这个部分的逻辑,只有在没有足够内存时才进行释放。

接下来,明确了内存管理的关键步骤:首先检查当前是否有足够的内存来存储新的数据。如果有足够的内存,则直接分配;如果内存不足,则进行释放操作。这个改进确保了内存的有效利用,避免了不必要的内存释放操作。

另外,还考虑到将这一过程抽象成一个通用的函数或方法,可能会使代码更加简洁和可重用。不过,目前还没有决定是否要进一步抽象,可能需要进一步思考和实验。

最终,修改后的逻辑更加高效、简洁,确保了内存的合理管理,避免了重复的释放操作,保证了代码的稳定性和性能。

在这里插入图片描述

game_debug.cpp:制作一个PerFrameArena SubArena

在代码的这一部分,首先讨论了如何设置和初始化一个子内存区域(subarena),它用于处理特定的内存分配需求。通过设置一个子内存区域,可以专门为帧(frame)数据分配内存,从而提高内存管理的灵活性。

首先,考虑到内存的总大小,决定为子内存区域分配一定比例的内存。例如,可以将总内存大小的三分之一或四分之三分配给这个子内存区域。这个比例需要根据实际需求进行调整,以确保内存分配的合理性和性能优化。

在实现时,需要确保对内存空间的合理划分,并且能够判断内存是否足够。如果子内存区域有足够的空间来存储新的数据,就可以继续进行数据的存储操作。如果内存不足,则需要采取适当的释放和回收操作,以确保系统能够稳定运行。

此外,提到的arena has room for方法被用来检查当前内存区域是否还有足够的空间。这个方法会通过传递帧或存储事件的大小来进行检查,以决定是否需要释放或重新分配内存。

总结来说,通过合理设置子内存区域,能够更高效地管理内存资源,避免内存浪费或不足的问题,从而提升系统的性能和稳定性。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

调试器:进入PushSize,发现没有足够的调试内存

在这一部分,讨论了内存分配的具体细节,特别是在为调试数据分配内存时所需的空间。首先提到的是内存的总容量为64,但是在实际分配时,可能会决定只分配总容量的一半作为调试内存区域的空间。这是因为在系统初期阶段,可能并不需要为调试信息分配过多内存,但随着系统的运行和需求的增加,未来可以根据需要调整这部分内存的分配。

提到的AllocateRenderGroup是与内存分配相关的操作,实际上是为了确保调试数据的内存可以正常分配。虽然目前选择分配的内存空间较少,但这个决定可以在后续的开发过程中根据实际需求进行调整。例如,可能会选择为调试数据分配更多的内存,以提高性能或避免内存不足的情况。

总的来说,这是对内存管理的初步设定,提供了灵活调整内存分配大小的空间,以便根据实际使用情况优化内存使用。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏并注意到我们正在存储所有调试数据

现在理论上来说,已经完成了所有调试数据的存储和恢复,且这些数据是根据其来源位置进行管理的。接下来,目标是通过这些数据来填充层次化的视图,以便恢复到之前的状态,且不再受到之前限制的影响。理论上,这一目标已经接近完成。

然而,时间上已经所剩不多,距离结束大约还有13分钟。因此,需要思考如何选择优先完成的部分,或者从哪一部分开始着手,以最大化剩余时间的利用。

总的来说,当前的工作重点是通过已存储和恢复的数据,来提升系统的功能,特别是在层次化视图方面,最终实现目标。

game_debug.cpp:调查为何我们的Last frame time不正确

首先,要弄清楚为什么最后一帧的时间不正确。检查了“most recent frame wall seconds elapsed”,但发现问题依然存在。每次进行帧标记时,应该会记录该帧的时间,理论上这部分的处理应该是正确的。所以疑问是,为什么没有得到正确的值。

接着,检查了“frame marker”和“seconds elapsed”的设置。看起来它确实在正确记录时间。那么可能的问题在于“most recent frame”列表的更新。具体来说,关于“most recent frame”列表,似乎没有完全理解数据是何时被添加进去的。

经过进一步检查,发现“frame marker”部分使用了断言来确保数据在“collation frame”中被正确放置,并且在完成帧合成后,再将其放入列表中。按理说,这部分应该是正常工作的。

接下来,会进一步深入“frame marker”代码,看看到底哪里出了问题。

调试器:触发FrameMarker并检查值

程序正在运行,已经到达帧标记的位置。此时,查看了正在处理的合成帧,发现“swell second flats”被设置为零,这是符合预期的。接下来,程序会设置一些值,值看起来是正确的,然后将数据添加到列表中。然而,最最近的帧的时间(“most recent frame wall seconds elapsed”)是错误的。

这部分的错误解释了为什么显示的时间不正确。在这里,应该同时设置“most recent frame”信息,而这部分没有被正确处理。进一步检查时,突然发现可能是因为Visual Studio的调试功能导致文件被意外修改。程序员对Visual Studio的这种行为感到不满,尤其是不希望它自动写入文件。希望能够设置调试模式,让它不触碰文件,避免这种情况发生。

game_debug.cpp:正确设置MostRecentFrame

现在想要解决的问题是,最最近的帧的时间(“most recent frame wall”)这一部分没有按照预期正常工作。经过调整后,理论上应该可以恢复正确的帧时间。
在这里插入图片描述

运行游戏,看到Last frame time现在正确了

现在问题已经解决,帧时间恢复正常,这正是预期的结果。这意味着帧的处理正在按预期进行。接下来,开始对当前的情况进行一些统计分析,进一步了解和优化系统表现。

game_debug.cpp:打印出Arena的内存使用情况

接下来,想要了解当前存储事件的情况,包括它们占用了多少内存。为此,计划在代码中输出该内存使用情况,特别是想查看在当前“arena”中已用的内存大小。为了做到这一点,可以使用现有的函数来获取内存剩余量,并通过调整对齐方式(假设为1字节对齐)来准确计算实际的内存使用情况。然后,将结果以千字节为单位打印出来,这样就能方便地查看实际占用的内存大小。

需要注意的是,需要确保正确地传递地址以获取所需的信息。

在这里插入图片描述

运行游戏,看到Per-frame Arena的剩余空间

目前的测试并不能迅速测试内存占用情况,因为现在存储的事件数量远不足以消耗掉内存。由于事件推送量很少,这种情况也并不让人感到意外。实际上,这个测试的设计还只是一个初步的测试片段,因此不会快速触发内存的耗尽。

game_debug.cpp:缩小该SubArena的大小

为了加速测试过程,可以考虑将内存分配设置得极小,比如分配 30KB 或 50KB 的内存,这样程序会很快用完内存,从而更快地测试内存的耗尽情况,这样的做法会使得测试能够迅速验证内存管理的效果。
内存不停减少
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,看到新的Per-frame Arena剩余空间

现在,程序的内存分配已经按预期工作,当内存耗尽时,它会降到零,并开始回收内存。如果内部的回收机制也正常运行,那么一切就按预期完成,结果非常理想。

调试器:进入StoreEvent并确保一切正常

目前,程序的内存分配和回收机制已经按预期工作。经过检查,程序运行正常,内存回收过程也顺利进行。接下来,计划将所有的UI相关部分重命名,并开始利用现在集中管理的机制进行进一步的调整。这一部分工作完成后,程序就能顺利地继续发展。现在,程序已经处于一个良好的状态,接下来的目标是将所有相关功能进一步整合优化。

如果你可以在C和Java类似的语言中选择,你可以像C一样操作内存,但这门语言会告诉你如何实现一些概念(像Python如何处理行偏移,可能不太好)。那时你会选择什么?为什么?

需要在C语言和Java类似的语言中选择一个来处理内存和其他工作,那么选择会非常明确。

C语言是非常合适的,特别是在进行底层内存管理和硬件交互时,C语言提供了很高的控制性。它对于执行效率要求较高的系统是非常好的选择。然而,当需要进行更高层次的组织、代码生成以及更加抽象的任务时,C语言的劣势就显现出来,因为它缺乏自动化的内存管理(如垃圾回收),并且在处理更复杂的编程任务时会显得繁琐。

至于Java,它的内存管理自动化是一个优点,但它的使用场景并不总是合适。在很多复杂的、需要细粒度控制的任务中,Java显得不够高效和灵活,尤其是在性能要求高的系统中,Java的垃圾回收机制可能会成为一个瓶颈。因此,对于需要精确控制和高效性能的任务,C语言比Java更具优势。

如果选择的话,我会倾向于使用C语言,尤其是当需要进行底层系统编程时。但如果是处理更高级别的编程任务,选择时我可能会偏向使用其他语言,特别是那些能够提供更好抽象和内存管理功能的语言。

提个警告:优化器可能会把字符串存储在同一位置,这会打破整个GUID系统

如果说到"atomizer"可能将字符串存储在相同的地方,这可能会导致整个(GUID)系统出现问题。根据理解,无法将两个字符串存储在同一位置,特别是它们的内容是不同的。因此,除非有其他的想法,否则这种情况是不可行的,因为不能在相同的存储位置存储不同的字符串。

你觉得在基于测试的编码和更高效/优化之间有完美的平衡吗?还是更喜欢其中一种?

关于是否在测试驱动开发(TDD)和效率优化之间找到完美的平衡,实际上是一个关于时间和质量的权衡问题。测试驱动开发是一种合理的做法,前提是它不会导致开发时间过长。如果在开发过程中,编写测试的时间比后期调试错误的时间更少,那么测试驱动开发就是值得做的,因为它能帮助提前捕捉到潜在的问题,减少后期的调试工作。如果测试所花的时间超过了后期调试的时间,那么这就是浪费时间,对开发没有任何帮助。

开发的目标应该是以最少的时间写出最好的代码,这是核心。如果测试驱动开发能够帮助达成这一目标,那就值得做,但如果它拖慢了进度或影响了效率,那么就不值得强迫执行。因此,开发者不应盲目遵循某种做法,而应根据实际情况来决定是否采用测试驱动开发。

每个人的编程习惯和错误类型不同,因此编写的测试也应该根据个人的错误倾向来调整。例如,有的人经常犯某种类型的错误,而另一些人则倾向于犯不同的错误。针对这些不同的错误类型,测试的重点也会有所不同。因此,测试驱动开发应该根据开发者自身的经验和习惯量身定制,而不是遵循一成不变的规则。

总的来说,开发者需要根据项目的复杂性、错误的种类以及自身的编程习惯来决定是否采用测试驱动开发。最重要的是,无论使用什么方法,最终的目标是以最短的时间开发出最优质的代码。

你对clang的看法是什么?

对于Clang,有一些想法。首先,如果尝试在源代码树中构建Clang,源代码会显得非常混乱,简直是一场噩梦。它过于面向对象,很多操作需要数千行代码,而这些本该用一行代码就能完成的事情,这一点非常不喜欢。

不过,从产品的质量上来看,相对于Visual Studio的编译器,Clang实际上做得相当不错。它的编译效果相对较好,表现令人满意。

尽管如此,Clang的编码实践使得其源代码非常难以处理,可能比应有的效率低,也比应有的更难修改。因此,虽然Clang的质量相较于其他编译器很好,但在工作上的便利性和效率上,相对来说还是有一些欠缺。

我想他是指字符串字面量,像-Og标志,我认为

可能指的是字符串字面量,类似于旧的 “g” 标志,但具体意思不太清楚。

你对Unity等有什么看法?

关于Unity等游戏引擎,主要的看法是,它们的使用价值取决于是否能够满足所需的功能。如果引擎能做自己想要的事情,那就可以使用;反之,不能的话就不能使用。大部分人低估了使用像Unity或Unreal Engine这样引擎所涉及的复杂问题,尤其是在做高质量游戏时。虽然使用引擎能让开发变得简单,但仅仅依赖引擎并不能自动保证高质量,仍然需要大量的工作来确保最终的质量。

此外,引擎虽然可以帮助设计和开发,但如果你的设计需求超出引擎的典型使用场景(比如做出与引擎原本的演示效果大相径庭的内容),就需要更多的工作来调整和优化。尤其是对于设计决策方面,很多引擎的设计方式和使用方式并不符合自己的需求,所以可能不太喜欢使用它们。

还有一个重要的原因是,不想过于依赖别人提供的核心技术,尤其是在公司建设过程中,拥有自主掌控的技术平台更加有利。因此,尽管Unity等工具有其优势,但个人的做法可能更倾向于不使用这些工具,除非非常必要。但这并不意味着其他人不应该使用Unity等引擎。

迄今为止,在这个项目上你遇到的最大障碍是什么?

在这个过程中,最大的挑战并不是我们一直在面对困难,而是我们实际上只是在进行编程工作。没有特别大的障碍需要克服。

你建议C++第一年的人做哪些编程项目来全天编码?(如果之前已经回答过可以跳过)

对于初学者来说,建议首先做一个简单的“太空侵略者”游戏。这是一个非常基础且简单的项目,适合刚开始编程的人。通过制作这个游戏,能够掌握一些编程的基本概念,比如如何控制一个小船、如何让它发射子弹、如何处理屏幕上的小行星以及如何检测碰撞等。这些都是学习编程的重要步骤。如果能够完成这个简单的项目,那么就为更复杂的项目打下了基础。重点是理解和掌握基本的编程技能,之后再向更复杂的游戏或项目进发。所以,做“太空侵略者”游戏是一个非常好的起点。

你认为调试系统还会有多少新特性?

目前,调试系统还没有完全达到预期的效果。虽然在某些方面已经有了进展,但整体还需要进一步清理和优化。目前正在努力将数据接口做好,并改善数据的流动性,以便更好地展示和查看信息。一旦这些基础工作完成,系统的表现会更好。虽然可能会增加一些额外的功能,比如支持不同的设备,但目前的重点是确保系统能够顺畅运行,而不仅仅是增加更多的功能。因此,未来的改进更侧重于优化现有功能和清理系统,而不是大规模添加新特性。

你觉得学完微积分1/2有助于成为一个好程序员吗?

学习微积分对于成为一个优秀的程序员并非必需,尤其是在不涉及到模拟的编程中。如果没有做任何模拟相关的编程(比如物理模拟、渲染、声波传播等),那么就不需要微积分。然而,如果从事的是与模拟相关的工作,比如游戏编程、物理模拟、或者即使是一些简单的假物理模拟,那么微积分是必不可少的。尤其是在进行像光线渲染、振动传播等模拟时,微积分是非常重要的。因此,如果计划从事游戏编程,微积分是必须要学的;但如果只是做网页服务器编程,那么则不一定需要微积分。

实际上是-MSVC编译器的-GF标志。它启用字符串池,将所有字符串字面量放入内存的只读部分,从而只存储每个不同字符串一次

在这个过程中,提到了字符串池化的概念。字符串池化是指将所有字符串字面量放入一个只读的内存区域,这样内存中只存储每个不同的字符串一次。这样做的目的是节省内存,避免重复存储相同的字符串。

然而,这里要强调的是,字符串池化只会对相同的字符串进行池化,换句话说,只有当两个字符串完全相同时,才会被存储在相同的内存位置。如果每个字符串都是唯一的,编译器不会将其池化,因为它们在内存中是不同的。因此,在这种情况下,池化并不会发生。

而对于每个“wid”(可能是某个唯一标识符)生成不同的字符串,这些字符串会被分配到不同的内存位置,因为它们是不同的字符串。只有在两者字符串完全相同的情况下,才会考虑进行池化,这样会确保它们共享相同的内存空间。

总之,如果两个字符串在逻辑上相同,它们会被池化,从而节省内存。如果它们不同,则每个字符串会占用独立的内存位置。

你认为自C以来没有好的语言吗?还是说没有像C一样能做的好,像是一个好的“竞争者”?例如,如果有人需要做一个小应用程序,你仍然会选择C,还是认为某些高级语言在这方面更好?

在这段话中,提到了对于编程语言的看法,尤其是对C语言的评价。认为尽管有很多编程语言的出现,但从C语言之后,并没有出现真正能超越它的语言。虽然有一些语言如D语言,它在某些方面与C相似,并做了一些改进,但这些语言并没有在本质上超越C语言,反而只是对C语言做了一些小的改动。

总的来说,表达的观点是,尽管存在许多新的语言,但没有哪一款语言能够像C那样在功能性和效率上得到如此广泛的应用和认可,也没有出现明显比C更先进的语言。

如果它不池化字符串并且两个相同的字符串字面量得到不同的地址?

在这段讨论中,提到的是字符串池化(string pooling)的概念。池化字符串是指相同内容的字符串只在内存中存储一份,从而节省内存空间和提高性能。 这里的关键点是:系统并不关心是否执行字符串池化,因为每个生成的字符串都是不同的,因此它们不会被池化。

具体来说,每个字符串的生成都是故意的,目的是让每个字符串在内存中有不同的地址,即使它们的内容相同。因此,不会发生池化,系统也不会因为这个原因出现问题。

总结来说,尽管字符串池化在很多情况下是有益的,但在这种特定情况下,系统并不需要字符串池化,因为每个字符串的生成都确保其地址唯一,避免了池化的发生。

注意,“好语言”是相对的,取决于你在做什么。我在Prolog中做过一些事情,是我不想用C来做的

在这段讨论中,表达了对编程语言选择的强烈偏好。对于编程语言的选择,作者认为编程语言的优劣与所做的工作关系不大,关键在于是否能够有效地完成任务并且能够长时间稳定运行。他个人偏好使用C语言进行编程,甚至在进行本应使用其他语言的任务时,也倾向于用C语言自己实现。

他提到,虽然某些任务(比如类似Prolog的任务)可能有专门的语言来处理,但使用这些语言的编译器或解释器时,往往会面临兼容性、运行速度或优化问题。他曾经尝试过用PHP、MySQL等工具进行开发,但最终总是因为这些工具的不稳定性或依赖问题导致问题不断,代码无法顺利迁移或出现崩溃。

因此,他始终坚持使用C语言编程,因为C语言代码能稳定运行,不会因为平台、版本变化等因素出错,且能高效处理大量数据。相比之下,他认为许多现代编程语言(如C#、Java等)并不可靠,且在他看来,这些语言不值得使用。他特别提到,他希望能有一个比C语言更强大的继任者出现,这样可以弥补当前C语言的一些不足,并且促进更多的开发者重新关注C语言,编写更多的库,而不是转向现在流行的那些不太理想的语言。

总结来说,作者坚定认为C语言依然是最合适的选择,并认为许多其他编程语言的设计和使用方式存在较大问题,自己不愿意使用这些语言。同时,他期待C语言的继任者能够出现,改善现有的编程生态。


网站公告

今日签到

点亮在社区的每一天
去签到