游戏引擎学习第303天:尝试分开对Y轴和Z轴进行排序

发布于:2025-05-21 ⋅ 阅读:(26) ⋅ 点赞:(0)

成为我们自己的代码精灵α

所以现在应该可以正常使用了。不过,这两周我们没办法继续处理代码里的问题,而之前留在代码里的那个问题依然存在,没有人神奇地帮我们修复,这让人挺无奈的。其实我们都希望有个神奇的“代码仙子”,能帮我们把代码问题一挥而就,回来时发现所有问题都解决了,但现实是我们必须自己动手,自己想办法解决问题。既然如此,那我们就得自己动手“洒代码魔法粉”,开始修复工作。今天准备好键盘,准备开始继续努力了。

回顾并为今天的内容做准备

我们继续探讨精灵排序的问题。之前我们已经基本确定,单一的统一排序方法行不通,至少需要比统一排序更复杂的方案。我们开始尝试构思如果采用完整的图排序(graph sort)会是什么样子,但其实不确定是否真需要完整的图排序。离开期间,在论坛上有人提出了各种建议,有几种不同的解决方向,但目前还不清楚哪种方法才是最合适的。唯一可以确定的是,简单的统一排序无法满足需求,需要更复杂的处理方式。

Z缓冲不适合二维内容

从直觉上来说,我们认为使用Z缓冲区(Z-buffering)并不是一个好主意。原因在于,我们需要处理一些语义上的特殊排序,比如英雄的头部应该画在身体前面,尽管从技术上讲头部可能应该在身体后面。如果用Z缓冲,我们就得不断和它“作斗争”来实现这些特定效果,这会非常麻烦和复杂。

我们希望找到一种不用Z缓冲的解决方案,因为虽然Z缓冲在真正的3D环境下表现出色,但在2D或伪3D情况下,它往往需要很多“假装”的技巧来调整,确保它能按照我们想要的方式工作,同时避免出现错误的绘制顺序。没有真实的3D模型和形状时,想让Z缓冲准确无误地运作非常困难。

目前的代码和渲染器还在排序部分工作,具体实现还在调整中。我们开始考虑建立一个精灵图(sprite graph)的结构来处理排序问题,但具体细节还在探索阶段。接下来还要升级开发环境,解决一些版本问题,然后继续完善排序逻辑。

quartertron的排序建议,或者用插入排序实现简易的二叉空间划分法1

聊天中有人建议,可以先分别对Z轴精灵和Y轴精灵进行排序,因为这两类精灵各自内部的排序都是可行且正确的。然后再把这两个已排序的列表合并。这种思路可能解决一些排序问题,但具体是否正确还不确定,还需要进一步验证。

另外,有人提议可以用插入排序来模拟一种简易的二叉空间分割(BSP)树。插入排序不同于现在用的排序方法,它更像是逐个比较并找到元素合适插入位置,类似在一棵二叉树中逐层测试并定位。这种方法可能有助于更准确地对数据进行排序,形成一种类似BSP树的结构。

不过,这种方式的问题在于如何保持树的平衡,否则排序性能可能退化到平方级别(N²),这需要特别考虑和处理。

总体来说,采用分别排序Y精灵和Z精灵再合并的方法比较简单易行,但是否能解决现有的所有问题还不明确。为了进一步理解,也考虑查看当前的项目进展和数据,但还没有升级到最新的开发环境版本,这需要尽快完成。

黑板讲解:分别对Z轴精灵和Y轴精灵排序,然后合并排序这些已排序的精灵

我们现在讨论的方案,基本上是先把精灵分成两类:一类是平躺在地面的精灵,这些沿着Z轴排列,称为Z精灵;另一类是直立的精灵,面向我们,沿Y轴排列,称为Y精灵。

对于Z精灵来说,因为它们平躺且不会相互穿透,所以可以用Z轴坐标来排序,理论上不用管Y轴排序。即使两个Z精灵一个在另一个后面,只要它在空间中更高,也不会真正发生重叠。

对于Y精灵来说,它们直立且只沿Y轴排序即可,因为Y精灵之间不会因为Z轴位置变化而影响相互排序,只有前后关系决定绘制顺序。

这样,我们可以分别对Z精灵和Y精灵进行排序,保证两组各自有正确的绘制顺序。问题是排序好后,如何合并这两个排序列表?即在绘制时,如何决定先绘制哪类精灵,什么时候切换到另一类。

聊天中有人建议,先用各自的排序规则(Z轴和Y轴)对两组精灵分别排序,然后用一个合并步骤来将两个排序列表融合成一个最终绘制顺序。这个合并步骤可以用类似归并排序中的合并操作,利用一个简单的“假定”排序规则来决定哪个精灵先画。

这个方法的优点是灵活:我们可以用任意合适的排序算法(比如基数排序)分别对Z精灵和Y精灵排序,最后只需一个归并操作完成最终排序,拓展了排序算法的选择空间。

此外,这个方案虽然巧妙,但还未完成:我们还需要改进渲染部分,特别是考虑精灵的实际边界(范围),以避免绘制顺序错误。目前的边界处理还不完善,可能导致错误的绘制结果。

具体实现上,可以先在合并排序的第一步,将所有精灵分成两个桶(Y精灵桶和Z精灵桶),分别排序后再合并。如果发现这样做效率不高,可以考虑在生成精灵时就把它们分别放入不同的桶,以减少排序时的开销。

总的来说,这个方案思路清晰,易于扩展且有一定算法美感,值得尝试实现和测试。

game_sort.cpp:引入MergeSort()的两个版本——MergeSortY()和MergeSortZ()

我们现在讨论的合并排序算法,主要是基于“谁在前面”的排序标准来实现的。为了实现这种排序,首先需要对精灵的边界信息进行跟踪,比如每个精灵的最小Y值。排序的目标是确保绘制时,离观察者更远的精灵先绘制,离观察者更近的精灵后绘制,从而实现从后到前的正确渲染顺序。

具体来说,排序时会比较两个条目(例如两个精灵)的最小Y值。如果条目A的最小Y比条目B更小,说明A离观察者更近,应该后绘制,因此需要交换它们的位置,保证绘制顺序正确。排序过程中,在进行分区操作时也是基于同样的逻辑:优先绘制最大最小Y值的精灵。

类似地,对Z轴的排序也是用同样的合并排序逻辑实现,只是排序的依据变成了Z轴相关的边界值。对于X轴也可以这样处理,但目前重点还是在Y轴和Z轴的合并排序。

目前代码的实现比较冗长,很多功能是重复的,比如Y轴和Z轴的排序代码几乎一模一样,感觉有些重复,可能会考虑用模板或泛型来简化代码结构,减少重复代码。不过这样做会在编译时生成重复的代码,没办法完全节省最终输出代码体积,只是让源码更简洁。

总体来说,这个合并排序方案的核心是在排序时结合精灵的边界信息(最小Y或最小Z)来判定绘制顺序,保证视觉效果的正确。实现时需要克隆部分排序代码以便跟踪精灵边界,暂时没有办法用单一通用排序函数处理所有情况,但后续可能会寻找更优雅的代码合并方案。

先改一下tile_sort_entry

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

引入MergeSort()的两个版本——MergeSortY()和MergeSortZ()

在这里插入图片描述

game_sort.cpp:引入IsInFrontOf()的两个版本——IsInFrontOfY()和IsInFrontOfZ()

我们现在的目标是完成一种合并排序的方案,分别对Y轴和Z轴的精灵进行排序,然后再将它们合并为一个最终的渲染顺序。整个流程依赖于一个“谁在前面”的判断逻辑,即根据精灵的位置,决定哪个精灵应当先绘制。

核心逻辑如下:

1. 拆分排序

我们将所有的精灵分为两类:一种是以Y轴为主的精灵(站立的,比如角色、柱子),另一种是以Z轴为主的精灵(贴地的,比如地面效果、阴影)。对这两类分别进行排序:

  • Y轴排序规则

    • 若精灵A的最小Y值(ymin)小于精灵B的ymin,说明A靠近屏幕下方,也就是离观察者更近,应该后绘制。
    • 因此:如果 A.ymin < B.ymin,A 就应排在后面,发生交换。
  • Z轴排序规则

    • Z轴方向与屏幕朝向相反,因此Z值越大,代表越靠近观察者。
    • 因为精灵只有zmax(没有zmin),所以比较的是zmax值。
    • 若A的zmax大于B的zmax,说明A更靠近观察者,应后绘制。
    • 因此:如果 A.zmax > B.zmax,A 就应排在后面,也发生交换。

通过上述两个判断,我们就可以分别对Y轴精灵和Z轴精灵使用合并排序来获得两个有序列表。

2. 合并阶段

在完成Y轴和Z轴的独立排序之后,我们需要一个“最终合并排序”的阶段,将这两个有序列表组合成一个总的绘制顺序。

此合并过程需要一个统一的“谁在前面”的判断准则来决定从哪个列表中取下一个元素。例如:

  • 若当前Z轴精灵A应在当前Y轴精灵B之前绘制,那么就从Z列表中取出A;
  • 否则,就从Y列表中取出B。

这个逻辑可能仍旧基于类似于前述的“谁在前面”比较函数,也可能需要一个更高级的合并策略来处理交叉渲染的边界情况(比如一个Z轴地面元素压在一个靠前的Y轴角色脚下的情况)。

总结:

  • 我们实现了两个合并排序逻辑,分别基于Y轴最小值(ymin)和Z轴最大值(zmax);
  • 通过这种分开排序,再进行最终合并的方式,我们可以获得一种较为稳健的绘制顺序;
  • 当前的实现虽然冗长重复,但可以先确保逻辑清晰和正确;
  • 后续可以通过函数模板或策略模式优化代码结构,避免重复逻辑;
  • 这种排序方式避免了Z-buffer的复杂性和控制困难,适合需要自定义绘制顺序的半2D场景渲染逻辑。
    在这里插入图片描述

考虑如何区分Y轴和Z轴精灵

我们现在正在完善整个精灵排序系统的合并阶段。到目前为止,我们已经实现了两个分别基于Y轴(Y精灵)和Z轴(Z精灵)的合并排序逻辑。接下来的工作,是将这两种类型的精灵排序结果合并为一个统一的渲染列表。下面是我们详细的操作思路和处理流程:


1. 异质数据的预处理:分离Y精灵和Z精灵

这次的排序不再是对一个同质(相同类型)精灵集合进行排序,而是对一个包含Y精灵和Z精灵的混合集合进行操作。因此,我们第一步要做的就是将它们分离

具体思路是:

  • 创建一个临时缓冲区(Temp Buffer),大小等于当前所有精灵的缓冲区;
  • 将所有的Z精灵移动到这个临时缓冲区中;
  • 再将所有的Y精灵复制回主缓冲区中;
  • 最终形成两个独立的子集:一个包含所有Z精灵(在temp中),一个包含所有Y精灵(在原缓冲区中);
  • 这种方式利用现有的缓冲区空间来分离数据,避免额外内存分配。

这种处理虽然在效率上不是最优的,但目前的目标是先验证整个算法链是否可行。未来如有性能瓶颈,可以直接在输出时就将精灵写入不同的缓冲区,从根源上避免在渲染阶段再拆分。


2. 独立排序

完成分离后,我们对这两个子集分别使用前面实现的两个排序函数:

  • Z精灵使用基于zmax的排序逻辑(较大者优先绘制);
  • Y精灵使用基于ymin的排序逻辑(较小者优先绘制);

由于每个子集内部都是同一类型的精灵,它们的排序逻辑是一致的,排序过程是清晰且高效的。


3. 合并两个已排序的缓冲区

当Y精灵和Z精灵分别排好序之后,我们进行最后一步:合并这两个排序好的列表

此时使用的是“谁在前面”的判断逻辑(is_in_front_of):

  • 比较两个当前候选精灵的位置;
  • 判断哪个应当先绘制;
  • 将其插入到最终的渲染顺序中;
  • 依次推进,直到两个列表都被完全处理;

该阶段相当于一个双指针遍历合并排序(Merge Sort)中两个有序数组的标准合并步骤。


总结流程:

  1. 分离精灵

    • 使用一块足够大的临时缓冲区;
    • 将Z精灵移动到Temp,Y精灵保留在原缓冲区;
  2. 各自排序

    • Z精灵使用zmax降序合并排序;
    • Y精灵使用ymin升序合并排序;
  3. 最终合并

    • 使用统一的“前后”判断准则(is_in_front_of);
    • 将两个排序结果合并为一个最终渲染列表;

这种设计虽然初期在实现时多了一步数据拆分的开销,但其逻辑清晰,结构稳定,并为后续性能优化和算法替代提供了明确的边界(比如可以替换排序算法、改为GPU排序、使用并行策略等)。后期如果验证此逻辑正确且高效,可以再做底层结构调整,从一开始就将不同类型的精灵分开处理,进一步提升渲染阶段的效率。
在这里插入图片描述

game_sort.cpp:让MergeSort()区分Y轴和Z轴精灵

我们要遍历整个缓冲区中的所有元素。首先确定当前正在处理的精灵(sprite)在缓冲区中的索引位置,也就是我们当前所在的“first sprite index”。

然后,对于每一个元素,我们可以判断它是否是一个特定类型的精灵,比如是否是一个Z的精灵(IsZSprite)或 Z 向的精灵(Z sprite)。如果是某种特定类型,比如东向精灵,我们希望将其移动到临时目录(temp directory)中进行处理。

在实现上,我们可以使用一个类似 temp[zCount++] = me; 的语句,把当前的精灵对象放入临时数组中。这里 zCount 是一个用于记录已处理 Z 类型精灵数量的计数器,每次添加一个就自增一次。整个过程是一个草图式的设计思路,目的是先大致规划出整个数据筛选与处理逻辑的结构。

核心步骤包括:

  • 遍历整个缓冲区
  • 获取当前元素的索引
  • 判断是否属于某种类型(如 Z sprite)
  • 将符合条件的元素移动到临时结构中
  • 更新对应的计数器(如 zCount++)

整体意图是为了从缓冲区中筛选出特定类型的精灵,并为后续操作(如移动、排序或重组)做准备。
在这里插入图片描述

黑板讲解:分离并压缩数组

我们开始将元素从原始缓冲区移动到另一个临时缓冲区时,首先需要理解缓冲区中包含了各种不同类型的元素,比如 Z 精灵(Z sprite)、Y 精灵(Y sprite)等。假设我们希望把所有的 Z 精灵提取出来并放入一个新的空缓冲区中。

具体操作中,每当我们遍历到一个 Z 精灵时,将它复制到临时缓冲区中,这个步骤本身并不复杂。但是,关键在于:一旦将某个 Z 精灵移出原缓冲区,该位置就形成了一个“空洞”(hole),原来的数据结构中就出现了一个空位。

为了处理这个空位,我们需要在继续遍历的过程中执行压缩操作,即把后面的非 Z 元素(比如 Y 精灵)向前搬移,填补这些空洞。比如,找到一个 Y 精灵后,应将其拷贝到前一个空位所在的位置,保持原缓冲区的连续性。这种做法的目的是对原缓冲区进行就地压缩,使得未被移除的元素能够连续存放,避免中间出现无效的空隙。

因此,我们需要两个关键步骤配合完成:

  1. 从原缓冲区中筛选出所有 Z 精灵,并依次复制到临时缓冲区中;
  2. 同时在原缓冲区内,对剩余非 Z 元素进行压缩搬移操作,以消除由于移除 Z 精灵产生的空洞。

这样既能完成目标元素的提取,也能保证原缓冲区的数据结构维持紧凑。整个过程是为了实现数据结构的逻辑整理和优化,确保后续处理效率更高。

game_sort.cpp:继续实现MergeSort()分离精灵功能

我们在处理缓冲区中的精灵数据时,需要引入几个关键变量来辅助逻辑的实现:一个是 Y 精灵的计数器(yCount),一个是 Z 精灵的计数器(zCount),以及当前处理的索引(index)。

具体做法如下:

我们会有一个指向当前处理元素的指针,比如 sortSpriteBound,它等于原始缓冲区的起始地址加上当前索引 index,代表我们正在查看的那个元素。

接下来,我们检查这个元素的类型:

  • 如果是 Z 精灵,就把它复制到临时缓冲区中对应 zCount 所指的位置,并将 zCount 自增。这样做的结果是原缓冲区当前位置形成了一个“空洞”。
  • 如果不是 Z 精灵(即是 Y 精灵),我们就把它压缩移动到缓冲区最前面尚未使用的空间,也就是当前的 yCount 所指的位置,并将 yCount 自增。

需要注意的是,在最开始我们还没遇到任何 Z 精灵时,Y 精灵可能会被复制到它原本所在的位置,这种情况下会出现“自拷贝”,但这并不会影响结果,因为复制到自己身上是无害的。我们可以选择优化这部分逻辑,比如在位置相同的时候跳过拷贝,但当前阶段的重点不是性能优化,而是先确保逻辑正确性。优化的事情可以在确认这个方法是可行、值得使用之后再进行。

执行完这一遍遍历之后,我们会得到两个结果:

  • 原始缓冲区只剩下所有 Y 精灵,顺序被压缩整理过;
  • 临时缓冲区中存放着所有 Z 精灵。

接下来,我们可以将 Z 精灵从临时缓冲区复制回原始缓冲区的末尾,或者反过来,把 Y 精灵复制到 Z 精灵之后。这样做的目的是将两个类别的精灵组合回一个连续的数组中,同时保持顺序和结构的清晰。这种方法虽然不是最高效的实现方式,但在当前阶段我们更关注逻辑正确性而不是性能。

另外提到一个补充点:理想情况下我们应该提前准备好足够的临时缓冲区空间(temp memory),避免运行中频繁申请内存。如果是在更严谨的代码结构下,临时缓冲区应该作为参数传入,而不是硬编码处理,但在当前这个草图阶段,为了简化思路,我们可以先内嵌操作,以验证整体流程是否可行。
在这里插入图片描述

黑板讲解:两个数组的大小

我们在前面的步骤中,将缓冲区中的元素分离成了 Y 精灵和 Z 精灵两类,并分别放入不同的位置:Y 精灵保留在原缓冲区的前部分,Z 精灵则被移动到了一个临时缓冲区中。

现在我们意识到一个非常重要的事实:原缓冲区和临时缓冲区一开始的大小是相同的,因此分离出来的元素数量本质上决定了剩余空间的大小,也就是说:

  • 原缓冲区中,Y 精灵占据了前面一部分空间,后面的未使用部分正好等于我们提取出去的 Z 精灵数量。
  • 同样地,Z 精灵所在的缓冲区,只被填满了部分,剩下的空间正好等于我们保留下来的 Y 精灵数量。

也就是说,这两个缓冲区中未被占用的部分,正好可以用作对方的临时空间。这是一个非常关键的推理,因为它意味着我们不再需要额外的内存来进行归并排序的中间缓冲区处理。

因此,我们现在可以非常巧妙地利用这些已经“空出”的空间来执行归并排序的第一阶段。具体来说,我们可以:

  • 使用 Y 精灵所在缓冲区的“尾部空白”作为 Z 精灵排序的临时空间;
  • 使用 Z 精灵所在缓冲区的“尾部空白”作为 Y 精灵排序的临时空间;

通过精确计算指针位置和传入合适的参数,我们可以在不再需要额外内存分配的前提下完成整个排序流程的合并阶段。这种方法本质上是一种内存复用策略:利用数据结构自身经过预处理后产生的空白空间,实现无额外内存开销的高效排序。

这个设计显著提升了空间利用率,同时降低了系统复杂度,也为后续进一步的优化打下了良好的基础。

game_sort.cpp:让MergeSort()调用MergeSortY()和MergeSortZ()

我们接下来可以调用已有的归并排序(merge sort)函数来分别对 Y 精灵和 Z 精灵进行排序。这里使用的是之前已经实现好的标准归并排序算法。

我们现在已经知道:

  • yCount 是 Y 精灵的数量;
  • zCount 是 Z 精灵的数量;
  • 原始缓冲区 first 中前段是 Y 精灵;
  • 临时缓冲区 temp 中前段是 Z 精灵。

为了排序:

  • 对 Y 精灵排序时,我们以 first 为起点,传入的临时缓冲区是 temp + zCount,也就是跳过前面的 Z 精灵,用其后部作为临时空间;
  • 对 Z 精灵排序时,我们以 temp 为起点,临时缓冲区使用 first + yCount,也就是跳过前面的 Y 精灵,用其尾部作为临时空间。

这两个排序过程完成后,Y 精灵和 Z 精灵分别在自己的缓冲区中是有序的,且都已经就地排好。

需要注意的是,我们目前的实现中,归并排序函数在排序完成后会执行一个数据复制回原数组的步骤,即将临时缓冲区中的数据拷贝回原缓冲区。这一部分逻辑现在其实已经不太必要了,因为我们可以直接在原始数组中操作。但由于该归并排序函数是递归的,并且可能多次使用临时缓冲区中的内容,因此当前阶段我们还是保留这个复制操作,避免影响其正确性。不过,在最终优化中,这段复制逻辑是完全可以去除的,从而减少不必要的内存操作。

在完成对 Y 精灵和 Z 精灵的各自排序后,我们分别有两个已排序的子区段:

  • first 指向排序后的 Y 精灵;
  • temp 指向排序后的 Z 精灵。

接下来我们需要做的是:将这两个已排序的子区段合并成一个完整的排序结果。也就是实现归并排序的最后一步。

然而这一步的合并存在一个挑战:标准归并的策略通常是假设有一个足够大的空间可以将两个已排序数组按顺序合并写入。而在当前这个结构中,我们并不清楚是否有足够的临时空间可以支持这种合并操作,也无法像前面那样“巧妙”地利用已知空白区进行内存复用。

因此我们可能无法像前面那样精细复用已有缓冲区的空白部分来完成这一合并操作,这一阶段可能就需要一个真正独立的合并缓冲区,或采用一种额外的内存支持。这也提示我们,在后续实现中需要评估是否保留这种合并方式,或者重新设计这一阶段的内存策略以保持整体的空间效率。
在这里插入图片描述

在这里插入图片描述

黑板讲解:排序数据的复制

在这一部分,我们意识到一个非常巧妙的细节:尽管我们以为排序后的结果需要“拷贝回原缓冲区”,但其实并不需要。

原因是,在归并排序过程中,我们实际上是进行了一个“复制式排序”操作,也就是说,排序结果本质上已经被保留在原始缓冲区或另一个缓冲区中。换句话说,即便我们传入的是 firsttemp,排序函数本身并没有销毁源数据,而是将排序结果复制到了目标位置,同时源缓冲区中的数据依然完整保留。

这意味着我们在排序完成后,实际上手头有两个副本:

  • 一个是在目标缓冲区中的排序结果;
  • 另一个是在临时缓冲区中保留下来的排序副本。

因此,我们原本以为在归并排序后需要将结果再“拷贝回来”以保证顺序一致,其实完全没有这个必要。因为排序结果早就已经以正确的形式存在了,不管是要读取排序后的 Y 精灵还是 Z 精灵,都可以直接使用已经排序好的缓冲区内容。

这个认识带来了一个直接的优化机会:我们可以省略掉归并排序函数最后的“结果拷贝”步骤,既减少了一次数据传输,又提升了效率。

这同时也提醒我们,在整个排序系统的设计中,理解数据在缓冲区中的真实分布状态,以及每一步操作对原始数据和目标数据的影响,是非常重要的。掌握了这些,就可以做出更高效、更精简的实现逻辑。

game_sort.cpp:考虑让MergeSortZ()复制到临时缓冲区

我们在这里思考的是一种边界情况:当归并排序过程中实际上没有“实际工作”要做时(即数据已经是有序的,或只包含一个元素),我们该如何处理拷贝行为。

首先明确目标:最终所有的数据都应该集中在 first 缓冲区中。因此,在排序 Z 精灵的时候,我们不能完全依赖排序函数中的“就地结果”,因为在某些情况下(例如数据本身已经有序),排序函数可能不会实际执行任何拷贝动作,数据就停留在了 temp 缓冲区中。这就造成了一个潜在问题:我们最终需要的排序结果可能还留在了 temp 中,而不是 first 中。

为了解决这个问题,我们需要做两件事之一:

  1. 强制拷贝结果:即使排序过程中没有真正交换或移动元素,也要确保我们将排序结果复制到目标缓冲区(first)。这是为了保证最终的数据位置符合整体流程的设计要求。

    • 比如,在 Z 精灵排序时,如果排序函数的实现会将最终结果保留在 temp,那我们必须手动将其复制回 first
    • 同理,在 Y 精灵排序中也一样,确保结果最终都统一保存在 first
  2. 修改排序逻辑:也可以考虑修改归并排序函数,在其设计中支持显式指定“结果应该落在哪个缓冲区中”。这样在排序完成后,排序函数能自动判断当前操作是否需要执行真正的拷贝,或只是指针切换。这种方法更优雅也更高效,但也更复杂。

此外,还涉及一个小例子来说明这个问题:

  • 假设我们排序的是两个元素,分别是 first[0]first[1]
  • 如果它们本来就已经排好序了,那么排序函数可能根本不会将数据写入临时缓冲区;
  • 但在我们的系统中,我们预期排序结果应该最终位于 first 中。

因此,为了避免这些边界问题带来的混乱,我们当前阶段选择保守做法,即 无论是否真正发生排序操作,都执行一次结果拷贝,以确保数据位置正确可靠。这虽然可能多花了一点时间成本,但可以换来更清晰的逻辑和更容易维护的代码结构。

在未来优化时,我们可以再考虑是否对这些无效拷贝进行剪枝优化。
在这里插入图片描述

game_sort.cpp:当有1或2个Z轴精灵待排序时,让MergeSort()复制到临时缓冲区

我们现在讨论的是对 Y 精灵和 Z 精灵分别进行归并排序的具体策略,以及如何处理缓冲区的数据拷贝问题,确保最终数据正确且效率较高。

首先,针对排序的边界情况,如果 Z 精灵的数量小于等于 2(比如 1 或 2 个),我们必须强制执行一次数据拷贝操作,将结果从临时缓冲区复制回主缓冲区,因为在这种情况下归并排序不会自动完成拷贝。这是为了保证数据的最终一致性和正确性。

排序的流程是:

  • 对 Y 精灵执行归并排序,数量是 yCount,数据存在主缓冲区 first 中,临时缓冲区使用 temp + zCount(即跳过 Z 精灵部分的空间);
  • 对 Z 精灵执行归并排序,数量是 zCount,数据存在临时缓冲区 temp 中,临时缓冲区使用 first + yCount(跳过 Y 精灵部分的空间)。

这里我们用条件判断来区分 Y 精灵和 Z 精灵的排序依据:

  • 判断一个元素是 Y 精灵还是 Z 精灵,通过比较它的排序关键字(比如 wMinwMax)是否相等;
  • 如果不相等,则认为是 Z 精灵,按 Z 的排序规则进行排序;
  • 如果相等,则认为是 Y 精灵,按 Y 的排序规则进行排序。

针对小规模 Z 精灵(数量小于等于 2)的特殊情况,明确在排序后要把 temp 中的数据复制回 first,这样保证了主缓冲区包含所有最终的已排序数据。

接下来,我们整理了缓冲区的使用方案:

  • 主缓冲区 first 包含排序后的 Y 精灵;
  • 临时缓冲区 temp 包含排序后的 Z 精灵;
  • 临时缓冲区剩余部分和主缓冲区剩余部分互为对方临时使用的空间。

关于缓冲区的切换,优化思路是:

  • 原先可能会先从主缓冲区读取数据,再写入临时缓冲区,最终又要把数据复制回主缓冲区,产生多余的拷贝;
  • 优化后可以直接以临时缓冲区作为数据读取源,直接写入主缓冲区,从而避免最后的额外复制步骤。

具体实现上就是把归并排序的输入缓冲区改成临时缓冲区 temp,而输出缓冲区改成主缓冲区 first,利用好两者剩余的未使用空间作为辅助缓存。

这样一来,排序的流程更简洁,避免了不必要的数据搬运,保证了内存利用效率和性能。

总结来看,我们通过判断数据规模,合理安排归并排序的输入输出缓冲区,并配合必要的拷贝,确保无论大小数据都能正确有序地存放到预期的缓冲区,且整体内存操作更加高效、简洁。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_sort.cpp:清理编译错误

在这里插入图片描述

在这里插入图片描述

递归调用栈溢出吗

在这里插入图片描述

在这里插入图片描述

修改一下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

段错误

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏,观察排序结果是否接近预期

我们现在的进展虽然比之前好多了,明显有了改进,但整体来看距离理想状态仍然还有不少差距,工作还远未完成。虽然基础框架已经搭建起来,排序和合并逻辑也更加清晰,但仍存在需要优化和完善的地方,需要继续努力才能达到预期的效果和效率。

运行游戏,触发SortEntries()中的断言错误

我们打算更仔细地检查当前的排序和合并逻辑,尤其是比较操作部分。虽然有一点不确定,比如某些条件下的判断标准是否完全正确,但暂时先不急于断定有无错误。因为如果排序或比较逻辑本身没问题,那么排序整体才有意义。我们认为现在下结论还为时过早,重点是先细致地逐步梳理和验证每一步的实现,确保基础逻辑没有问题,再去排查潜在的错误。
在这里插入图片描述

在这里插入图片描述

调试器:进入MergeSort(),检查YCount和ZCount

打算深入检查排序的效果。首先,从整体流程看,我们开始时会检测缓冲区里有多少Z精灵和Y精灵,并分别对它们进行排序。观察排序完成后的结果,可以看到Z精灵和Y精灵的数量符合预期。具体来说,Z精灵数量远多于Y精灵,因为每个地板瓷砖都是Z精灵,而Y精灵通常是树、英雄等占用的对象。这种分布符合逻辑,说明分类和计数过程是合理的。接下来,我们应该更细致地检查排序后的具体内容,确认排序操作是否按预期正确执行。

game_sort.cpp:给MergeSort()添加验证测试

我们决定从索引0开始,遍历到Y精灵的数量范围内,逐一检查每个精灵,断言它们确实属于我们预期的类型。这样做的目的是验证分类和排序是否准确无误,确保每个位置上的精灵类型符合之前的逻辑判断,避免潜在的错误或数据混淆。这个步骤是为了对之前的处理做一个严格的确认,保证数据的完整性和正确性。
在这里插入图片描述

game_sort.cpp:引入IsZSprite()函数

我们在代码中定义了一个判断精灵类型的函数(比如isZSprite),并且考虑使用之前传递过的参数,比如sortSpriteBound(排序精灵边界)来帮助判断。通过写这个函数,我们可以更方便地根据传入的边界或条件来确定当前处理的是哪种类型的精灵,从而使分类和排序过程更加明确和结构化。这样做能够提升代码的可读性和可维护性,同时确保判断逻辑的一致性。
在这里插入图片描述

game_sort.cpp:继续完善MergeSort()的测试

我们根据是否是“IsZSprite”(假设是某种类型标识)来分类精灵。无论哪种情况,Y缓冲区里存放的是Y类型的精灵,Z缓冲区里存放的是Z类型的精灵。这样,我们明确区分了两种类型的精灵,确保它们分别归类到对应的缓冲区中,方便后续的排序和处理。
在这里插入图片描述

调试器:进入MergeSort(),确认通过测试

这样做能够给我们一定的保障,验证所有精灵确实被正确地分配到了两个缓冲区中,分类和排序过程没有出错。这看起来是有效的,接下来还可以进一步增加其他验证或处理步骤,确保整体流程的正确性和稳定性。

game_sort.cpp:引入VerifyBuffer()函数验证缓冲区内精灵类型,并让MergeSort()调用它

我们可以写一个验证函数,输入一个缓冲区,然后判断缓冲区里所有元素是否都是Z精灵或者Y精灵。传入true表示验证全是Z精灵,传入false表示验证全是Y精灵。这样,我们可以用它来验证各个缓冲区,比如用它来验证Y精灵缓冲区和Z精灵临时缓冲区。合并排序后,也可以用这个方法来验证排序结果是否正确。比如排序后,我们检查half 0缓冲区里的全部都是Z精灵,数量是Z计数;half 1缓冲区里的全部都是Y精灵,数量是Y计数。这样就可以确保排序和分类过程的准确性。
在这里插入图片描述

没触发断言

在这里插入图片描述

调试器:再次进入MergeSort(),确认数据都正确

现在我们至少可以验证两个主要缓冲区,确保它们输出的内容没有经过错误的魔法式乱序,所有元素都在正确的位置上。验证就是通过大量断言来完成的,确保分类和排序过程中没有出错。整个过程其实很简单,就是遍历检查所有数据是否符合预期,保证了数据的正确性和排序的准确性。

考虑做更好的合并操作

在验证完成后,确认排序的内容是正确且有序的,我们接下来需要把这些部分合并起来。不过现在的合并过程还有些问题,特别是在处理这些平面分层的Z轴和Y轴排序时,当前的合并方式可能不够理想。因为我们是按Z轴层和Y轴层逐步向上处理的,决定下一步该选择哪个层次时,简单判断两个元素是否重叠并不一定够用。

所以我们觉得需要设计一个更完善的合并方法,才能正确地将这些分层排序结果合并。具体该怎么做还需要进一步思考和调整。目前还不清楚别人是怎么处理这部分的,可能需要参考其他人的方案或者再深入研究。总之,合并部分是当前的关键难点,需要更多时间和精力来完善。

黑板讲解:排序的具体操作

现在遇到的情况是,从侧面观察场景,摄像机往下看,我们实际上是在沿着两个方向移动不同的“平面”。每次到达一个新的平面时,我们必须决定这个新的平面是不是应该下一个绘制,或者是之前的平面应该先绘制。这个决策看起来很难做出。

心里有个疑问,如果能准确地做出这个绘制顺序的决策,那为什么不能用一个单一的排序标准来完成呢?这让整个方案显得有些怪异,不太确定目前的方法到底能不能真正奏效,毕竟很难判断哪一个平面该先绘制。

总之,这部分的排序和绘制顺序问题还很复杂,暂时没有明确的解决方案,还需要继续思考和尝试。

game_sort.cpp:查看IsInFrontOf()函数的实现

我们现在快速回顾一下当前的逻辑:

目前处理的是 Z 精灵和 Y 精灵之间的排序问题。在实际测试中,“两个都是 Z 精灵”的情况现在理论上不会发生了,至少在设计上不应该再出现这种情况。虽然在验证函数(如 is_in_front_of)中可能仍然会调用这种情况,但在实际排序逻辑里不会遇到两个 Z 精灵的比较。

在比较时,对于“两个都是 Z 精灵”的分支逻辑,当前逻辑会绕过它。我们现在会进入接下来的两个判断分支,也就是判断 A 包含 B 或 B 包含 A 的部分。

因为现在我们不再有两个 Z 精灵之间的比较,那么在判断是否要以 Z 为主排序时,之前的逻辑是:

  • 如果两个都是 Z 精灵,或者它们之间有 Y 轴重叠(即 Z 精灵和另一个精灵的 Y 范围有交集),那么我们以 Z 值排序;
  • 否则就以 Y 值排序。

这意味着:即使一个是 Z 精灵,另一个是 Y 精灵,只要两者在 Y 轴上存在重叠,就仍然按 Z 值排序,以保证视觉层次正确;如果没有交集,就按 Y 值排序。

接下来逻辑上我们比较的是:

  • 如果按 Z 排序,就比较 zmax
  • 否则按 ymin

这一点与之前的排序方式保持一致,也就是说:

  • 如果 Z 值有重叠或者是纯 Z 精灵比较,则使用 zmax 决定前后;
  • 否则,使用 ymin 来判断。

整体来看,这套排序系统试图通过判断几何包围盒在 Y 和 Z 平面上的关系来决定绘制顺序,核心是保持视觉正确性。逻辑逐步剥离了复杂情况,比如两个都是 Z 精灵的分支,从而简化了判断流程。下一步仍需要继续验证这些逻辑在边界条件下是否能够稳定运行。

# game_sort.cpp:让VerifyBuffer()验证精灵排序是否正确

现在我们对 verify_buffer 函数进行改进,目的是不仅仅验证缓冲区中的元素是否是 Z 精灵或 Y 精灵,还要验证这些元素在排序上是否是正确的顺序。我们添加了一个新的逻辑:遍历整个缓冲区,确保其中每个元素都“在前”于它前面的那个元素,也就是说,缓冲区中的元素已经正确地按绘制顺序进行了排序。

具体实现方式是:

  • 遍历缓冲区,从第一个元素开始(跳过索引 0);
  • 对于当前元素和前一个元素,调用 is_in_front_of 或类似函数;
  • 确保当前元素处于正确的前后顺序(即当前元素要在前一个元素的后面);
  • 如果不满足条件,则通过断言抛出错误。

这样做的好处是,我们不仅可以验证缓冲区是否装入了正确类型的精灵(Z 或 Y),还可以验证排序是否生效,从而捕捉潜在的排序逻辑错误。

这种检查属于双重验证机制:

  1. 类型验证:确保 Z 精灵和 Y 精灵没有被混入错误的缓冲区;
  2. 顺序验证:确保排序逻辑(如根据 zmaxymin)在结果上是有效的。

这让我们对排序流程的准确性更有信心,也使得调试更加高效。

在这里插入图片描述

在这里插入图片描述

调试器:运行游戏时触发VerifyBuffer()断言

在进入合并操作的核心逻辑之前,我们发现自己之前出现了一些混乱。面对这种情况,我们的处理方式是:由于整个过程中有大量数据在流动,如果一味依赖调试器逐步查看数据,会变得非常低效。因此,我们选择了一种更直接的策略 —— 编写专门的测试代码,对关键状态进行自动验证。

我们采用的思路如下:

  • 不是试图在调试器中手动检查大量变量,而是通过添加断言或验证函数来自动验证数据是否符合预期;
  • 特别是在排序合并前,我们增加了验证函数 verify_buffer,用于检查精灵是否被正确分类(Z 精灵或 Y 精灵);
  • 同时,我们还在验证函数中添加了顺序检查,确保排序操作之后的数据确实是按预期顺序排列的;
  • 这些验证手段帮助我们快速发现数据处理中的逻辑错误,而不需要反复手动调试。

这种方法的优势在于能在逻辑混乱或者状态复杂的阶段,及时拦截并定位潜在问题,为后续合并处理的“魔法部分”提供了一个干净、可信赖的数据基础。通过自动化验证手段,我们可以更有信心地进行后续复杂逻辑的实现与测试。

关于“测试驱动开发”的简短讨论

我们之所以选择在这一阶段添加更复杂的测试逻辑,是基于一个明确的权衡判断:通过写测试代码来验证当前的排序和合并逻辑,可能比手动排查 bug 更高效。这并不严格属于“测试驱动开发(TDD)”,但背后的核心思想是类似的,即在开发过程中判断写测试是否能节省时间。

在这种思考模式下,我们始终在做一个理性取舍:

  • 每当遇到需要调试的代码时,我们会思考:如果此处写测试,是节省时间还是浪费时间?
  • 大多数情况下,尤其在游戏开发中,写测试未必划算,测试代码本身的维护成本也不容忽视;
  • 但也有例外,比如当前这种排序和合并逻辑复杂、数据交互繁多的情形,写测试反而是更省时省力的方式;
  • 这也是为什么我们在这个阶段选择了编写自动化断言和验证函数的原因,期望能更快发现并定位 bug;

当然,目前的测试代码是否能准确发挥作用,还要进一步确认。有可能我们并没有发现真正的 bug,而只是测试本身写错了。这也是为什么我们认为测试驱动开发虽然有帮助,但始终伴随着一定成本的原因——测试逻辑本身也必须正确,才能发挥其价值。

总结来说,在开发过程中,我们持续动态评估写测试是否值得,尤其在逻辑密集或调试成本高的环节,测试往往能带来切实的帮助。此刻,我们正是基于这样的考量,决定通过测试来加速问题定位与验证流程。

调试器:进入VerifyBuffer(),检查失败时精灵的位置

我们决定深入检查循环第一次执行时的具体情况,以确保整体逻辑大致正常。从测试结果来看,虽然有部分测试通过,但仍存在失败情况,说明测试并非完全无效,而是有一定参考意义的。

在某次失败的测试中,我们发现被比较的两个精灵具有相同的 Z 值。也就是说,这两个对象在深度排序上没有明确的先后顺序。正是这种情况导致断言失败,引发我们进一步的思考:当两个对象具有相同的 Z 值时,是否仍然应该执行进一步的排序逻辑来保证稳定性。

于是,我们开始重新评估当前的排序策略。最初的做法是在某些条件下才使用特定的排序逻辑,比如仅在两个对象都为 Z 精灵或存在遮挡关系时,才进一步判断谁在前。但从这次测试可以看出,这种分支逻辑在 Z 值相等的情况下可能不足以处理所有边界情况。

因此,我们开始倾向于调整逻辑,将原本只在部分条件成立时才使用的比较方法,改为在所有情况下都统一使用相同的排序函数。这种做法能确保排序在所有输入下都保持一致性和确定性,避免排序不稳定带来的错误或视觉问题。

这也意味着,在深度值一致的前提下,我们将使用一个更精确或更严格的比较函数,以明确决定绘制顺序。这种处理方式不仅增强了系统鲁棒性,也为后续调试减少不确定因素。整体来看,这次测试失败虽然暴露了问题,但也帮助我们理清了逻辑盲点,推进了排序机制的完善。
在这里插入图片描述

game_sort.cpp:将MergeSortY()转变为MergeSort(),以便同时使用IsInFrontOf()进行判断

我们开始反思当前的处理方式,意识到或许原先的策略才是更合理的选择。我们之前尝试在某些情况下简化判断逻辑,仅在特定条件成立时才使用 is_in_front_of 来决定绘制顺序,但测试中暴露出的错误表明这种做法可能不够严谨。

因此我们考虑恢复原来的方式,即无论在何种情况下,都统一调用 is_in_front_of 来决定排序顺序。这样可以在 Z 值相等时,进一步依据 Y 值或其他因素进行细致排序,从而避免出现排序不稳定的情况。例如两个 Z 精灵 Z 值相同但位置略有不同时,这种精细判断显得尤为重要。

我们意识到,通过统一使用 is_in_front_of 来比较两个对象的优先级,可以带来更一致的行为,也更符合绘制的逻辑需求。这一方式不仅适用于 Y 精灵之间的排序,也适用于 Z 精灵之间的排序。Z 值一致时,通过 Y 值来补充判断,可以保证视觉上的正确遮挡关系,避免混乱。

接着我们还注意到命名上的一些模糊问题。虽然目前的排序函数被称为 merge_sort,但它实际上承担了更多职责,不仅是执行标准的合并排序,还融合了复杂的绘制优先级逻辑。为此我们开始思考是否需要为这个过程更换更准确的函数名,以更清晰地表达其真实作用。毕竟,这不仅仅是一个通用排序过程,更像是一个绘制排序逻辑驱动的过程。

总的来说,我们决定恢复统一的排序判断逻辑,即始终使用 is_in_front_of 来判断对象的前后关系。这样做不仅提高了系统的稳定性和正确性,也简化了处理逻辑,避免因不同分支判断带来的隐藏 bug。同时,我们也开始审视函数命名的清晰度,希望通过明确职责来提升代码的可读性和可维护性。
在这里插入图片描述

game_sort.cpp:将MergeSort()重命名为SeparatedSort(),并完全移除MergeSortZ()

我们决定对当前的排序逻辑进行更明确的区分和命名。将目前这个按照绘制前后关系进行判断的排序过程命名为 merge_sort,因为它真正执行了合并排序的核心操作。而之前的那个初步分类排序过程则被称为 separated_sort,它的主要功能是将不同类型的精灵(如 Z 精灵与 Y 精灵)分离到各自的缓冲区中,而不是进行真正的排序逻辑处理。

接下来,我们决定简化并统一排序逻辑。我们去除了之前在某些特定情况下绕过 is_in_front_of 的判断逻辑,恢复为在所有排序过程中都统一使用 is_in_front_of 来判断前后绘制关系。这意味着无论是在处理 Z 精灵还是 Y 精灵的排序中,始终通过该函数来决定两个元素的相对顺序。

这样做有几个明显的好处:

  1. 逻辑统一性:不再需要判断当前是否是 Z 精灵还是 Y 精灵,只需一律用 is_in_front_of 来判断,简化了代码结构。

  2. 提高稳定性:过去的逻辑在精灵属性接近或重叠时可能出现排序不一致的问题,统一使用 is_in_front_of 能避免这种排序模糊性。

  3. 更贴近实际需求:绘制顺序本质上是一个空间中的可视优先级关系,而 is_in_front_of 正是用于表达这一空间逻辑的函数,用它做排序依据更符合系统设计目的。

  4. 减少Bug风险:不再依赖多个路径或分支处理排序,有利于排查错误与调试。

最后我们将所有使用排序的地方都更新为调用统一的 merge_sort 方法,确保在任何时候,排序都是依照同样的规则进行。这一步标志着我们彻底抛弃了之前试图通过条件判断来优化排序性能的做法,而是回归到一个更稳定、可维护、逻辑清晰的排序策略。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

调试器:运行游戏,捕捉测试认为排序错误的精灵

我们现在使用统一的排序方式,在排序过程中保持了 Y 和 Z 精灵的正确逻辑,即无论是哪种类型的精灵,都通过 is_in_front_of 来判断前后关系。然而,我们在某个排序断言中触发了失败。

根据调试信息的观察:当前排序中出现了一个距离我们更近(Z 值更小)的精灵被排在后面(也就是绘制得更晚),这表面上看是符合预期的。但问题在于:虽然该精灵在 Z 上靠近我们,但它的 Y 值却比另一个精灵低,且两者中只有一个是 Z 精灵。

这就造成了矛盾和困惑:为什么 is_in_front_of 没有判定靠近我们、Y 值也更低的那个精灵应该更早绘制?

我们怀疑问题出在 is_in_front_of 对混合精灵类型的处理上。当两个精灵中只有一个是 Z 精灵时,排序逻辑会依赖于特定条件,比如是以 Z 为主、还是 Y 为主。这种情况下的处理可能不够严谨,或逻辑没有覆盖所有的实际交错情况。

因此,我们有以下几点思考:

  1. Z 精灵 vs Y 精灵混合比较逻辑需明确
    当前 is_in_front_of 的实现可能只在两者同为 Z 精灵或 Y 精灵时表现良好,而对混合场景未进行完善的判断逻辑处理。

  2. 是否需要强制同类型精灵优先比较?
    例如:若一方为 Z 精灵而另一方为 Y 精灵,是否应该先依据精灵类型决定排序优先级,再比较坐标。

  3. Y 值在某些场景下优先级是否应高于 Z?
    因为 Y 轴往往表示屏幕“上下”,而 Z 表示“深度”,但渲染顺序取决于美术意图,比如俯视或侧视角度。

  4. 需要补充测试用例与日志
    为了进一步确认 is_in_front_of 的判断是否合理,可能需要打印出相关精灵的详细坐标、类型及其排序结果,排查可能出现的逻辑误判。

综上,我们意识到混合精灵排序是个边界复杂的情况,当前的逻辑在该情况下还存在不清晰或潜在错误,需要进一步明确规则与实现逻辑,确保判断稳定一致。后续应在 is_in_front_of 内部增加更精确的精灵类型和坐标综合比较,以避免此类断言失败。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

问答环节

表示尝试用不同方法,利用屏幕上投影的精灵和它们的最小/最大Y值(上部)作为深度排序的关键

我们尝试采用一种不同的方法,即利用投影到屏幕上的精灵图像进行排序,并使用这些精灵在屏幕上的最小和最大 Y 值作为深度排序的关键值。这种方法的核心思想是通过分析精灵在最终投影画面中的覆盖区域,来确定它们在视觉上的前后关系,而不是依赖于三维空间中的 Y 或 Z 值直接排序。

目前,这一方法仍在推进中,尚未完全完成或验证其效果,正在开发阶段,尚未得出最终结论或实用成果。不过这种方法本身具有一定潜力,特别是在复杂重叠和倾斜视角场景下,能够更贴近真实渲染顺序的判断逻辑。

我们会持续关注这一方向的进展,目标是更准确地处理精灵之间的深度遮挡问题,确保在任何角度和布局下都能保持正确的绘制顺序。

正在进行中

我们想了解一下Quarter Tron对我们当前实现方法的看法,特别是想知道他是否认为这种实现是合理的,或者这是否和他之前尝试的方法相似。由于没人主动提问,我们自己也在思考是否应该主动去问他的意见,以便确认目前的思路和实现是否在正确的方向上,或者是否存在值得借鉴的经验和改进空间。

问什么是全栈开发者?

全栈开发者,实际上就是以前人们称之为程序员的那种角色。全栈开发者不仅仅会写某一种语言,比如仅仅会写JavaScript,还要对计算机的底层原理有一定的了解。他们不仅会写前端界面,也能够理解甚至修改底层数据库等技术。举例来说,一个只会发SQL查询语句,看到结果却不懂底层数据库是如何运作的人,并不能算是真正的全栈开发者。因为他们不知道数据是如何“魔法般”地被处理和返回的。

所谓全栈开发者,其实是能够完成从前端到后端再到底层系统整合的“端到端”编程工作的人。他们可以被指派去完成各种编程任务,而不是只会写网页前端或者只会数据库查询。如果一个人只懂写网页但不懂后端,或者只懂数据库却不会写前端代码,那么他们都不算是全栈开发者。

现在软件开发领域工作量庞大,分工也越来越细,有很多人虽然做着开发相关的工作,但并不具备全栈开发者的全面技能和视野。全栈开发者则是那些既懂前端技术,也懂后端架构,还理解系统运作原理,能够处理各种编程挑战的综合型开发人员。

质疑排序会失败,因为当Y轴精灵重叠时,较低的Z值有时需要画在较高Z值之上

发现排序失败的原因在于,有时Z值较低的元素需要绘制在Z值较高的元素之上,尤其当Y轴方向发生重叠时。这种情况导致简单的基于Z值的排序无法正确反映实际的绘制顺序。对此,有人提供了一个很好的示意图来说明这种情况,图示清晰地表现了为何仅凭Z值排序不足以保证正确的显示顺序,特别是在Y轴上发生覆盖时。考虑到这一点,需要在排序算法中同时考虑Z轴和Y轴的关系,以解决绘制顺序的问题。

黑板讲解:PoohShoes的示意图

从侧面观察,有三个元素A、B、C,从摄像机视角来看,A在最前面,C居中,B在最远处,但B的Z值比C还低。这导致单纯用Z值排序的方法无法正确排序,因为看起来顺序应是B最远、C中间、A最前,但Z值却不符合这个关系。这个问题实际上回到了之前遇到的循环依赖问题,说明仍然需要使用图结构的方法来处理这种复杂的覆盖关系。虽然我们之前打算用图的方式解决这个问题,感觉还是必须这样做。虽然如果能避免使用图结构会更好,但目前看来难度较大,所以还是得用图的方法去处理。

问之前提到的“插入排序”和自动生成二叉树表示的想法最后怎么样了,假设对所做内容有大致理解

我们曾考虑过用插入排序,并且尝试自动生成一个二叉树来表示这些精灵。这个想法的核心是通过插入排序逐步构建一棵二叉树,以便更有效地管理和排序精灵。不过,目前我们还没有完全实现这个方案,主要是因为我们还不确定它是否可行,或者是否能解决现有排序中的复杂问题。如果这个方案有效,我们可以尝试在接下来的工作中实现它,作为对当前方法的补充或替代。现在我们仍在评估当前算法的可行性,确保没有遗漏什么关键点,才能决定是否要转向这个新的思路。

黑板讲解:图排序

我们设想用插入排序构建一个二叉树来排序精灵,具体做法是:比如有精灵B,B作为树的根节点;接着遇到精灵A,判断A在B后面,所以A放在B的左侧;然后遇到精灵C,C在B前面,所以放在右侧;再遇到另一个精灵D,先判断D在B的哪一侧,再判断D相对于C的位置,最终插入到合适的位置。这个过程通过不断比较和判断来决定精灵在树中的位置。

然而,问题在于这种方法可能会退化成和之前遇到的排序问题一样的复杂情况。因为如果两个精灵之间关系不明确,或者它们的位置无法明确分割,就无法通过简单的树结构来解决。这导致插入排序的二叉树方法并不能避免排序中的循环依赖或冲突问题。

因此,我们怀疑真正有效的方案只能是基于图结构的排序方法,即构建依赖关系图,通过图的拓扑排序等方法来解决精灵排序问题。总的来说,这种插入排序+二叉树的方式因为无法准确处理复杂的空间关系,难以取代图论方法。

问如何将精灵从世界坐标转换到屏幕坐标

我们把世界坐标转换成屏幕坐标,基本上就是用一个标准或者非常基础的正交投影来完成的。这个过程本身比较简单,并没有做太复杂的操作,就是把三维空间中的点直接映射到二维屏幕上。这样处理后,屏幕上的坐标就能用来进行后续的排序和绘制工作。整体来说,这个转换步骤并不复杂,属于基础的图形学操作。


网站公告

今日签到

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