回顾并为当天内容做准备
我们目前有一个排序功能,虽然不敢说完全稳定,但昨天它基本上是工作的。我们还做了一些可视化来观察排序的效果,整体看起来已经还算不错。
现在不太确定下一步该怎么走,因为有两件事必须完成。第一件是要把精灵放到不同的层里,这也是之前处理中断时正在做的事情。第二件是要解决排序中的 n² 复杂度问题,因为长期来看,这样的复杂度是不现实的。虽然电脑现在很快,排序还能保持合理的帧率,但这依然是个不太好的做法,特别是它处在渲染循环的核心部分,做 n² 复杂度的处理显然不是理想的方案。
运行游戏,查看上次进度
我们打开项目并进行了编译,加载当前的进度后,可以看到之前实现的排序系统运行得相当不错。虽然我们在屏幕上启用了比较花哨的信息叠加显示,但这些可以很容易地关闭,目前确实也不打算继续调试排序分组,因此可以先关闭那个全局的显示排序分组开关。
现在的画面表现还算理想。虽然有些问题需要处理,但大多数问题逻辑清晰,看不出排序存在明显错误。考虑到目前输入的数据质量并不高,排序的结果已经相当令人满意。
不过,仍然存在一些具体问题需要解决,例如角色跳跃时的位置处理。在某些跳跃的场景中,Z轴的值分布显得不合理,因此我们必须重新思考如何让Y轴方向上的角色正确地跳过障碍物,这样视觉上才不会出现错位或穿插的错误。
还有一些其他问题也值得关注。例如,当角色跳跃时画面中会出现某种图形错误,似乎与排序分组中的环状依赖有关,不过尚不能完全确认。此外,当前屏幕上还有高亮的光标描边效果,这些视觉元素本身并不提供有意义的信息,反而在排序上增加了不必要的复杂性。
目前处于“robe mode”状态,角色绘制仍在进行中。整体来看,虽然排序功能在基本层面上运行良好,但为了提升精度与视觉正确性,仍需对精灵分层、Z轴控制和某些调试元素进行优化和调整。
game_entity.cpp:在 UpdateAndRenderEntities() 中关闭体积高亮显示
我们之前绘制的那些体积,其实是碰撞体积,是我们在系统中创建的大量“推动体积”。这部分的实现我们已经做过了,但由于中间花了一段时间在识别码(ID)相关的功能开发上,导致我们有些遗忘这些逻辑已经迁移到了新的位置。
当我们试图找那段处理逻辑时一度找不到,其实是因为它已经被挪到了另一个源文件中,也就是说相关逻辑并没有丢失,只是代码位置变更了。我们应该将其放在内存视图管理模块中,这是我们当时决定的结构重组的一部分,只是一时之间没记起来。
目前由于环境温度上升,穿着连帽衫已经变得过热,为了更舒服地继续工作,只能脱掉了。这个插曲虽然小,但也提醒我们:工作时的物理环境对开发效率同样有影响。整体来看,我们对体积处理的代码已经写好,只需注意文件的组织结构和模块间的引用即可。
运行游戏,考虑将良好数据放入排序程序以实现稳定排序
当前的画面显示,在没有额外精灵图的情况下,左侧屏幕上之前出现的一些排序错误并没有再出现。这说明当前的排序机制在没有干扰时表现还是相当不错的。
这一点提示我们,在后续工作中需要更加注意输入数据的质量。必须确保输入的数据合理,不能期望排序系统处理那些本身就没有稳定解的问题。我们目前的排序本质上是在处理一些模糊的、不具确定性的情况,也就是说,我们让它对一些根本没有明确先后关系的元素进行排序,依赖的是一种“模糊逻辑”或“经验式”的策略,而不是绝对严密的规则。
尽管如此,在很多实际场景中,这种模糊处理方式已经产生了令人满意的结果。从这一点来看,这是一个很不错的开始,值得继续推进。当然,我们仍然需要持续回顾和优化,特别是在后续加入更多绘制元素,排序逻辑面临更复杂的情况时,必须有更多可见性来观察其实际效果。
总的来说,现在的状态是令人满意的,但我们必须保持警觉,在继续开发过程中持续优化数据结构与处理逻辑,确保系统稳定性和绘制正确性。
考虑下一步如何进行
当前的重点是两方面:一是尽可能让当前使用的 N 2 N^2 N2 排序逻辑能够合理运作,二是要继续推进图层系统的设计,这部分此前正在进行中。
关于排序的部分,当前虽然运行效果尚可,但必须承认,持续依赖 N 2 N^2 N2 复杂度的算法从长期来看是不可行的。因此,需要设法确保该算法在视觉效果和性能之间取得平衡,至少在目前阶段要让它“正常工作”。
另一方面,更棘手的问题是图层系统的处理。这主要因为目前使用的是一种等轴测的视觉方式(orthographic view),但在实际效果中又混入了一定的透视特性,也就是在垂直方向上,物体高度的变化会导致其在屏幕上的缩放。例如,角色往上走时,看上去就会“变小”,这种变化并不符合纯正的等轴测渲染逻辑,而是带有一种混合式的透视效果。
这一特性会让图层系统的实现变得复杂。通常在等轴测游戏中,图层划分是基于网格坐标或深度顺序来完成的,而现在由于引入了垂直维度上的视觉缩放,需要在排序逻辑中加入对“高度影响视觉缩放”这一因素的支持。这可能意味着需要动态调整图层或者绘制顺序,以保证在玩家向上或向下移动时,各层图形仍能正确叠加,不出现穿插或错乱。
虽然也可以选择放弃这个缩放特性,保持严格的等轴测视图,从而简化图层处理逻辑,但目前并不打算这么做。主要原因是这个缩放效果带来了一种非常独特的视觉观感,看起来很有趣,也很有吸引力。因此,希望在接下来的开发中尽可能保留这一视觉设计,除非最终在实现过程中发现确实无法兼顾效果与技术成本,才会考虑舍弃。
总的目标是在不损失性能和清晰度的前提下,尽可能保留当前这种混合视角带来的丰富层次感,同时确保排序和图层逻辑足够稳定可靠。
决定解决 n² 复杂度问题
目前面临的核心问题有两个:一是现有的排序规则无法正确处理图层之间的覆盖关系,尤其是当多个元素在视觉上叠加时会产生问题;二是构建精灵图(BuildSpriteGraph
)的过程采用了 N 2 N^2 N2 复杂度的算法,性能在特定情况下会成为瓶颈。
首先,当前使用的排序规则基于一些临时性、经验化的处理方式,仅仅是为了在常规场景下呈现出还算“好看”的结果。但它并不能处理真正复杂的叠加关系,比如多个图层之间的遮挡,因此需要引入更加明确的图层分离逻辑。这样可以解决调试信息(debug overlay)被 Y 轴方向上的精灵遮挡的问题。当前的排序规则实际上是“正确地”阻止了调试层的显示,但这显然不是我们想要的结果。
其次,虽然当前机器性能很强,即使是几年前的电脑,也能在一定程度上“掩盖” N 2 N^2 N2 算法的低效——也就是说,在精灵数量较少的情况下,即便执行了一个低效的排序流程,系统也依然可以维持良好的帧率。但这种情况是不可持续的,一旦精灵数量激增,构建精灵图的时间成本就会迅速飙升。
我们可以通过打开调试视图快速制造出大量精灵,从而迫使排序算法进入性能瓶颈状态。此时就可以清晰地看到 BuildSpriteGraph
所消耗的时间暴涨,说明当前排序逻辑在数据量大的情况下会严重拖慢渲染流程。
因此,当务之急是思考一种更高效的替代方案来缓解这一性能瓶颈。今天的任务就是围绕这个问题展开,寻找一个可以替代或优化现有 N 2 N^2 N2 排序机制的解决方法,这是当前最值得投入时间去处理的方向。这样做既可以提升整体性能,也为后续的图层系统建设打下更稳定的基础。
黑板讲解:确定哪些精灵重叠的问题
我们现在面临的问题本质上是:屏幕上存在一系列矩形区域(通常对应于精灵的可见范围),我们需要判断这些矩形中哪些彼此重叠。当前的实现方式是用一个典型的两层嵌套循环来遍历所有矩形的组合,逐一比较它们是否重叠。这种方法的时间复杂度是 N 2 N^2 N2。
具体来说,如果场景中有 5 个精灵,我们会将每个精灵与其余所有精灵进行一一对比,判断是否存在重叠。这导致总共进行的比较次数为 N × ( N − 1 ) / 2 N \times (N - 1) / 2 N×(N−1)/2,虽然比完全的 N 2 N^2 N2 少了一半,但在时间复杂度上仍然是 O ( N 2 ) O(N^2) O(N2),即随着精灵数量的增加,计算量会呈现平方级的增长。
这种增长曲线极其不利,因为一旦场景中精灵数量变多,处理这些比较的代价就会迅速上升,最终拖垮性能,特别是在每一帧都要执行的渲染循环中更是难以承受。
因此,我们的目标是寻找一种更高效的方式来构建这个重叠关系图,避免使用暴力的穷举比较。可以设想存在一些更简洁的方案,比如采用空间划分、预筛选等方式来减少不必要的比较次数。
这其实是一个典型的几何计算问题(computational geometry),广泛存在于图形学、物理引擎、碰撞检测等领域。我们需要借助这类问题中的经典解法来设计出适合我们精灵重叠检测的高效解决方案。接下来会围绕这一点深入探索如何降低复杂度,从而让渲染过程更加高效、可扩展。
黑板讲解:网格划分作为缓解 n² 复杂度方法
我们面对这个重叠检测的问题时,有很多不同的解决方法。我们常用的方法是“分桶(bucket)”策略,因为这种方式相对简单且易于实现。
具体而言,我们将整个屏幕空间划分为若干“桶”(即网格单元),然后将每个精灵根据其所在位置分配到相应的桶中。当我们需要进行重叠检测时,我们并不是将某个精灵与所有其他精灵进行比较,而是只比较它和那些与它所在桶有重叠的桶中的精灵。也就是说,每个精灵只需要与部分可能相关的精灵进行比较,而非所有精灵。
这种方法并没有从根本上消除 N 2 N^2 N2 的复杂度,因为在每个桶内部,最坏情况下还是可能出现所有精灵彼此比较的情况,但它的优势在于“缩小了 N”。通过局部化比较对象的范围,使每个桶内的 N 较小,从而避免整体系统陷入 N 2 N^2 N2 带来的性能灾难。即便仍是 O ( N 2 ) O(N^2) O(N2),但它通常是在一个远低于全局 N 的子集上操作,所以整体效率有很大提升。
虽然我们使用的是这种近似或折中的方式,但其实在计算几何的文献中,确实存在一些更高效的解法,有可能彻底摆脱 N 2 N^2 N2 的限制。虽然我们不是几何算法领域的专家,但我们记得其中的一些基本原理:在执行重叠测试时,每一次测试其实都会带来一些可传递的信息,从而使我们可以推断出哪些测试是不必要的。
举一个简单的例子:假设我们已经知道 A 不和 B 重叠,而且 B 不和 C 重叠,那么在某些条件下,我们也许能推断出 A 和 C 不重叠,进而省略对 A 和 C 的比较。这种“剪枝”思想就是更高效算法的基础。
因此,接下来我们计划先从分桶策略入手优化当前实现,之后再考虑是否有进一步降低复杂度的可能性,比如通过扫描线法、空间分区树(如四叉树、KD 树等)等更高级的算法。
黑板讲解:一维重叠测试
我们现在来考虑一种更高效的重叠检测方式,从一维重叠的角度出发。如果我们将问题简化成一维,也就是说只检测区间在一个坐标轴上的重叠情况,那我们就可以利用排序来加速整个过程。
设想一下,我们有若干个区间段(span),每个区间有起点和终点。如果我们先根据每个区间的起点对它们进行排序(例如按最小值排序),那么我们就可以按照顺序依次检查每一个区间是否与它后面的区间重叠。一旦发现一个区间与后面的某个区间不重叠,就可以确定这个区间不会和再后面的任何区间重叠,因为它们的起点已经在当前区间的终点之外了。
这种方法的关键在于排序是 O ( n log n ) O(n \log n) O(nlogn) 的操作,而不是 O ( n 2 ) O(n^2) O(n2)。而且由于区间排序后我们只需要进行有限的线性扫描即可完成所有可能的重叠检测,因此整体复杂度可以有效控制在 O ( n log n ) O(n \log n) O(nlogn)。
我们进一步思考能否将这个思路扩展到二维重叠检测中去。在二维情况下,可以先沿一个轴(如 X 轴)排序,再在排序后的同一 X 值区间内沿 Y 轴进行稳定排序或滑动窗口式判断,从而有效减少需要测试的对数。这样的算法结构,在计算几何领域中是很常见的,它利用了多个坐标维度的排序信息,来逐步筛掉不可能重叠的组合,避免了无谓的测试开销。
不过我们更倾向于使用更简单直接的“分桶法”,原因在于以下几点:
使用场景决定性能瓶颈:我们的程序在屏幕某一区域内同时活跃的 sprite 数量很难超过一千个,也就是说,虽然我们在理论上可以实现一个复杂但更优的算法结构,但在现实中它很可能不会带来明显的性能提升,反而增加了实现的复杂度。
分桶法的局部性原理更契合场景:我们每帧只关心屏幕上有限范围内的 sprite,使用网格化的空间划分(即把屏幕划成很多小网格),让每个 sprite 加入其覆盖的桶中,然后在同一桶或相邻桶中进行重叠测试。这种方法虽本质上在桶内仍可能是 O ( n 2 ) O(n^2) O(n2),但每个桶内的 N 都很小,所以总体复杂度表现良好。
实现和维护成本低:相较于构建一个稳定排序系统并结合二维多层次过滤逻辑,分桶法简单很多,也更易调试、维护和优化。
我们接下来的重点将会放在两个问题上:
- 如何高效地设计和实现桶的存储结构;
- 如何高效地将每个 sprite 分配到正确的桶中,并在重叠检测中快速定位桶中的相关对象。
这些设计上的选择直接关系到我们最终系统的效率与灵活性,因此需要进一步探讨与实验。
黑板讲解:如何做桶分割或空间划分
我们当前面临的核心问题,是如何通过空间划分来减少矩形之间的重叠检测次数。设想我们把整个画面划分成一个网格,用这种方式进行空间划分(spatial partitioning),以此判断哪些矩形可能会重叠,哪些则根本不需要进行检测。
比如,我们有一些分布在屏幕上的矩形,有的彼此非常接近或者重叠,有的则相距很远。我们希望检测那些在同一区域内的矩形,比如位于相邻格子或同一格子里的矩形,而不浪费计算资源去检测那些完全不相干、彼此距离遥远的对象。
这就引出了空间划分策略的必要性。我们通过将空间划分为多个小区域(桶 bucket 或格子 cell),每个矩形注册到其所占据的所有格子中,只在同一个格子或相邻格子内进行碰撞检测,从而有效避免不必要的比较。
但问题并不只是划分区域那么简单。关键在于:很多矩形并不完全落在单一的格子内,而是跨越多个格子。例如某个大矩形可能横跨四个格子甚至更多。这就引出了两个重要问题:
我们如何记录一个矩形所属的多个格子?
- 比如一个矩形同时横跨左上、右上、左下、右下四个格子,我们就需要将它登记到这四个格子中,这样任何一个格子的查询都可以找到它。
- 这意味着我们的数据结构必须允许“一个对象属于多个桶”。
我们如何高效查询这些桶?
- 我们要确保无论哪个格子被查询,都能查到与其相关联的所有矩形。
- 同时我们也要避免重复检测,例如一个矩形出现在两个格子中,而另一个矩形也在这两个格子中,我们在两个格子中都可能尝试比较它们,这会导致重复。
因此,虽然空间划分的概念很简单——按位置分组减少检测次数,但实现细节却涉及一系列设计决策,主要包括:
- 网格尺寸的选择(格子太小则增加桶数量,格子太大则精度不足);
- 如何在多个桶之间管理同一个对象;
- 如何高效地在查询和检测时去重;
- 如何平衡内存使用和查询性能。
我们倾向于选择那种能够在实践中提供良好性能,并且维护和实现都相对简单的策略。这种方法未必在理论上最优,但对我们当前的需求和对象数量(通常不会非常巨大)来说,已经非常有效。我们将进一步探索和实现这种基于空间划分的碰撞检测系统。
黑板讲解:拆分每个实体
我们在处理对象跨越多个空间桶(bucket)的情况时,有一种做法是:将对象拆分记录,在每一个它接触到的桶中都存一份记录。这样做的好处是,在进行重叠检测时,我们可以只查找当前对象所涉及的那些桶,然后仅检测这些桶中已有的对象,而不需要扫描整个空间的所有对象。这种做法的逻辑简单、清晰,执行效率也不错,尤其在对象分布相对均匀的场景下。
具体来说,流程如下:
对象注册阶段:
- 当一个对象生成或移动时,我们计算它的边界所跨越的所有空间桶;
- 然后在每一个相关桶中插入一条指向该对象的记录(或引用);
- 这样,一个对象可能在多个桶中拥有记录。
检测阶段:
- 当我们想检测某个对象的重叠关系时,只需要查找它所在的那些桶;
- 从这些桶中获取潜在重叠的对象集合;
- 只在这个集合中进行精确的矩形重叠判断。
这种方式虽然会导致某些对象在内存中有多份引用,但这并不会造成太大的资源浪费,因为这些记录一般只是指针或引用,而非对象本身的复制。
不过,这里也引出了一个需要权衡的点:
数据冗余与更新复杂度:
如果一个对象频繁移动,我们需要不断地更新它所在的桶集合(移出旧的桶,插入新的桶),这在高频率更新的场景中可能会带来一定的管理成本。重复检测问题:
由于一个对象出现在多个桶中,我们在合并所有相关桶内的检测候选集时,必须确保不会重复检测同一对对象。这通常可以通过一个“已检测对列表”来实现,或者通过编号排序只比较一次来规避。
综上所述,我们认为“对象多桶注册”的方式是一种灵活且高效的策略,适用于我们的碰撞检测需求。尽管它带来了一些实现复杂度上的增加,但换来的计算节省和效率提升是值得的。在实际实现中,我们将进一步设计合适的数据结构来支持这一机制,并对性能和可维护性进行综合考量。
黑板讲解:创建空间更层次化的表示方法
我们可以采用一种更层级化的空间划分方法,避免把对象拆分到多个桶里。具体思路如下:
定义顶层区域
先把整个空间看成一个大的区域,所有落在这个区域内的对象都可以存储在这个顶层桶里。递归划分空间
将这个大区域一分为二,形成两个子区域。这样,某些对象会完全归属于左边的子区域,有些归属于右边的子区域。测试时,只需要分别检测每个子区域内部的对象重叠。进一步细分
可以继续对每个子区域再一分为二,形成更细的层级结构。这种递归细分可以持续下去,形成一个树状的空间划分结构。处理跨界对象
当一个对象跨越两个子区域的边界时,传统网格方法会把这个对象放进两个区域中(在两个桶中都存一份记录)。但是,在层级划分方法中,可以选择不把该对象存到子区域,而是存到其父级区域,即更高层级的桶中。检测逻辑
检测某个对象时,我们会沿着层级结构定位它所在的区域,同时测试所有它可能存在的区域中其他对象,确保不会遗漏任何可能的重叠。
这种层级划分的好处是减少了对象的重复存储,避免了跨区域对象被复制到多个桶带来的数据冗余;同时保持了空间的层次结构,便于快速定位和检测。缺点是结构更复杂,需要设计合理的层级划分规则和遍历算法。
总体来看,这种层级空间划分法可以更高效地管理空间中的对象,特别是当对象大小和分布差异较大时,能够更灵活地适应不同情况,提高碰撞检测和重叠判断的效率。
黑板讲解:四叉树、KD树和BSP树
在空间划分的方法中,有多种经典的数据结构,比如四叉树(Quadtree)、KD树(K-Dimensional Tree)和BSP树(Binary Space Partitioning Tree)等。它们本质上都是不同类型的空间分割技术:
四叉树
四叉树总是将空间划分为四个部分,适合二维空间的均匀划分。KD树
KD树每次只沿着一个正交轴(x轴或y轴)进行划分,通常是一分为二,形成一个二叉树结构。它不像四叉树那样四分,而是连续二分。BSP树
BSP树更灵活,划分线可以是任意方向的,不限于正交轴。它是一种更广义的二叉空间划分树,KD树可以看作是BSP树的一种特殊情况,只使用正交轴。
虽然这些结构设计上很巧妙,但在实际应用中,如果空间密度和细节层级(Level of Detail, LOD)变化不大,它们的优势并不明显。它们更适合处理具有不同细节层级或者密度不均匀的场景,比如当视角变化导致某些区域需要低细节而其他区域需要高细节时,这种层级化空间划分就很有用。
然而,在当前面对的场景中,空间是平坦且边界固定的,细节层级一致,且精灵(sprites)在屏幕左侧和右侧分布均匀,没有明显密度差异或层次变化。在这种情况下,使用复杂的空间划分结构可能会浪费大量开发时间,而收益不大。
因此,假设是采用更简单、更直接的空间划分策略,比如基于网格的划分,可能更合适且高效,不必追求复杂的树状分割结构。这样既能满足性能需求,也更容易实现和维护。
黑板讲解:偏好非常简单的网格桶方案
最有效的方法很可能是采用一个非常简单的网格桶(grid bucketing)方案。这个方案会将每个精灵(sprite)根据其覆盖的区域存储在多个桶里,也就是说,如果一个精灵跨越多个网格,就会在对应的多个桶中存储它的记录。虽然这样会有重复存储,但这种方法的实现简单且高效,适合当前的需求。
这种简单的网格划分方案相比其他复杂的空间划分算法(如四叉树、KD树或BSP树)并没有明显的优势,且实现复杂度较高,开发成本也更大。虽然存在一些更复杂且“聪明”的方法,可能理论上更高效,但考虑到我们的使用场景和目标,简单的网格方案更实用。
另外,针对一些特殊情况,比如某个精灵特别巨大,覆盖了很多网格区域,会导致它被重复存储在大量桶中,这样反而会影响效率。为了解决这个问题,可以采用一种折衷策略:
- 对于大小适中的精灵,依然按网格划分存储。
- 对于非常大的精灵(覆盖多个网格甚至更多),不放入网格桶,而是单独放在一个线性数组中。
这样一来,网格桶依然能够快速定位大多数精灵的重叠检测,但对于少量巨大的精灵,直接在线性数组中逐一检测,避免了网格划分带来的病态性能问题。由于大型精灵数量通常较少,这样的检测开销也在可接受范围内。
综上,结合网格划分和特殊处理大对象的线性检测,是当前情况下既简单又高效的解决方案,符合实际使用需求,避免了复杂实现带来的额外负担。
运行游戏,考虑 BuildSpriteGraph() 的性能开销
当前我们观察到一个性能瓶颈,尤其是在打开调试视图时,性能开销极大。尽管在发布模式下性能会好很多,但在调试模式下构建精灵(sprite)的过程异常昂贵,甚至占据了93%的执行时间。这种开销是不可接受的,即便调试视图只是为了辅助开发,也不能忽视其造成的性能问题。
为了解决这个问题,我们计划实现一个简单的网格系统,以减少每帧进行的精灵构建和碰撞检测的数量。我们并不是追求最优解,也不是为了过早优化,而是希望通过一个合理的方案来避免调试视图变成灾难性的性能负担。
当前系统的问题在于:每次都对所有矩形进行两两比较(即暴力穷举),这在调试时尤其糟糕。当精灵数量一多,这种方式的性能会指数级下滑。我们需要做的,是将需要进行检测的矩形数量缩小到那些可能有相交的矩形范围内,避免不必要的计算。
我们已经确认了性能瓶颈所在,优化目标也很明确:加速这个函数的执行。这个情况非常理想,因为我们不需要四处猜测哪里慢,只需集中力量解决这一处热点代码即可。优化的核心是:通过某种方式(如网格分区)对空间进行划分,使得每个对象只需要和它所在网格及其相邻网格的对象做碰撞检查,从而大幅减少对比次数。
因此,我们准备尝试实现这个简化的网格划分方案,即使只是初步版本,也能让我们感觉更放心,不至于在打开调试界面时拖垮系统性能。这个方案并不复杂,实现成本也不高,却能带来显著的性能提升,是值得投入的优化方向。
game_render.cpp:引入 sort_grid_entry 结构体,让 BuildSpriteGraph() 记录精灵占据哪些网格方格
我们已经开始着手实现一个基于网格的加速结构,以优化调试视图中精灵构建时的性能问题。为了简化原始的全体两两检测方式,我们采用了空间划分策略,将整个画面区域划分为一个固定大小的网格,每个精灵的包围盒(sprite bounds)将被插入到它所覆盖的所有网格单元中。
具体实现上,首先定义了一个简单的链表结构,用于表示网格中的条目。初步命名为 SortGridEntry
,它包含两个字段:
occupant
:指向一个精灵的包围盒,表示该单元格中所包含的精灵对象。next
:指向下一个在该单元格中的条目,用于形成一个链表结构。
虽然这种链表方式在缓存友好性方面可能存在问题,但由于这些结构是连续分配的,缓存局部性不会太差,而且我们优先关注的是功能正确性和基础性能提升,在确实遇到缓存瓶颈前暂不优化。
接着定义了网格的尺寸。暂定为 16x9,仅仅是基于测试目的。因为我们通常在一个16:9的屏幕空间内处理精灵,这样可以较好地覆盖整个可视区域。每个网格单元将以二维数组 SortGrid[GridWidth][GridHeight]
的形式存储头指针,表示每个单元格中链表的起点。
初始化时,网格中的每个位置都被设置为 nullptr
,表示为空。
然后我们对每个输入节点(即所有精灵对象)进行遍历。对于每个精灵的包围盒,我们需要计算出它在网格中所覆盖的范围,即其在 X 和 Y 方向上覆盖的所有网格坐标(从起始格到结束格)。这是一个嵌套循环:对于精灵包围盒所覆盖的每一个网格单元:
- 分配一个新的
SortGridEntry
。 - 将该 entry 的
occupant
指向当前精灵。 - 将 entry 的
next
指向当前网格单元已有的链表头(也就是SortGrid[x][y]
)。 - 更新
SortGrid[x][y]
,使其指向新创建的 entry。
通过这个过程,网格中的每个单元都会记录所有与之重叠的精灵边界,每个单元格中形成一条链表,链表中存的是与该格子有关的精灵。这种做法的好处是,后续在进行碰撞检测或精灵更新时,只需要在局部格子中遍历对应的条目链表,而不需要全体精灵互相比较,从而显著减少检测次数。
总的来说,这种基于空间分区的方式极大地优化了算法的时间复杂度,为后续更高效的调试与运行打下了基础。下一步将是在该结构的基础上,实现具体的检测逻辑,验证效果并进一步调整网格大小参数以达到最佳性能表现。
game_render.cpp:考虑一些立刻可行的优化可能
我们目前采用了一种新的方法来处理这个问题,在这种方式下,我们其实不再需要单独进行成对的检查。原因是每次我们向网格中添加一个元素时,我们可以直接在当前的网格单元中检查已有的所有元素是否符合连接条件,然后立即添加对应的边。理论上,这样可以省去后续再进行一次 N² 成对循环检查的过程,直接在插入的同时完成检查和边的建立。
目前我们使用的是索引来处理这些元素,但需要注意,我们使用的是特定的“章鱼索引”,也就是说,可能并不是最有效率的方式,但考虑到整个程序的其他部分也是这样写的,所以暂时我们保持一致,继续使用这种索引方式。
在这个基础上,我们需要针对每一个网格单元建立某种机制,使我们能够在插入元素的同时进行连接判断与边的创建。这种方式将逻辑集中到一个阶段处理,避免了之后冗余的遍历操作。值得注意的是,我们已经预感到还有进一步的优化空间,但那部分内容会稍后再处理。
我们目前的重点在于,在添加每个元素时,利用当前网格中的已有元素立即进行连接判断,并将边建立在此阶段完成,从而大大简化整体流程。整体目标是提高效率、减少不必要的计算,并为后续的优化打下基础。
game_render.cpp:考虑让 RecursiveFrontToBack() 不创建图的边
我们在递归的前向遍历(front-to-back)中,其实可以进行一些优化。当前的实现中,我们会去构建一张图,并把所有从某个节点出发的边都保存下来,然后再进行遍历。但实际上,这些边的保存并不是必须的,也没有真正的用途。
我们只会处理每个元素一次,并且一旦处理完就标记为已访问,不会再重新访问它们。因此,我们其实可以完全省略构建边的过程。在访问到某个元素时,直接进行所需的测试即可,而不必先将边保存在图结构中再去访问。这意味着我们可以完全省略掉构建图边这一步,直接在遍历过程中做测试。
更进一步地,我们还可以将所谓的“图遍历”过程直接内联到处理流程中。换句话说,不再通过显式地走图的边来遍历邻居,而是直接在当前元素所处的网格中进行迭代,查看与当前元素相关的其他元素,并进行前向可视性(front-to-back)的判断。这种方法可以简化代码结构,并避免维护不必要的图结构。
我们的目标是减少不必要的数据结构开销,只保留实际需要的操作。通过在当前处理点上直接进行遍历和判断,我们可以更高效地完成前向处理逻辑。这种方式更直接、也更符合我们的处理流程,尤其适用于一次性处理且无需重复访问的场景。
game_render.cpp:引入 GetGridSpan() 函数
我们需要处理的另一件重要事情是网格的 X 范围(grid X span),也就是计算某个矩形在网格中的覆盖范围。为此,我们打算实现一个函数,暂时称为 getGridSpan
,这个函数的输入是一个矩形(rectangle2i),表示屏幕空间中的一个区域,输出则是该矩形在网格中所对应的最小和最大网格索引。
我们已经有了类似的数据结构,即 rectangle2i
,它正好包含了我们所需要的信息:最小 x、最小 y、最大 x、最大 y。通过这个结构,我们就可以从屏幕区域计算出它在网格中的跨度。
计算时,我们会将矩形的边界转换为网格索引范围,例如从 minX
到 maxX
,同时在取 maxX
时会使用“小于等于”的判断逻辑,因为这样更符合人类直觉的语言语义,也能减少误解和错误。我们希望确保这些计算合理,避免无效值,因此需要对结果进行边界裁剪(clipping)。例如,如果 minX
小于 0,就将它裁剪为 0,避免出现越界情况。
此外,我们可能还希望在这个函数中返回一个布尔值,用来判断这个矩形是否完全在屏幕之外。如果完全不在屏幕范围内,就可以直接跳过后续处理,因为这类元素无论如何都不会影响显示排序。但这也不是绝对的。由于我们正在执行的是一个拓扑排序(topological sort),理论上某个完全不在屏幕内的对象仍然可能通过依赖关系影响排序结果,比如带来视觉上的跳动或闪烁。
尽管如此,我们倾向于认为如果排序会因一个不可见对象而不稳定,那么排序逻辑本身可能就已经存在问题,因此从结构设计角度来看,跳过这些完全不可见的对象是合理的。如果后续实验证明这种跳过带来问题,我们也可以很容易地取消这种裁剪和忽略策略。
总之,这部分逻辑的目标是根据元素在屏幕上的矩形区域,计算其在网格中的跨度,并在确保索引合法的同时,判断该对象是否需要进一步处理。这样可以提高整体效率,减少不必要的计算。
黑板讲解:从网格检测视角添加边
我们现在处理的是网格上的 X 和 Y 检测,目的是在遍历网格单元时直接添加图中的边(edge)。初步设想是可以在遍历的过程中就将边添加进去,而不需要后续再做一个专门的图构建阶段。
不过这种做法也有一些潜在问题。例如,如果我们基于每个网格单元进行检测,那么在存在多个网格重叠区域的情况下,同一对元素可能会被多次检测并多次添加边。比如说,矩形 A 和矩形 B 有重叠,它们共同出现在两个不同的网格单元中,我们在每个网格单元中都会检测到它们之间的关系并尝试添加边。这会导致同一对节点之间出现多条边。
理论上这似乎不是大问题,因为在实际的遍历阶段,我们只会处理每个节点一次,因此重复的边不会对最终结果造成影响。但是这种重复确实可能会干扰到拓扑排序中的循环检测机制。也就是说,虽然算法功能本身不会受太大影响,但如果我们依赖某种机制去识别并打破图中的循环,那么这些多余的边可能会让一切看起来像是存在循环,导致错误判断。
为了解决这个问题,我们考虑引入“generation number”(代数标记)的机制。具体来说,每一轮或每一次测试过程都附带一个唯一的代数编号,当我们尝试测试两个元素之间的关系时,只有在这一轮未测试过的组合才会被真正处理。这样,我们就可以有效避免在不同网格单元中重复处理相同的对象对。这种方式可以确保即便对象出现在多个网格单元中,也只会对它们之间的关系进行一次检测。
当然,我们是否真的需要这个 generation number 取决于我们是否真正依赖循环检测机制。如果我们不太关心循环检测(或者不打算做复杂的循环打破逻辑),那这个问题就可以暂时忽略。否则,引入 generation 编号将是一种合理且有效的优化手段。
目前我们暂时保留这种可能性,看后续的具体实现需求是否会涉及到循环检测优化。一旦发现影响到排序稳定性或性能,再进行修改也非常容易。总的目标还是尽量在构建过程中减少重复操作,保证数据结构简洁、逻辑清晰,便于拓扑排序顺利进行。
game_render.cpp:让 GetGridSpan() 以总屏幕区域为基准来划分传入的屏幕区域
在构建网格桶(bucketing)的过程中,我们使用了一个叫 GetGridSpan
的方法来计算一个矩形在整个屏幕网格中的覆盖范围。这个操作的核心目的是将一个源区域(source)映射到我们定义的网格系统中,判断它到底覆盖了哪些网格单元。
我们接收一个输入矩形 resultRect
,接下来要做的,是把这个矩形按网格划分归入到具体的单元格里。为了实现这个,我们首先要知道每个网格单元的尺寸,即网格的物理大小,这里我们用一个 v2
类型的向量(二维向量)表示每个单元格的宽和高。我们也会准备这个向量的倒数 inverseCellDim
,用于将坐标从世界空间转换为网格空间。
接下来的计算流程如下:
计算最小角点的位置(Min Corner)在网格空间中的坐标:
- 使用
source.MinCorner
与inverseCellDim
进行 Hadamard 乘法(对应分量相乘),这样可以得到该点在网格空间中的“浮点”坐标。 - 示例:
minInGridSpace = source.MinCorner * inverseCellDim
- 使用
计算最大角点的位置(Max Corner)在网格空间中的坐标:
- 同样的方法,得到浮点坐标
maxInGridSpace = source.MaxCorner * inverseCellDim
- 同样的方法,得到浮点坐标
将浮点坐标转换为整数网格索引(truncation):
- 使用截断(向下取整)将浮点数转换为整数索引,用于确定矩形在网格中的起始和终止单元格。
- 示例:
gridMinX = TruncateDown(minInGridSpace.x)
,gridMaxX = TruncateDown(maxInGridSpace.x)
,Y轴亦然。
这样一来,我们就得到了该矩形在网格中所覆盖的最小和最大单元格坐标范围。通过这组坐标,我们可以遍历对应区域内的网格单元,将该矩形插入到每一个它所覆盖的网格桶中。
整个过程非常直接、清晰、有效,几乎不涉及复杂计算。为了进一步提高效率,我们可能会预先计算并缓存 inverseCellDim
,避免在每次调用时都重复除法操作,尽管现代处理器中除法已经不如过去那样昂贵,但作为优化手段依然值得保留。
最终,我们完成了一个通用的、基于网格空间映射的空间分区方法,使得每个矩形(精灵)都能快速归属于其覆盖的网格区域,为后续的局部碰撞检测或绘制提供了高效的数据支持。
game_render.cpp:让 BuildSpriteGraph() 计算 InvCellDim(网格单元倒数)
为了完成网格划分和加速结构的实现,我们还需要一个关键的信息:每个网格单元的尺寸(cell dimension),以及它的倒数(inverse cell dimension),因为我们要将世界空间(例如像素坐标)映射到网格空间中。
实现的核心思路如下:
1. 计算每个网格单元的尺寸
我们需要知道屏幕的整体尺寸(宽度和高度)以及我们将网格划分为多少列和行。例如,我们有一个 16×9 的网格,如果屏幕的宽度是 1600 像素,高度是 900 像素,那么:
- 每个网格单元的宽度为:
screen_width / grid_width
- 每个网格单元的高度为:
screen_height / grid_height
因此,我们可以得到:
cell_dim.x = screen_width / grid_width;
cell_dim.y = screen_height / grid_height;
2. 计算每个单元的倒数尺寸
我们不希望每次将坐标映射到网格空间时都进行除法操作,因为频繁的浮点除法在某些平台上可能会影响性能。所以我们直接计算尺寸的倒数:
inverse_cell_dim.x = grid_width / screen_width;
inverse_cell_dim.y = grid_height / screen_height;
这样做避免了先除后取倒数的重复操作,同时也提升了代码效率。
3. 获取屏幕尺寸
为了计算上述内容,我们需要屏幕的宽度和高度。这些信息可以通过渲染命令中获取,例如:
commands->Width;
commands->Height;
如果这些字段已经存在并可以使用,那么我们可以直接将它们作为参数传入需要计算网格维度的函数中,避免重复查询。也就是说,在调用构建网格结构的函数时,我们可以传入:
BuildGrid(..., commands->Width, commands->Height);
这使得整个流程自洽且易于维护。
4. 后续工作
一旦我们有了 cell_dim
和 inverse_cell_dim
,就可以将每个矩形(精灵)转换成网格坐标范围,并将其插入对应的网格桶中,从而完成整个空间分区的预处理逻辑。
总结
- 使用屏幕宽高除以网格划分数计算出单元格尺寸。
- 直接使用网格划分数除以屏幕宽高计算出倒数尺寸。
- 利用倒数尺寸将任意输入矩形快速映射到网格坐标范围。
- 所需的屏幕宽高可以从渲染命令中直接获取,无需额外计算。
至此,我们的网格空间划分逻辑已经具备了完整的结构准备,可以进入下一步的功能实现阶段。
game_render.cpp:让 GetGridSpan() 工作方式稍作调整
我们现在进一步完善网格划分的实现细节,主要目的是将一个矩形正确地映射到屏幕的网格中,并确保处理过程中避免不必要的操作。具体步骤与逻辑如下:
1. 提前处理边界裁剪(排除屏幕外矩形)
当前所有矩形都是 v2i
或 b32
(整数矩形),因此在计算时更高效。我们需要首先判断某个矩形是否完全位于屏幕范围之外,如果是,就可以直接跳过它,避免后续无意义的计算。
- 做法是通过一个
RectanglesIntersect
的函数判断当前矩形和整个屏幕矩形是否有交集。 - 如果没有交集,就说明它完全在屏幕外,可以被忽略。
2. 构建屏幕矩形
为了进行交集判断,先构建一个完整屏幕的矩形:
rect2i screen_rect;
screen_rect.min = {0, 0};
screen_rect.max = {screen_width, screen_height};
这个矩形代表了整个屏幕的区域。
3. 传参调整
为了统一处理,我们把所需的所有参数都整理好传入 GetGridSpan
函数:
- 屏幕矩形(screen_rect):用于裁剪判断。
- 屏幕尺寸(screen_width, screen_height):计算单元格尺寸时用。
- 逆单元格尺寸(inverse_cell_dim):将实际坐标映射为网格坐标。
- 需要分配的内存 arena:用于创建网格条目。
- 目标结构体(destination):用于接收计算后的网格范围。
- 被插入网格的精灵索引或指针(occupant index)。
尽管参数看起来有些多,但这些都是必要信息,能确保函数通用性强、逻辑清晰。
4. 逻辑清晰,排错方便
在实现过程中注意语法与逻辑上的小错误,例如非法的标点、错误的类型匹配、变量未声明等。同时需要特别留意参数顺序和类型是否与实际调用匹配,避免因疏忽导致崩溃或逻辑错误。
5. 占据区域链表(Occupant Entry)记录
一旦矩形成功通过裁剪检测并进入网格计算阶段,就需要在其覆盖的所有网格格子中添加一个条目,表示它“占据”了这个区域。这些条目以链表的形式存储在网格每个格子中。
总结
- 使用整型坐标更高效,矩形裁剪判断通过交集函数完成。
- 构建完整屏幕矩形用于判断。
- 将所有计算所需参数整合并传入统一函数中,保持函数结构清晰。
- 交集检查能过滤掉屏幕外无效矩形,避免浪费性能。
- 对于位于网格内部的矩形,使用链表结构记录其在网格格子中的存在。
整个流程为网格划分和空间查询的优化建立了良好基础,确保性能可控,逻辑合理,结构清晰。
运行游戏,注意现在开始进行分桶处理
我们目前已经成功实现了网格分桶(bucketing)的逻辑,也就是说,我们已经把所有需要处理的矩形对象正确地划分并放入了各自对应的网格单元中。这意味着每一个矩形都被记录到了它所覆盖的所有网格格子中,现在每个网格格子都包含了一个链表,用于指向那些与该格子有重叠的对象。
这是实现加速检测或空间查询机制的重要第一步。通过这个分桶机制,我们后续就可以只对相邻格子内的对象进行相交检测,而不再需要全量两两比较,从而显著降低计算开销。
目前这个机制只是完成了“填桶”的过程,也就是说:
- 已经建立了网格;
- 已经计算了每个对象在网格中覆盖的范围;
- 并将其插入到了所有涉及的格子中;
- 所有格子中现在都保存了链表结构,记录相关对象。
但是我们还没有真正“利用”这些桶来做优化运算,比如碰撞检测、渲染裁剪、可见性测试等。也就是说,分桶数据结构现在只是被正确地构建出来,但还没有被用于实际的性能加速逻辑中。
接下来的任务就是使用这些桶信息来加速后续的操作。比如,我们可以只在同一个桶内或相邻桶内进行对象相交判断,或者在渲染流程中利用桶来过滤不必要的绘制对象。
这个部分将作为接下来的工作进行处理。现在已经完成的阶段为后续的加速奠定了扎实的基础。
问答环节
使用网格系统,你认为它适合用于像素级精确碰撞检测吗?
我们认为当前这个分桶机制并不真正适用于像素级精确碰撞(pixel perfect collision)。原因在于这两者在本质上的关注点和处理方式是完全不同的,我们可以从以下几个方面来解释:
粒度层级不同
分桶机制(如我们当前实现的基于矩形边界划分到网格中)是一种“粗粒度”的空间划分方法。它的目的是尽可能快速地排除那些不可能相交的对象,减少后续需要进行精确检测的对象对数。这个机制关注的是矩形边界或大体形状的空间关系。而像素级精确碰撞是一种“细粒度”的检测方式,它关心的是对象在像素层面上的实际绘制内容是否发生了真实重叠,即是否在图像上存在不透明像素的精确重叠。
使用阶段不同
网格分桶适合用作第一阶段的加速结构,用于快速筛选可能发生碰撞的候选对象。而像素级碰撞则通常是在确定两个对象在边界层面上可能相交之后,进一步进行的精细检测,用来确认“是否真正相撞”。数据需求不同
网格分桶仅需要对象的包围盒(bounding box)信息,而像素级碰撞则需要访问图像的像素数据,通常涉及纹理的alpha通道或位图掩码等。这种检测的计算开销也更高,无法大规模进行,因此通常只能在过滤之后的小范围对象上使用。实现逻辑差异
网格分桶通过矩形与网格单元的覆盖关系进行链表插入与空间划分,而像素级碰撞的实现则是基于两个对象纹理的重叠区域,对应像素点逐个判断是否“都不透明”,这属于图像处理范畴。
总结来说,当前我们实现的网格分桶机制可以作为优化管线的第一步,在初步筛选碰撞对象方面非常有用,但并不直接适用于像素级碰撞。真正用于像素级检测的,仍然需要更深入的纹理访问与逻辑判断。所以,分桶可以与像素碰撞联合使用——前者做预筛选,后者做最终确认。这样可以兼顾性能与精度。
黑板讲解:判断矩形是否相交,甚至可能像素级相交
我们在处理精确碰撞检测时,如果是像素级别的碰撞,判断过程实际上是分两个阶段的。首先必须做的是矩形包围盒(bounding box)之间的相交测试,因为如果两个矩形根本没有交集,那么它们内部的像素自然也不可能发生重叠,因此完全没有必要做进一步的像素级检测。
一旦矩形测试表明两者确实有交集,那么我们才会继续进入第二阶段——像素层面的检测。这一阶段的操作是基于两个矩形的交集区域,对应到两个精灵图像(sprite)的局部子区域中去检查具体的像素值是否存在真实的重叠,也就是说是否两个图像在交集区域中存在非透明(或非零)像素的重合。
所以,我们当前正在实现的基于空间网格的分桶(grid-based bucketing)机制,在逻辑上属于第一个阶段——加速矩形之间的相交检测。这种机制与是否执行像素级检测没有直接关系。无论后续是否需要像素级精度,这一层次的矩形处理始终是必需的,而且在流程中是先行发生的。
另外,如果真的要在高分辨率下做像素级检测,直接对每一个像素逐个检查效率会非常低下,因此我们通常还需要进一步对精灵图像本身做“预编码”或分块处理。例如,提前将图像划分为若干小块,每块记录其是否包含有效像素(如是否包含不透明像素)。这样可以在像素检测前先跳过那些“全透明”的区域,从而显著提升效率。
总结如下:
- 无论是否做像素级碰撞检测,矩形相交检测始终是第一步;
- 当前的分桶系统就是为这一阶段服务的,用于加速查找可能发生碰撞的候选对象;
- 像素级检测是额外的一步,需在矩形相交成立后才执行;
- 为了提升像素检测效率,还可能需要图像层级的分块与预处理;
- 所以不管是否做像素检测,这个分桶与矩形检测的流程都是必要且通用的,不会因精度需求变化而变化。
这层逻辑是稳定而独立的,可以为后续任何精度的碰撞检测提供高效的候选集合筛选支持。
为什么不让调试叠加层单独渲染,并按提交顺序绘制,避免浪费时间排序?
我们当前的调试系统确实包含了大量精灵,因此是一个非常合适的压力测试目标。虽然从表面上看,把调试覆盖层渲染到一个独立图层并按照提交顺序直接绘制,似乎可以避免对这些精灵进行排序,从而节省时间,但我们最终没有选择这么做是有充分理由的。
我们之所以仍然将调试图层纳入排序流程,而不是跳过排序,是因为:
我们需要验证排序系统的性能是否足够
调试图层包含大量精灵,因此是进行排序算法性能测试的理想场景。我们正在开发的排序机制正好可以借此机会进行全面测试,验证其在高负载下是否仍然表现良好。既然排序系统迟早要承受这种级别的压力,我们不如现在就测试它。目标是让排序本身足够快
我们的最终目标是设计一个足够高效的排序流程,以至于即使是调试图层这种精灵密集型场景,也不会成为性能瓶颈。如果我们在调试层上绕过排序逻辑,那就无法有效测试排序系统本身的效率。冗余优化是没有必要的
如果排序已经足够快,再对调试图层专门做跳过排序的处理,不仅复杂化系统逻辑,而且反而浪费开发时间。优化应该只在有明确性能问题时进行,避免“过度工程”。代码路径统一更有益于维护
让调试图层遵循和正常图层一样的处理路径,有助于维护和调试。如果调试图层用的是另一套逻辑,就容易出现调试路径和正式路径行为不一致的情况,反而不利于开发。
总结:
我们没有为调试图层特设“跳过排序”的优化路径,是因为当前排序系统设计目标就是足够快,哪怕面对高密度精灵也不应成为瓶颈。既然如此,调试层反而是理想的实战测试对象,我们通过实际运行情况验证排序效率,一旦它在这种场景下都能保持高性能,就无需为特定图层设计复杂的旁路机制。这不仅简化了系统逻辑,也更能确保整体鲁棒性和一致性。
我认为无排序模式在过场动画中也能很好地使用
过场动画的情况也完全适用于我们当前的排序模式,因此我们没有必要为此额外设置一种“特殊模式”。当前的排序逻辑已经足够通用,可以同时覆盖正常渲染流程和过场动画这类场景。如果没有充分理由,我们不会引入额外的处理模式,因为那样只会增加系统复杂性和维护成本。
我们始终遵循“保持统一逻辑路径”的设计理念:如果一种处理方式可以有效支持多个使用场景,并且没有性能或功能上的明显问题,那么就应该优先采用这种统一的方案,而不是为每个特殊情况都创建独立机制。这种方式更简洁、更稳健,也有助于后续系统的扩展和调试。
当前的排序系统既适用于实际游戏运行中大量精灵的处理,也能自然地应用于过场动画的显示逻辑,因此完全不需要在它们之间做区分。我们可以确信这套排序机制具备足够的通用性与效率,应对这类视觉演出没有任何问题。
为什么不使用英雄精灵的合并矩形来排序绘制,而是按固定顺序绘制各个部分?
我们计划使用英雄角色精灵的合并矩形来进行绘制排序,并且按照固定顺序绘制组成部分。这实际上是我们之前就打算做的事情。现在排序系统已经支持指定某个元素应该排在另一个元素之前,这给了我们更灵活的排序能力。
不过目前渲染系统还没有扩展到能够理解和利用这种排序优先级的信息,也就是说,这些排序优先级的信息还没有被传递到渲染管线中去,渲染流程无法根据这些信息进行调整。过去我们也没有真正能利用这类信息的方法,虽然理论上可以通过某种变通办法,比如将多个位图当成一个虚拟的整体去排序,但这并不理想。
总的来说,我们非常希望能够实现这种基于合并矩形的排序方式,让整体角色绘制更加有序和合理,但目前还没完成这个功能,尚未将这套排序信息集成到渲染管线中去。
使用C语言能比C++更优化游戏吗?
关于用C++优化游戏是否比用C更有效,这其实取决于“优化”的具体含义。从本质上讲,C是C++的一个子集,特别是C++11标准前后,C++增加了一些C没有的特性,但这些特性并不直接提升性能,更多是语法和功能上的扩展。
从优化角度来看,任何用C能写出的程序,基本上都能用C++写出来,二者在性能上的差别并不大。之所以有人觉得C代码比C++更容易优化,主要是因为C语言本身没有C++那些高级特性,程序员不会无意中使用会拖慢性能的复杂功能。
C++引入了很多自动化和复杂的特性,比如构造函数、运算符重载、类型转换等,这些看似简单的语法糖背后可能隐藏大量的运行时开销。程序员在不注意的情况下可能写出执行效率低下的代码,因为代码表面看起来简单,但实际上触发了许多函数调用和转换操作。
所以,C++代码变慢的原因通常不是语言本身的问题,而是程序员使用了语言的复杂特性导致的。如果程序员避免使用这些容易引起性能瓶颈的功能,C++的性能和C是相当的。
总结来说,C++本身并没有内在的性能劣势,只是其丰富的特性提供了更多可能导致性能下降的机会。只要合理使用,C++完全可以写出和C一样高效的代码。
RTTI(运行时类型信息)
只要不使用虚类型,运行时类型信息(RTTI)就不会对代码产生影响。换句话说,C++代码如果不涉及这些特性,生成的代码和C代码完全一样。也就是说,只有在真正使用这些语言特性时,才会产生额外的性能开销。
同样,异常处理也是如此。如果程序中从不抛出异常,编译器实际上不需要在代码中插入复杂的异常处理机制。当然,实际上像Windows结构化异常处理这类机制已经存在,所以即使是C代码,在某些平台上也可能承担部分异常处理的开销。
总体来看,只有主动使用C++的这些高级特性,才会让代码变得更复杂和更慢;如果不用,这些特性就不会影响性能。