公众号江苏建设信息网站,永康做网站的公司,上海电商网站设计,WordPress去掉你的位置回顾并为今天的内容做准备
昨天#xff0c;我们解决了一些关于排序的问题#xff0c;这对我们清理长期存在的Z轴排序问题很有帮助。这个问题我们一直想在开始常规游戏代码之前解决。虽然不确定是否完全解决了问题#xff0c;但我们提出了一个看起来合理的排序标准。
有两点…回顾并为今天的内容做准备
昨天我们解决了一些关于排序的问题这对我们清理长期存在的Z轴排序问题很有帮助。这个问题我们一直想在开始常规游戏代码之前解决。虽然不确定是否完全解决了问题但我们提出了一个看起来合理的排序标准。
有两点不确定第一我们不能百分之百确定这个排序标准总是适用于所有情况尽管我们尽量考虑了大多数可能遇到的情况但也可能有未想到的特殊精灵情况。第二点则是关于排序算法本身的问题。即使排序标准是合理的它也可能不会产生一个完全一致的排序结果也就是说排序标准不一定能保证产生一个全序或部分有序的关系。这样使用常规的排序算法比如归并排序可能无法正确解决排序问题。
我们计划用一个简单但开销较大的N²算法在调试场景下检查排序结果。具体做法是排序完成后逐对比较所有精灵根据排序规则确认排序是否正确。如果发现有任何排序顺序错误就说明确实遇到了无法用简单排序解决的问题。
这种情况并不致命只是意味着需要使用更复杂的图论方法来解决排序问题。昨天提到的 Andrew Russell 写的博客中讲了类似的情况他在对《River City Ransom Underground》的精灵排序时发现只能用带有语义理解的图排序方法因为存在不可排序的情况。
虽然目前还不确定是否必须用图排序但如果必须用情况也还算好因为可以解决问题。只不过我们更倾向于用更简单的排序方法比如归并排序因为图排序可能效率更低。虽然我们还没有具体测试图排序的性能但一般情况下简单排序会更快。
总之目前我们已经写好了排序代码代码可以编译但运行时发现排序功能并没有被实际调用程序仍在执行旧的排序代码这不是我们想要的效果。接下来会继续调试和测试看看排序在真实场景中的表现是否符合预期是否会遇到上述两种潜在问题。
game_sort.cpp仔细检查 IsInFrontOf() 函数的方向是否正确
今天的主要目标是继续完成排序逻辑的最后一部分。排序系统基本已经写好了不过还剩下一个需要处理的问题。此前有个名叫 Steven或四分之一Tron的观众在聊天室里指出了一个潜在的错误。他提醒我们在处理两个都是Z类型的精灵时可能忘记翻转某些符号方向。
具体来说Z类型精灵是指带有范围边界的精灵。问题涉及的判断逻辑是当两个精灵都不是“扁平”的也就是说它们的 YMin 和 YMax 不相等时我们就认为它们都是Z类型的精灵如果其中一个的 YMin 等于 YMax那么它就不是Z类型。
最初的逻辑是如果两个精灵的 YMin 不等于 YMax那它们就是Z类型否则就是一个是Z类型一个不是。回头看这段判断逻辑还是觉得是正确的。因此也不确定聊天室里提到的错误是否真的存在。暂时认为这个逻辑没问题但也不排除可能忽略了某些细节。
此外在这过程中还出现了一个技术问题——键盘暂时失灵导致操作受阻但很快恢复了正常。
总之现在的重点就是继续推进保持代码逻辑清晰必要时结合聊天室里的反馈进一步修正排序判断的边界条件。后续会继续观察排序系统在实际运行中的表现确保所有Z轴相关的排序处理都能正确执行。 game_sort.cpp考虑如何免费实现分层排序并将整个渲染器转变为基于精灵边界的排序
我们已经完成了我们需要的归并排序merge sort现在它可以对 sort_sprite_bound 类型的数据进行排序。不过这个过程当中有一些有趣的细节值得深入思考虽然暂时不确定是否要马上利用它们。 当前排序的数据结构
除了 YMin、YMax、Z 和 ZMax 等用于排序的字段之外每个 sort_sprite_bound 还带有一个 Index 字段这个字段指向我们最终要绘制的对象。 引入“虚拟平面”的想法
我们突然意识到一个潜在的优化思路 可以人为插入一些“虚拟平面”作为图层的分隔标志从而实现图层内自动排序。
假设我们插入一个拥有“无限 Y 区间”的虚拟物体它在 Y 轴上会始终被认为是一个 Z 精灵由于它没有实际绘制内容只存在于排序阶段那么在排序过程中它就可以作为一个“层分割线”将所有 Z 值在其下方或上方的元素分组通过这种方式我们在排序的同时也顺便将内容分隔成了不同的图层。
当我们在之后处理排序结果、依次“退役”渲染指令时即开始真正绘制只要检测到这些“虚拟平面”即可知道是否到了新的图层。
这是一种聪明的技巧用归并排序的规则自动实现图层划分无需额外的图层管理逻辑。 现有排序系统中的处理
我们当前的排序系统是用于多种用途的比如用于显示性能计数器时也要用排序。 所以我们不应该破坏原有 sort_entries 的那部分逻辑它仍要继续保留。
接下来我们的计划是
把整个渲染器的排序方式切换为基于 sort_sprite_bound将所有需要绘制的内容推入 sort_sprite_bound 列表中借助排序规则自动实现合理的前后关系和图层分隔。 实际挑战数据准备
实现排序不难真正的挑战在于我们现在并没有准备好所有必要的排序信息。
我们需要确定每个图形元素的
YMin / YMax表示它在屏幕上的垂直覆盖区域Z / ZMax表示其深度范围用于在 Z 精灵中判断前后关系
特别是我们常用的“直立卡片”类型的元素如角色等它们是竖直的、站立在地面上的可以忽略 Y 区间的跨度仅需设置一个 YMin YMax 的值。
这样做对我们的排序规则非常友好因为它会优先按 YMin 排序省去了复杂的计算。
而对于一些“地面瓷砖”类型的元素它们是平躺的贴图有实际的面积覆盖
我们确实知道它们的大小因为地面砖块通常都是单位格所以我们可以准确地设置它们的 YMin / YMax从而参与到更复杂的排序中。 理想情况
如果一切顺利我们只需要为这些“地面元素”设置正确的边界信息然后对所有元素调用一次排序函数即可得到正确的绘制顺序无需额外的图层机制或特殊处理。
这会大大简化渲染流程而且运行效率也较高。 下一步计划
替换渲染器使用 sort_sprite_bound 的方式插入实际的渲染指令逐步调试排序结果是否正确如果必要引入“虚拟平面”作为图层隔断 这是当前的分析与计划。接下来就要开始动手处理这一切了。
game_render_group.h考虑完全从 entity_basis_p_result 中去除 SortKey
我们现在的进展是这样的 当前渲染流程的结构Render Group
我们正在使用的渲染系统中主要依赖一个叫 render_group 的结构来推送所有的渲染指令。而我们以往在这个过程中使用的是一种叫做 SortKey 的机制来排序可见实体entity basis和相关的基础绘制结果basis result。
但是随着我们切换到使用 ZMax 和 YMin/YMax 为核心的新排序机制旧的 SortKey 方案将不再适用。 可能不再需要 SortKey
从现在的排序规则来看我们实际上只关心 ZMax 值这是排序判断“谁在谁前面”的核心因素之一。这个值理论上并不需要进行复杂变换
它只需要相对于摄像机的位置但其实即便不做这个偏移也可能不影响排序因为我们排序是相对的只要所有值在同一个空间里排序顺序就仍然是有效的。
因此如果我们足够幸运可能可以完全移除 SortKey 相关的逻辑在 entity 和 basis result 等结构中都不再使用它。这样会让代码变得更简洁、逻辑更清晰。
不过我们也不能太早下结论现在还不能确定这个设想是否完全可行只能说这是一个很好的可能性。 接下来的目标
暂时不去管是否真的能完全摆脱 SortKey我们还是把重点放在
将新的排序逻辑正式集成到渲染器确保所有实体和绘制项都能正确地填入 ZMax、YMin/YMax 等关键数据替代原来的排序流程检查整体排序是否正确地反映了视觉前后关系。
我们将以此为方向继续推进渲染系统的重构和测试。
game_render_group.cpp让 PushRenderElement_() 接收 sort_sprite_bound 而不是 SortKey
我们目前正在重构渲染系统的排序部分把原本使用的 SortKey 排序逻辑全面切换为基于 sort_sprite_bound精灵边界的新排序方法。以下是我们这段时间处理的核心内容与思路 将 SortKey 替换为 Sprite Bound 排序逻辑
之前的做法是在推送渲染元素Render Element时会生成一个 SortKey并将其作为 sort_entry 存储用于后续排序。
现在我们不再使用 SortKey而是使用结构体 sort_sprite_bound它内部包含了更丰富的空间信息
YMin, YMax: 精灵的垂直边界范围ZMax: 精灵的深度排序依据Index: 实际渲染命令在 push buffer 中的位置。
我们开始在代码中替换所有以 sort_entry 命名的逻辑为使用 sort_sprite_bound。 渲染流程中的调整
在实际操作中我们发现我们并没有在太多地方使用 sort_entry这使得替换的复杂度没我们预期的那么高。
我们定位到具体调用 PushRenderElement 的地方然后修改逻辑使其推送 sort_sprite_bound此结构比原来的 sort_entry 稍大大约两倍但仍然在可接受范围内。 Push 过程的关键细节
我们注意到 sort_sprite_bound 中的 Index 成员并不应该由调用者填充而应由 PushRenderElement 内部计算并设置
因为只有 PushRenderElement 知道渲染命令在 buffer 中的偏移量所以在调用方传入时只需要传入 YMin, YMax, ZMax 等排序信息Index 字段应在 push 时自动设置确保其准确对应实际的渲染命令位置。
这就带来了一个小问题我们不能直接传入完整的 sort_sprite_bound 实例因为其中的一个字段Index是由函数内部控制的而不是由调用者决定的。 设计上的权衡思考
这让我们开始思考是否需要调整结构设计
是继续保留当前设计在外部构建 sort_sprite_bound再在 push 时覆盖 Index还是将 Index 从结构中剥离让排序信息和缓冲区位置分开管理
目前还没有定论但我们倾向于保留结构的完整性只是在使用时注意内部字段的控制归属。为了简化接口与使用未来也可能考虑封装成构造函数或者构建器来创建这个结构。 当前目标
用新的 sort_sprite_bound 结构完全替代旧的排序逻辑将所有 PushRenderElement 逻辑改为支持新结构确保 Index 正确反映渲染命令位置在后续排序时使用新结构进行排序YMin/YMax/ZMax。
我们已经完成了主要的结构替换工作接下来将开始在实际数据流中测试并调试这些变更。 game_sort.cpp将 sort_sprite_bound 分离成 sprite_bound并让 IsInFrontOf() 接收 sprite_bound
我们正在进一步优化渲染排序系统的结构与接口设计目的是让排序相关的数据结构更加清晰、解耦并减少潜在的优化器混淆或调试性能损耗。以下是我们目前的思路和调整内容 结构设计上的优化思考
当前我们在推送渲染指令时需要提供三个核心值
YMin精灵底部位置YMax精灵顶部位置ZMax用于深度排序的最大 Z 值
之前我们使用 sort_sprite_bound 结构体来打包这三项加一个 Index但由于 Index 是在 push 渲染时才生成的而不是调用方能确定的所以我们在传参时会面临一个逻辑上的割裂。
为了解决这个问题我们引入了一个新的中间结构例如
struct sprite_bound {float YMin;float YMax;float ZMax;
};然后我们在推送函数中传入的是这个结构而不是完整的 sort_sprite_bound。在内部再由系统自动设置 Index 值构造最终的 sort_sprite_bound用于排序和渲染调度。
这种做法的好处在于
接口更简洁调用方无需知道 Index 的存在结构更清晰把用于排序的信息和控制信息分离更易优化让编译器更容易理解数据结构的只读特性。 接口重构与调用优化
由于我们现在传入的是一个简单的 sprite_bound 类型值
所有使用 IsInFrontOf() 比较函数的地方也要相应更新不再使用包含 Index 的完整结构去判断谁在前谁在后只需要在比较时传入两个 sprite_bound 即可。
例如
bool32 IsInFrontOf(sprite_bound A, sprite_bound B);传值pass-by-value而非指针或引用的形式也带来潜在好处
编译器可以明确知道这些值在函数内部不会被修改避免潜在别名(aliasing)问题有助于更好的内联优化尤其是在 Release 模式下。
虽然这可能在 Debug 模式下稍微降低性能由于结构复制但整体来看在 Release 编译中能带来更高的效率和更稳定的优化行为。 旧逻辑的迁移与简化
我们同时对旧代码中使用 sort_key 的部分进行替换或标记
将原来的 sort_key 替换为新的 sprite_bound在推送逻辑中构建最终结构并写入渲染命令缓冲区在排序前操作中完全使用新的边界数据结构。 总结
我们目前的策略是在系统中引入一个更清晰、更职责单一的结构如 sprite_bound作为排序用的传参值从而
降低使用复杂度避免不必要的冗余字段传递减少编译器优化上的不确定性提升后续排序、渲染调度的可维护性。
未来我们还可以考虑进一步封装排序规则让调用层几乎不需要理解排序细节只需关注其渲染表达的语义。 game_sort.cpp考虑传给 PushBitmap() 的正确值
我们现在已经完成了排序逻辑内部的调整使其接受新的排序键 sprite_bound并在合并和单元素比较的两个关键环节都替换为传入该结构体。这一变化使得整个排序流程本身准备就绪可以从外部正式切换过来使用新的机制。但是接下来要面对的问题是原先所有调用排序的渲染接口现在都无法正确工作因为它们仍旧传入的是旧式的“排序键”如简单的 sort key 值而我们现在需要的是 包含 YMin、YMax 和 ZMax 的完整边界信息。 外部渲染调用的问题
主要问题集中在类似 PushBitmap() 这样的接口调用上
原先调用这些接口时只传入了一个 Z 值或简化的 sort key现在排序系统需要完整的边界信息YMin/YMax/ZMax才能完成排序所以原有的调用点都“失效”了必须重新构造这些值。 推测边界信息的思路
当前我们并没有完整的边界数据可用只能根据已有信息推断比如 对象是否为“直立型”upright 这个信息我们可以从对象的 transform 结构中获得如果是直立型精灵例如人物立绘我们可以假设它们是垂直延展的Y 方向范围很小如果不是直立的而是“贴地面”的如地板、道具等那么它们会在 Y 方向上有一定的宽度。 位图的尺寸 通过 bitmap 的高度和宽度我们可以估算它在 Y 方向上的投影范围将此尺寸应用在物体位置上推算出 YMin/YMaxZMax 的推算可以保持与原有逻辑一致甚至可以直接使用对象 transform 的 Z 值。 拟定临时方案测试效果
我们准备尝试一种折中的临时处理办法
使用对象 transform 中的“upright”字段来判断 Y 方向的延展方式如果是直立型我们可能设置一个极小的 Y 范围或统一的默认值如果是平铺型我们根据 bitmap 尺寸推导出一个 YMin/YMax然后将这些值组成 sprite_bound传入新的排序系统。
这不是绝对精确的方法但足以用于尝试渲染流程是否能正常运作。我们会根据实际运行效果来判断这个方法是否值得保留或进一步细化。 总结
目前的目标是将外部渲染调用过渡到新的排序系统。面临的问题是缺乏完整的排序所需边界数据。我们提出了以下策略应对
利用 transform 中的“upright”字段来初步区分排序方式使用位图尺寸推算 Y 方向的边界值尝试这种方法后观察排序与渲染是否合理后续再逐步精细化处理边界估算逻辑。
这是从系统结构向实际数据落地的关键一步也是排序系统彻底统一的前置条件。后续会继续调整 bitmap 渲染等接口确保每个调用点都能提供正确的边界信息。
game_render_group.h 和 *.cpp从 entity_basis_p_result 中删除 SortKey
我们现在明确了一点原有的 sort key排序键系统已经彻底作废因为我们现在采用的是新的排序机制基于 sprite_bound包含 YMin、YMax、ZMax 的结构体来完成排序。因此所有依赖旧 sort key 的逻辑都可以直接删除整个代码中与 sort key 相关的部分都将被移除。 清理旧逻辑
首先清除掉
所有从 DIM Basis如 entity basis传递 sort key 的过程在 PushBitmap 或其他渲染接口中设置 sort key 的操作在排序和渲染流程中依赖 sort key 的判断或处理逻辑。
这些旧逻辑都不再有任何作用统一删除。 新逻辑的核心依赖upright 状态
新的排序方式依赖的是 sprite_bound 的三个值YMin、YMax 和 ZMax。为了正确生成这三个值我们需要知道一个关键属性 当前位图是“平面贴地”还是“垂直竖立” 这个信息由 object_transform 中的 upright 字段给出这是我们目前判断物体朝向的唯一依据。 若为 upright true表示该对象是竖直朝上的比如人物立绘、招牌等 此时我们可以忽略 Y 范围直接使用 ZMax transform.Z 若为 upright false表示该对象是铺地的比如地砖、地面物品等 这时必须设置 YMin/YMax我们将通过 bitmap 的尺寸来推算ZMax 仍然可以直接取 transform 的 Z 值。 如何将 upright 正确用于排序构建
我们目前的问题是upright 字段存在于 object_transform 中但未必在所有地方都方便读取。所以我们面临一个设计上的抉择 是否应该把 upright 提取出来作为 PushBitmap 的一个明确参数 这可以使排序判断逻辑更加明确清晰避免未来 object_transform 内部结构变动带来的耦合问题。
虽然我们还没做最终决定但可以明确一点**无论放在哪里upright 的信息是不可或缺的。**因为排序逻辑离不开它。 总结
我们完成了以下几个关键步骤和思考
旧 sort key 全面作废相关代码将被清除新的排序机制依赖 sprite_bound必须构造出 YMin/YMax/ZMax构造方式依赖物体的 upright 属性必须保证其在调用 PushBitmap 时可以获取到未来可能需要调整接口设计将 upright 明确作为参数传入而不是隐含在 transform 中。
这一步已经为我们清晰了外部调用排序逻辑的入口条件接下来的任务就是在所有调用点确保 sprite_bound 能够被正确构造。这样整个渲染系统才能正式迁移到新的排序机制上。 game_render_group.cpp让 PushBitmap() 为 Upright直立精灵设置 SpriteBound.YMin 和 .ZMax
在当前的排序逻辑重构过程中我们面临两种情况竖直物体upright 和 平铺物体lying flat。我们分别讨论了它们在构造 sprite_boundYMin、YMax、ZMax时的差异。 对于竖直物体upright
这类物体的高度决定它在 Z 轴上的可见性但在 Y 轴上并不需要参与排序。
ZMax直接取 transform 的 Z 值即可YMin/YMax可以设为任意默认值因为在排序中不使用。
这是一个理想情况计算简单不容易出错因此我们已经实现了这部分逻辑并通过测试。 对于平铺物体lying flat
问题正好相反
ZMax确定且简单直接使用 transform.Z因为这类物体“贴地”Z 值不会随着尺寸发生变化YMin/YMax困难的地方在于我们不知道这个 sprite 的 Y 向投影范围。
虽然我们大致知道这个 sprite 是围绕某个 Y 值居中渲染的但缺少精确数据不知道这个 sprite 在 Y 轴上实际“占了多少空间”。 临时解决办法
我们决定采用一种简化策略虽然它可能不准确但可以作为临时手段以验证整体流程是否可行
YMin transform.y - 某个“向后”偏移假设值YMax transform.y 某个“向前”偏移假设值
这个偏移量的计算方式依赖 sprite 的宽高信息或尺寸参数但目前我们并未做精确计算只是粗略模拟。
我们意识到
这种方式不能覆盖所有情况很可能在游戏后续运行中某些 sprite 会因为排序错误而出现穿插、遮挡等视觉问题等整体系统跑通之后我们需要回头对这部分逻辑进行精度提升可能要读取 sprite 的真实边界、处理非居中渲染、旋转缩放等复杂因素。 下一步任务
继续推进 PushBitmap 调用链中 sprite_bound 构造的适配临时使用上述方式填充 YMin/YMax等排序稳定后再评估哪些错误是由不准确的边界计算造成的再反过来优化这部分逻辑引入更精细的边界推断方法。 总结
我们为平铺物体临时设计了一种不准确但可运行的 sprite_bound 生成逻辑确保整个新排序系统能跑通一遍。虽然它会带来一些排序误差但这样可以快速验证结构正确性等验证通过后再逐步替换为更精确的算法。这是一个典型的“先跑起来再精细化”的迭代策略。 game_render_group.cpp讨论 SpriteBound
我们已经完成了构造 sprite_bound 的逻辑现在可以直接将其传递到后续的排序流程中去整个排序路径可以顺利运转了。下面是我们对逻辑的进一步整合和优化详细说明如下 初步统一逻辑处理
我们识别出一些公共部分可以简化处理流程减少重复代码提高可读性和可维护性 ZMax 值始终是 transform 的 z 分量 无论是竖直还是平铺的 spriteZMax 都是基于 transform.z 进行计算因此我们将这部分代码抽离出来统一处理。 YMin / YMax 的条件注入 如果是平铺lying flat我们会为 YMin 和 YMax 添加推导的范围如果是竖直upright则跳过 Y 轴的范围计算只用 ZMax 即可。
这种分离式处理结构更清晰逻辑也更直观。 核心计算逻辑结构
我们在代码结构上形成如下模式
// 统一处理 zmax
sprite_bound.zmax transform.offset.z;// 判断物体朝向并决定是否处理 y 范围
if (is_upright) {// 竖直物体Y 范围无关仅设置 zmax 即可// YMin/YMax 保持默认值或不处理
} else {// 平铺物体需计算 y 范围sprite_bound.ymin transform.offset.y - backward_extent;sprite_bound.ymax transform.offset.y forward_extent;
}这样的逻辑可以清晰区分不同的 sprite 类型并提供基础的排序依据。 下一步可扩展方向
虽然当前的逻辑可以支撑排序逻辑的正确性但我们也意识到有不少地方仍存在提升空间 对偏移量 forward_extent / backward_extent 的来源进行精化 目前我们是基于 sprite 尺寸估算的需要根据实际 bitmap 尺寸或 bounding box 来精确推导。 支持更复杂的变换 未来如果引入旋转、非等比缩放等情况当前的基于 transform.offset 的方式将不再足够需要矩阵乘法等更复杂的处理。 统一封装 Y/Z 范围构造 可以进一步封装为一个辅助函数根据 sprite 的朝向、尺寸等信息自动构建完整的 sprite_bound。 总结
我们完成了将 sprite_bound 构造逻辑合理地嵌入渲染流程的改造同时抽离出 ZMax 的统一计算和 YMin/YMax 的条件处理逻辑从而形成了清晰可控的结构。这为后续排序系统的准确性和性能优化打下了良好基础。后续重点在于精化边界估算逻辑使排序更加精准。 game_render_group.cpp考虑让 PushRect() 做和 PushBitmap() 相同的计算并让 Clear() 排序在所有内容下方
我们现在推进到处理 push_rect 的部分。从逻辑上来看我们判断这部分很可能可以复用与 push_bitmap 相同的排序边界计算代码。虽然目前还不确定完全适配但初步猜测是可以的所以我们决定尝试用相同的计算方式处理。 push_rect 的处理思路
我们推测 push_rect 也能沿用 push_bitmap 中的 sprite_bound 构造方法即
zmax 统一设定为 transform.offset.z根据是否“竖直”或“平铺”来决定是否添加 Y 方向上的 ymin / ymax采用和 bitmap 一样的判定方法来处理 rect 的排序逻辑
这意味着我们有机会提取出一段共享代码来统一处理所有此类“可排序可绘制元素”的边界计算。 clear 操作的排序策略
接下来处理的是 clear 操作它并不是真正的 sprite但我们依旧希望它能“出现在最底层”也就是被排到所有内容之前渲染。因此我们需要构造一个特殊的排序边界来达到这一目的。
具体做法 构造一个“虚拟”的 sprite_bound其值如下 ymin REAL32_MINymax REAL32_MAXzmax REAL32_MIN
这样的设置方式可以确保 clear 排序值永远小于其他任何内容因此会被排在最前面达到“清屏在先”的目标。
这是一个非常简单粗暴但有效的策略。 接下来目标提取共享逻辑
由于我们现在已经有多个地方如 push_bitmap、push_rect、clear都需要构造 sprite_bound所以我们下一步的目标是提取一段共享的代码逻辑来统一处理这部分计算。
大致目标如下
编写一个辅助函数比如 ComputeSpriteBound(transform, size, is_upright)统一处理 Y/Z 方向的边界计算对于 clear 操作单独使用特定的常量值不通过函数以避免冗余计算
这样我们就能避免重复书写大量的逻辑分支提高系统的整体清晰度与健壮性。 小结
push_rect 有望复用 push_bitmap 的边界构造逻辑clear 操作通过构造极端值实现最底层排序下一步目标是提取出共享的 sprite_bound 构造逻辑统一用于所有绘制操作的排序准备流程
整体来看这是一种渐进式演进思路先统一策略再逐步抽象与优化。
game_render_group.cpp引入 GetBoundFor() 并将 PushBitmap() 的功能抽取到其中
我们现在开始将构造排序边界sprite_bound的逻辑进一步抽象成一个可复用的函数以便在不同渲染路径中比如 push_bitmap 和 push_rect共享。 目标提取出 GetBoundFor 函数
我们要做的是将构造 sprite_bound 的逻辑提取出来形成一个统一的工具函数比如叫 GetBoundFor。这个函数用于根据传入的参数生成用于排序的边界信息。
所需参数分析如下 object_transform.offset_p 对象的位置是排序中 Z 值和 Y 值计算的基础。 upright 标志位 用来判断当前图像是竖立的如角色或树木还是平铺的如地面贴图这影响 Y 和 Z 的排序方式。 offset 图像的偏移量在计算边界时需要加入用于精确定位。 height 图像的高用于计算 Y 向或 Z 向的扩展边界范围。
只要这四项有了就能精确地生成 sprite_bound。 调用方式及用法示例
函数调用形式如下
sprite_bound GetBoundFor(offset_p, upright, offset, height);调用中我们传入
当前的偏移位置 offset_p是否竖立的布尔标志 upright图像偏移 offset图像高度 height
然后函数内部会判断是“平铺”还是“竖立”从而使用不同的逻辑构造 ymin/ymax/zmax。 应用到 PushRect 中
在 push_rect 的情况下只需要把 height 设置为 Y 维度大小即可。因为 push_rect 通常就是用来画二维矩形而我们系统中的 Y 是垂直方向所以它也符合之前的建模方式。
因此可以直接写成
sprite_bound GetBoundFor(transform.offset_p, upright, offset, rect_dim.y);补充说明清理旧代码
随着这个函数的引入我们可以彻底删除之前在 push_bitmap、push_rect 等路径中重复的排序边界构造代码统一通过 GetBoundFor 处理。 总结
成功抽象出 GetBoundFor 方法用于统一构建 sprite_bound函数依赖四个参数位置、是否竖立、图像偏移、高度适配了 push_bitmap 和 push_rect提升代码复用性和可维护性后续如果排序逻辑要调整也只需改动一个地方
下一步我们可以继续观察运行结果是否符合预期若发现某些排序仍不对就再调整 GetBoundFor 的具体实现逻辑即可。 运行游戏并迅速遇到大量问题
我们目前遇到的问题是虽然在构建排序键sort key时做了结构上的改变并完成了代码更新但渲染器并不知道我们已经更改了排序键的格式。这会导致运行时崩溃或产生严重渲染错误。 问题核心
渲染器依旧使用旧的 sort_key 格式去解释和处理渲染命令。而我们已经将 sort_key 替换为一个新的结构比如 sprite_bound并用它来进行深度和顺序上的排序。
但是在 render 过程中渲染器仍然
从渲染命令中读取 sort_key使用旧的结构去解析数据比如使用了旧的字段、类型转换等在 SortEntries 时将内存内容强制转换为老式结构体 下一步调整方向
我们必须同步更新渲染器中的排序逻辑与结构定义以匹配我们更新后的 sort_key 结构。
首先定位到渲染器中的排序逻辑
找到 SortEntries 相关函数查看它是如何处理每条渲染命令的重点检查它是否对 sort_key 使用了固定偏移量、结构体强转或硬编码解读方式
例如可能看到如下逻辑
sort_entry (sort_entry_type *)command-sort_key;我们需要把这一类逻辑替换成基于新结构的访问方式。
然后更新 sort_entry 结构定义
如果我们将 sort_key 替换为一个包含以下字段的新结构
struct sprite_bound {float ymin;float ymax;float zmax;
};那么渲染器中排序函数就应当改为
sprite_bound *a command_a-sort_key;
sprite_bound *b command_b-sort_key;if (a-zmax ! b-zmax)return a-zmax b-zmax;
else if (a-ymax ! b-ymax)return a-ymax b-ymax;
elsereturn a-ymin b-ymin;注意这里必须确保排序逻辑保持一致避免前后代码不对齐导致逻辑错乱。 小结
渲染器仍然使用旧格式解析排序键导致严重问题必须更新渲染器中关于 sort_key 的访问逻辑和数据结构要确保排序函数匹配我们新的排序边界结构体sprite_bound所有基于旧结构体的强制转换、偏移操作都需要移除
更新完成后渲染系统才能理解新的排序键格式整体渲染才会恢复正确排序逻辑并正常运行。
game_sort.cpp引入 GetSortEntries() 用于将 Entries 转换为 sort_sprite_bound
我们正在对渲染系统中使用的排序键结构进行结构性重构以适配新的 sprite_bound 类型。在处理过程中发现了代码中存在大量直接对排序键内存进行类型转换cast的做法这种方式在我们更新排序结构之后将变得非常危险因为一旦结构体发生改变所有这些显式转换都会默默失效导致渲染逻辑出错且很难追踪和维护。 主要问题
目前大量地方通过如下方式读取排序键数据
sort_entry (sort_entry_type *)command-sort_key;这种做法的问题在于
对排序结构的更改不具备可追踪性所有调用处都必须手动更新易错、难以维护无法形成统一的数据访问通路 ✅ 解决方案
我们将这部分逻辑提取为统一的接口函数避免在多个调用点重复手动转换结构提高可维护性与健壮性。 实施步骤如下 定义统一的访问接口函数 在渲染器公共模块中新增函数例如在 sort.cpp 中 sprite_bound* GetSortEntries(game_render_commands *commands) {return (sprite_bound *)commands-SortMemory.Base;
}此函数明确指定返回类型为 sprite_bound*并隐藏了具体的类型转换过程。 替换所有旧的直接类型转换调用 将所有类似以下的代码 sprite_bound *entries (sprite_bound *)commands-SortMemory.Base;替换为统一调用 sprite_bound *entries GetSortEntries(commands);统一在各个渲染路径中使用新接口 包括 通用渲染器路径OpenGL 渲染路径可能存在的调试或测试路径 我们已经在通用路径和 OpenGL 路径中完成替换。 构建错误传播链以验证完整性 替换之后编译器将帮助我们发现所有仍然使用旧方式访问排序键的位置借助这些错误信息我们可以确保整个代码库中排序键的访问方式全部统一。 总结 排序键结构体更新后不能再通过直接强制转换方式访问 应将所有访问封装进统一的函数接口中 函数接口具备以下优势 自动适配结构更新提高代码安全性错误定位更清晰更易维护与重构 当前已完成替换并确保各路径同步更新
随着这个结构封装完成我们的排序逻辑访问就更加稳固后续再做结构调整也会变得轻松许多。 game_render.cpp让 SortEntries() 调用 GetSortEntries()不再调用 RadixSort()改用 MergeSort()
我们还有一个地方没有正确调用新的接口那就是执行排序本身的部分。现在排序调用的仍然是旧的 sort_entries 概念而不是我们新引入的 sprite_bound。如果当初在这里就调用了正确的封装函数那么一开始编译就会报错能立刻暴露问题。这也是我们进行接口封装的意义所在确保结构发生变化时编译器能帮我们发现所有错误使用的地方。 排序部分的更新与适配 停止使用 Radix Sort 当前的排序逻辑原本依赖于 Radix Sort但由于我们新设计的 sprite_bound 结构体是复杂的浮点值结构不再是适合 Radix 的整数位字段形式因此 Radix Sort 完全不适用必须改用 Merge Sort 改用 Merge Sort 实现 在实际排序代码中已经更换为 Merge Sort并更新相关调用确保它使用的是我们新的 sprite_bound 结构 void Sort(sprite_bound *Entries, sprite_bound *Temp, uint32 Count) {// Merge sort implementation...
}为此我们需要为 Temp 分配等量空间用来辅助排序操作。 同步修正内存分配处 在渲染初始化或排序所需内存分配中原本是基于 sizeof(sort_entry) 进行内存分配 NeededMemorySize MaxSortEntryCount * sizeof(sort_entry);这必须改为 NeededMemorySize MaxSortEntryCount * sizeof(sprite_bound);否则即使逻辑正确分配的内存不够也会导致数据越界或崩溃。 将大小定义和访问统一封装 为了统一后续处理可以考虑将排序项的类型与大小统一封装为函数或宏比如 inline size_t GetSortEntrySize() {return sizeof(sprite_bound);
}inline sprite_bound* GetSortEntries(game_render_commands *Commands) {return (sprite_bound *)Commands-SortMemory.Base;
}所有关于排序项类型和大小的调用都改用这些封装确保修改结构体时可以集中修改逻辑。 总结
已完全废弃 Radix Sort改用 Merge Sort以适配复杂排序键结构所有 sort_entry 使用点替换为 sprite_bound对内存分配中的类型和大小做了同步修正排序接口调用已封装为统一函数减少未来维护成本通过编译器错误实现错误定位闭环提升可维护性与健壮性
通过这一系列的结构调整和接口统一我们的排序系统现在更加稳健、灵活能够适应不同结构体下的渲染排序需求为后续功能拓展和调试提供了良好基础。 game_render.cpp将 GetSortEntries() 从 game_sort.cpp 中提取出来并引入 GetSortTempMemorySize()
我们在这里要做的是为排序内存的大小分配增加统一的查询函数比如 GetSortMemorySize 或 GetSortTempMemorySize这样在 Win32 层的代码中就不需要手动硬编码去计算排序内存的大小而是可以通过调用这个统一的函数来获取准确的数值。这么做的原因并不复杂只是希望将相关操作的逻辑尽量聚集在一起避免因改动某一个地方而遗漏另一个地方从而导致错误。 目标
我们要实现的就是在结构层级中创建一个获取排序内存大小的接口函数使得任何地方在需要知道排序内存大小时都可以调用这个函数而不是自己计算。 做法细节 引入内存大小接口函数 比如我们定义以下函数 size_t GetSortMemorySize(uint32 ElementCount) {return ElementCount * sizeof(SpriteBound);
}或者对临时排序缓冲区也有 size_t GetSortTempMemorySize(uint32 ElementCount) {return ElementCount * sizeof(SpriteBound); // 假设一样大
}用于统一调用 在 Win32 渲染初始化代码或排序所需内存配置处就不再是 size_t NeededSize ElementCount * sizeof(SpriteBound);而是 size_t NeededSize GetSortMemorySize(ElementCount);方便未来结构变化 如果以后 SpriteBound 的结构体变化或者内存排列方式发生变动比如多一个对齐字段、加了 padding、变化为指针等我们只需修改 GetSortMemorySize() 函数内部的实现其他调用此函数的地方完全不需要修改。 整体意义
我们这样做的核心目的并不是追求复杂的功能而是追求一致性和可维护性
改动结构时自动联动通过统一的接口任何结构变化都只需修改一个地方。提高可读性调用者更容易知道这里的内存是做什么用途的例如排序不用再去猜 sizeof(...) 的意图。避免重复逻辑减少冗余代码和硬编码。 总结
我们增加了统一的排序内存大小接口意在
绑定内存结构和调用逻辑的关系提高代码维护时的连贯性避免因结构更改导致遗漏更新的潜在 bug
操作本身很简单但能极大提升项目规模变大后的可维护性是一个典型的架构优化策略。 win32_game.cpp让 WinMain() 调用 GetSortTempMemorySize()
我们接下来要做的事情是在需要排序操作的地方统一调用 GetSortTempMemorySize 来获取所需的临时排序内存大小。我们把这种逻辑统一封装起来意味着今后在任何地方需要计算排序内存时都不再手动处理而是调用标准接口保证一致性和可维护性。 实施步骤详解 统一替换内存大小计算逻辑 将所有之前用来计算排序内存大小的地方例如 size_t NeededSize ElementCount * sizeof(SpriteBound);统一改成 size_t NeededSize GetSortTempMemorySize(RenderCommands);实现 GetSortTempMemorySize 函数 该函数内部会从渲染命令中提取元素数量然后乘以排序所需的数据结构大小 size_t GetSortTempMemorySize(GameRenderCommands* Commands) {return Commands-SortEntryCount * sizeof(SortSpriteBound);
}整理冗余代码 原先散落在各处的排序内存分配代码可以删除或重构比如临时数组的空间分配、类型转换等。 影响范围覆盖
OpenGL 渲染器确保在排序前使用统一的内存大小函数。Win32 平台下的渲染逻辑内存分配阶段也使用新接口。排序实现逻辑如 MergeSortSpriteBounds调用这个函数保证输入参数合法。GameSlow 或调试渲染路径这部分如果也涉及排序内存同样适配。 可预期的进一步修改
在继续推进过程中可能会发现以下情况
有的模块根本没有走通这条路径可能是死代码或者走了其他后门逻辑有的排序函数内部并未真正使用类型安全方式访问排序内存例如强转未统一某些旧路径直接写了 sizeof(...)没有调用 GetSortTempMemorySize()需逐步替换。 最终目的
整个重构的目的并不复杂
提高排序结构和渲染流程之间的耦合性可控降低维护成本未来排序结构一旦变动比如排序字段变化、尺寸变动、类型升级只需改动一处减少手动计算、强制类型转换、复制粘贴代码等易错操作保证渲染器中所有路径主流程、OpenGL、调试路径一致性。 总结
我们现在正将排序所需内存的处理逻辑集中封装到 GetSortTempMemorySize 这类接口中逐步淘汰所有手动计算与冗余逻辑使得未来无论渲染流程怎么扩展或排序逻辑如何复杂化我们都能通过统一入口点进行维护和升级。这是一种非常有效的工程实践虽然短期内修改面略大但长远来看将极大提升代码质量与稳定性。 game_sort.cpp将 SortEntries() 标记为 TIMED_FUNCTION()
我们现在正好处于一个非常合适的测试点恰好可以验证排序逻辑的准确性特别是“是否在前方is in front of”这个判断逻辑是否在各种情况下都能成立。 当前测试目的
我们的目标是验证排序系统在所有相关对象上的“前后”逻辑是否准确也就是 确保对于所有应该在前方的对象is_in_front_of 函数都返回 true。 这类验证在 game_slow 之类的调试或慢速执行路径中尤为重要因为可以精确观察渲染顺序的错误。 执行的具体内容 测试“我的头”是否在其他元素前方 举例来说像“我的头”这个精灵应该被排在地面元素或身体之上。这就意味着对应的排序判断逻辑必须能识别出其位置在前。 验证 is_in_front_of 逻辑在各种数据上的正确性 包括 平面精灵如地面砖块立起的精灵如人物、物体特殊对象如 HUD 或 UI 元素 调试构建中的意外表现 有一个情况令人惊讶就是在 debug 模式下并没有出现期望中的报错或异常。这说明可能存在一些函数逻辑在调试构建下被优化掉了或者断言未触发。 后续行动
运行调试版本观察输出通过手动或自动方式检查哪些对象被错误排序明确是否所有路径都调用了 is_in_front_of有可能部分对象走了旧路径或未被加入排序回溯 debug 构建未报错的原因例如检查是否缺失了断言逻辑、类型转换判断、nullptr 检查等利用当前测试场景增强单元测试将当前视为基础测试用例逐步扩展场景如遮挡、嵌套对象、非标准 transform。 总结
我们正处于验证排序逻辑核心的阶段利用现有场景可以高效测试 is_in_front_of 的正确性。尤其是像“我的头在什么前面”这类逻辑可以作为非常典型的判断用例。如果这类基础排序都不能确保准确那将影响整个渲染管线的视觉正确性。通过集中测试这部分逻辑同时检查 debug 模式下的意外表现有助于我们及时发现并修复渲染排序系统的核心问题。 调试器单步进入 SortEntries() 并检查 EntryA 和 EntryB 的 SortKey 值
我们现在正深入调试排序系统的问题目标是定位为什么某些精灵在渲染顺序中出现错误。通过逐步进入排序比较函数开始具体分析两个参与排序的精灵条目的数据。 目的验证排序比较逻辑是否符合预期
我们希望检查排序时比较逻辑是否在某些情况下未按我们想要的行为执行尤其是“ZMax 值较大的精灵应被排在后面”的规则。 调试过程和观察结果 初步测试失败 最开始尝试比较两个精灵条目时发现它们完全相同YMin、YMax、ZMax都一样这类情况下排序结果无法判定优先级即排序稳定性不保证顺序——这种情况跳过处理。 加入断言以缩小排查范围 于是我们添加了判断逻辑 若两个条目的 YMin、YMax、ZMax 全相等则跳过若有差异则进入断言确认我们是否确实在处理排序顺序有误的精灵条目。 成功抓到一个排序异常样本 在某一对条目中 两个精灵都是立起的YMin、YMax 相等然而其中一个的 ZMax 为 1.25另一个为 0.75根据逻辑ZMax 0.75 的条目应排在前但实际 ZMax 1.25 的条目却被排在前这表明排序行为有误。 排序状态分析 当前比对的索引为 70表示在前 70 个条目中排序都是正确的这一条目是第一个排序错误的个例从而可知这个 bug 不是系统性全错而是局部在特定数据下触发。 关键问题分析
ZMax 作为深度优先排序参考值未被正确比较排序逻辑可能忽略了部分排序关键字或比较函数实现不符合预期排序算法本身如 merge sort没有问题但排序函数的比较逻辑存在缺陷。 后续修正方向
检查排序比较函数是否严格实现了“ZMax 小者优先”逻辑确认所有排序输入项在排序前已正确填充YMin, YMax, ZMax为边界情况如精灵条目完全相同添加稳定性保障逻辑扩展测试用例专门测试排序的不同情况同高、同位、不同位考虑调试工具中增加详细的排序比较跟踪输出便于快速发现其他潜在错误。 总结
通过断言与逐步调试我们确认排序逻辑在某些特定情况下未正确执行具体表现为精灵的 ZMax 值未被当作排序关键优先级导致视觉错误。这类排序错误可能是极偶发的但在视觉上影响较大因此必须尽快修正排序比较函数的实现确保所有参与排序的字段都被严格处理。此轮调试也说明对排序逻辑的测试覆盖需进一步加强特别是针对“排序相等”与“排序不等”的边界情况。
game_sort.cpp用 #if 0 注释掉 SortEntries() 中的 game_SLOW 代码运行游戏并无任何异常
我们当前面对的问题是在渲染约 2000 个精灵时屏幕显示为空白这意味着排序或渲染过程中存在严重错误。考虑到目前排序数据混乱、难以逐一调试所有精灵我们决定采取更简化、可控的方式来逐步排查问题。 当前目标
让排序和渲染逻辑在可控的小规模场景中运行以便我们能观察排序效果并验证排序是否按照 Z 轴深度正确进行。 调整策略与计划 放弃使用 2000 个复杂的测试精灵 当前测试场景中的精灵数量太多视觉上混乱排序验证困难同时调试复杂效率极低。 简化输入使用离散层级的数据场景 计划切换到类似“过场动画”一类的场景。这类场景通常只需要按照 Z 值进行排序精灵数量有限分层清晰易于观察排序效果。 过场动画的优势 排序只依赖 Z 值无需考虑 YMin / YMax精灵位置稳定且视觉布局清晰是理想的验证排序系统是否基础正确的起点。 下一步行动 修改当前测试入口加载一个过场动画场景检查该场景下的排序输出是否按照 Z 值正确排列确认渲染是否成功显示预期精灵顺序若显示正常则逐步引入更复杂的测试数据结构如混合排序条件若依然显示为空白则需要进一步排查渲染管线本身的问题。 小结
当前的大规模精灵排序场景过于复杂不利于调试。我们决定先采用精简、结构明确的过场动画场景作为验证排序系统正确性的切入点。在该场景中只需确保精灵按照 Z 值渲染即可验证通过后再扩展至复杂场景。这是更科学、更高效的调试思路有助于快速定位问题根源并逐步恢复渲染正常。 game.cpp让 GAME_UPDATE_AND_RENDER() 只播放过场动画
我们现在回到项目中准备切换到一个更简单、更易调试的场景——过场动画cutscene用以验证排序逻辑是否正确运行。 当前目标
强制程序在启动后直接进入过场动画模式便于我们在更少干扰、数据明确的条件下调试渲染与排序系统。 操作步骤与验证流程 查找并确认入口设置位置 当前推测修改是在某段代码里设置了启动场景为 cutscene只是记不清具体在哪个函数内。 我们重新定位代码段并确认该逻辑是否生效。 强制进入 cutscene 找到该逻辑后通过直接赋值或跳转方式程序在启动后将直接跳入过场动画模式跳过主游戏流程。 运行程序确认结果 运行游戏后观察是否如预期那样直接加载并进入 cutscene。 成功标志 若界面上正确渲染出预期的过场动画内容如角色、对话背景等说明切换成功当前环境适合我们进行排序调试。 小结
我们已经找到并确认了强制程序进入 cutscene 的代码位置并成功启动了一个只涉及离散 Z 层级的过场动画场景。接下来可以在这个简单环境下调试 sprite 排序逻辑验证渲染是否符合 Z 值排序规则。如果一切正常将为更复杂场景提供稳定的基础验证参考。 调试器中断 SortEntries()确认 IsInFrontOf() 和 MergeSort() 的方向正确
我们现在正在对合并排序merge sort与前后判断逻辑进行逐步核查与调试目标是确保在过场动画cutscene中 sprite 的 Z 轴排序是正确的。 核查“是否在前方”的判断逻辑
检查 IsInFrontOf() 函数语义是 “A 是否在 B 的前面”。对于按 Z 轴排序的情况若 A 的 ZMax 大于 B 的 ZMax说明 A 更靠近观察者应排在前方 → 返回 true。 → 逻辑正确。若是按 Y 轴排序即从上到下绘制判断方向应相反 → 此处代码也处理了这种情况。 → 方向处理无误。 检查合并排序逻辑 合并排序会递归地对数组划分两半然后合并时对两半的首项进行对比 若“第二半的首项应在第一半前方”则交换两者顺序。否则保持当前顺序。 合并阶段也判断当前读取的是哪一半 若只剩一边直接输出若两边都有数据则使用 IsInFrontOf() 决定先写入哪一边。 这些逻辑在代码中被清晰实现判断条件也符合预期。 → 合并排序整体逻辑正确。 Y 坐标绑定判断
判断两个 sprite 是否为 Y 轴排序相关的 sprite是通过 YMin 和 YMax 是否不同来确认的。若两者的 Y 范围完全重合当前逻辑认为它们不属于可以按 Z 排序的范围。目前对于严格的 Z 轴排序场景而言如过场动画这个判断应该没问题。 准备实际运行测试 当前进入排序函数后只看到了两个 sprite → 数量过少不具代表性。 稍作运行之后发现有 39 个 sprite推测大多数是 debug sprite。 为减少干扰决定 暂时关闭 debug 渲染输出以便只关注真正参与排序的游戏元素 进入 handmade 系统模块通过 GameInternal 标志来关闭 debug 输出。 小结
我们确认了“是否在前方”的判断逻辑完全正确合并排序的实现和调用逻辑也符合预期计划通过关闭 debug sprite 来进一步简化测试环境从而专注观察 Z 层排序的实际效果接下来的测试将着眼于 cutscene 中的 sprite 是否按照 Z 值正确前后排序进一步验证渲染逻辑的准确性。
调试器中断 SortEntries()检查前 8 个 Entries 的 SortKey() 值
我们当前进入了一个理想的测试环境屏幕上不再绘制其他内容只有用于排序测试的几层基本图层。这些图层用于验证渲染排序的最简单情况是否表现正确。下面是详细分析 简化测试环境已构建完成
当前只保留了八个用于测试排序的图层这是最简化的测试场景有助于排查基础逻辑问题理论上应该确保所有渲染对象仅根据 Z 值排序且顺序完全可控。 排查图层数据
所有图层的 Y 值如 yMin、yMax被设置为特定数值但尚不清楚这些值为何如此设定为了理解排序是否正常首先需要弄清楚这些 Y 值的由来虽然当前排序主要基于 Z 值但 Y 值设定仍可能影响进入哪一类比较路径例如 Z-only 排序 vs. ZY 排序路径。 排序逻辑回顾
所有渲染条目都有 Z 值意味着它们应该全部走 Z-sprite 的排序路径排序过程只会比较 ZMax 值而不会用到 YMin/YMax因此这些对象的 ZMax 值应当能完全决定其排序顺序正确行为具有最大 ZMax 值的对象应排在最前。 观察异常现象
实际排序结果可能存在异常排序表现与预期不符说明可能存在逻辑错误或者初始数据设置不合理接下来将进一步调查 Y 值为何被设置成当前值确认是否误导了排序逻辑还需要逐步检查排序比较函数是否确实只在 Z-sprite 路径中使用 Z 值。 总结当前状态
已建立纯粹的图层测试环境渲染数据简洁、可控有助于定位错误正在聚焦于 ZMax 排序路径是否完全可靠下一步需确认排序行为与设定数据是否一致并检查 Z 值是否确实主导排序。 在如此简洁的场景下仍能观察到潜在排序问题说明排序逻辑中可能存在深层次的小错误。后续将重点跟踪每个图层的排序输入值与实际绘制顺序确保二者一一对应。
这不是一个前到后的渲染器
当前我们确认了一个重要事实这个渲染器并不是一个“从前到后”front-to-back的渲染器。这一点很明显意味着它并不会自动确保屏幕上距离相机更近的物体覆盖更远的物体。 渲染排序逻辑定位
当前渲染排序机制不是按照物体距离Z 值由近到远进行渲染这种渲染方式会影响遮挡关系特别是在需要深度层级呈现的场景下可能导致前景物体被后景物体覆盖若希望渲染器具备“从前到后”特性必须手动控制渲染条目的顺序或引入深度测试机制。 检查恢复断点逻辑
准备将一项判断逻辑重新加入代码中以便观察或验证某些关键状态此处可能是用于调试或确认某些变量、行为是否如预期进行后续可能会定位到排序错误具体出现在哪一环。 当前排序机制的局限
在现有机制下排序更多是依赖于手动设定的 sort key如 Z 值、Y 值等一旦 sort key 设置不合理或比较函数存在瑕疵就可能导致渲染结果与期望不符尤其在处理具有遮挡关系的场景时这种机制的缺陷会被放大。 接下来的方向
需要进一步审查排序逻辑是否能支持类似“从前到后”的行为如果不能需要评估是否引入新的排序标准或者在特定场景手动控制顺序重新启用某些检查如断言、日志有助于暴露问题出现的位置总体目标是确保渲染顺序与视觉逻辑一致避免前景物体被错误覆盖。 通过这一步我们不仅确认了当前渲染器不具备从前到后的自动排序能力也明确了改进方向要么改进排序逻辑以适应复杂视觉层级要么在特定情况下手动干预排序以获得正确结果。
game_sort.cpp让 IsInFrontOf() 按正确方向排序
我们在检查 is_in_front_of 函数时发现尽管它本身返回的判断逻辑是正确的但使用这个判断结果的地方逻辑是反的。换句话说当前的排序行为与渲染的需求相违背。 逻辑错误分析
is_in_front_of(A, B) 返回的是 A 在 B 前面也就是 A 更靠近相机但是在当前的排序实现中当判断 A 在 B 前时A 被移动到了更靠前的位置更早绘制然而在渲染中更靠近相机的物体应当在后面绘制以遮挡背景中的物体因此这个逻辑恰好应该反过来使用 —— 如果 A 在 B 前面A 应该排在后面晚一点绘制正确的处理应该是如果 A 在 B 前面就交换 A 和 B 的顺序让 A 在 B 后绘制。 修正排序方向
所以之前的交换逻辑是错误的正确的做法是当判断出 A 在 B 前时我们应该将 A 放在 B 的后面也就是交换 A、B 的顺序原本的排序使用方式是错误的必须将其颠倒。 修正后的行为预期
修正之后排序逻辑就会变成“后绘制的在前面”即前景物体会盖住背景物体视觉上会呈现符合物理遮挡规律的画面同时逻辑也与 is_in_front_of 的语义保持一致返回 true 意味着 A 更靠近所以 A 应该排在后绘制的位置。 总结
我们发现了一个关键性的问题尽管前置判断逻辑是否在前是正确的但其应用方式和渲染需求相违背。通过调整交换顺序逻辑确保 越靠近观察者的物体被越晚绘制从而正确处理遮挡关系修复了渲染排序的根本错误。 运行游戏确认基础逻辑没有彻底混乱
目前的目标是验证基础渲染排序逻辑是否正确。以下是我们当前的分析与状态总结 当前验证结果
通过对一组 简单的 Z 平面精灵Z-flat sprites 进行测试已经确认 基础的排序逻辑是正确的我们所实现的排序可以正确地按照 Z 值来判断前后关系并按从远到近的顺序渲染确保遮挡关系正确这意味着在最基本的情形下我们使用的合并排序与判断前后关系的逻辑并没有根本性错误可以正常工作。 验证目的说明
由于整个排序流程相对复杂为了避免基础部分出现问题导致后续调试困难我们先验证了最简单场景下的正确性这是为了确认系统的底层机制没有崩坏可以为接下来更复杂情况的调试提供信心排除了“根本逻辑错误”的可能性使我们可以更专注于更高级别的 bug 排查。 后续挑战与问题
游戏实际运行中的渲染情况远比简单测试复杂排序逻辑是否足够强大以处理所有真实情况仍有待验证特别是涉及 Y Z 层混合排序、多层遮挡、交叉重叠等情况可能无法通过简单的线性排序解决当前排序逻辑是否能“完全适用于复杂游戏场景”这一点仍不确定后续需要验证当前排序策略是否本质上能解决所有精灵遮挡逻辑还是说在某些情况下必须引入拓扑排序或其他更复杂的方法。 下一步计划
回到真实游戏场景中进行调试观察在复杂精灵组合下的渲染是否仍然正确定位当前存在的问题确定是否有更高阶的排序需求例如无法用单一比较函数处理的排序冲突继续改进逻辑或调整场景数据以适应现有排序机制。 总结
我们已确认基础排序逻辑在简单 Z 平面精灵上是正确的这为后续复杂场景调试打下了基础。下一步将转向游戏主流程中的实际排序逻辑识别剩余的渲染 bug同时思考是否需要更复杂的排序模型来支撑整个系统。我们准备继续深入。 game.cpp让 GAME_UPDATE_AND_RENDER() 直接进入游戏运行后确认线性扫描中没有明显失败
目前已将渲染流程切换回主游戏场景跳过了片头序列进入游戏后进行了初步观察和验证。以下是详细总结 当前观察结果 在当前游戏场景中进行 线性遍历检查linear sweep 时没有触发排序失败的断言这说明 在大多数情况下排序顺序看起来是有效的排序算法本身并没有在最表层出现明显错误基本排序机制在真实游戏场景中的表现尚可未立即暴露崩溃或逻辑异常。 当前存在的问题 屏幕上仍然存在大量 物体相互穿透interpenetrating objects 的情况 这导致排序测试不够明确因为物体之间的遮挡关系不清晰穿透情况削弱了我们观察排序正确性的能力使调试复杂化 当前测试场景的精灵排列较为混乱不具备良好的测试条件 缺乏有层次、清晰遮挡关系的对象难以精准验证深度排序逻辑。 下一步改进方向 明日的工作重点将转向为场景补充更真实的层次结构layering具体包括 明确设置对象的 Y、Z 轴位置避免模糊重叠构造更合理、分层清晰的测试对象改进原有的“图层系统”layer system提供可控的精灵分布 此外还将继续优化排序测试使其能更好地暴露潜在问题。 总结
当前初步验证表明在大场景中排序系统没有表现出崩溃或明显错误基础逻辑可用。然而由于场景中存在大量对象穿透导致排序正确性难以确认。接下来将重点重构测试用例增加更真实有效的分层精灵布局以便更清楚地验证渲染排序机制的有效性和鲁棒性。
game_entity.cpp让 UpdateAndRenderEntities() 绘制实体时按 Z 值排序不将它们压平到平面上
为了讨论方便假设暂时不做之前对实体进行的截断处理也就是说不将所有东西压平到同一层级而是直接在“世界模式”下输出实体并使用实体自身的真实Z值进行绘制而不是进行相对层级变换。
具体来说
目前渲染流程中有一个“相对层级变换”步骤会对实体的层级和深度进行调整目的是统一处理和简化排序。现在假设不做这一步直接用实体本身的Z值绘制。这样做的目的是观察在不做层级变换的情况下排序机制会产生什么效果看看能否更准确或有何不同。实际代码中这部分被移动到了单独的实体文件中需要注意这一点。通过这种方式可以验证排序系统对于真实Z值的处理是否合理以及观察排序结果是否符合预期。
简而言之就是尝试关闭原本为了简化排序而做的层级扁平化处理直接用实体的原始Z值输出来测试排序逻辑在这种情况下的表现。 运行游戏发现排序结果不符合预期
在这种情况下排序结果看起来不正确没有达到预期的效果。具体表现为观察到的一些对象的位置和排序顺序与预期不符说明排序逻辑可能存在问题。
另外因为开启了“游戏内部调试模式”导致鼠标光标被关闭为了更方便观察当前画面情况暂时将鼠标光标重新打开。这样可以更直观地查看对象的位置和排序情况帮助进一步调试和分析排序问题。
总结就是直接用实体真实Z值绘制时排序没有按预期工作需要检查排序算法和相关逻辑。同时为了更好观察先恢复鼠标光标显示。
问答环节
阅读这篇博客让我觉得非常有趣尤其是关于2D游戏中如何正确排序精灵显示的问题。虽然我平时不太做2D游戏但对这个问题一直不是很了解也没意识到它其实是个很有语义复杂度的问题——毕竟这不是完全的3D而是2D画面中通过某种方式模拟深度感。如何让精灵正确地表现“前后”关系其实远比单纯按Y轴排序复杂。
博客介绍了一个很棒的解决方案我觉得非常酷也让我对不同游戏中处理排序的方式产生了浓厚兴趣。因为肯定有很多游戏在这方面做过有趣的尝试和创新可能有的做法我以前完全不知道。现在知道这不是一件简单的事情而是一个需要精心设计的系统我非常想了解更多其他游戏是怎么解决排序问题的。
总体来说我很喜欢这篇博客里的思路和方法也期待玩这个游戏因为我已经很久没玩过类似“热血双截龙 Double Dragon”风格的游戏了感觉很怀念那种体验。
最后关于坐标的使用问题也让我有了更多思考。
排序规则使用的是世界坐标吧有没有可能用屏幕空间坐标从屏幕顶端往下排序
排序规则是基于世界空间坐标的另一种方法可能是根据屏幕空间坐标从屏幕顶部到底部进行排序。之前我们一直是用屏幕坐标来排序但这样做的问题在于排序屏幕坐标无法处理某些情况因为屏幕坐标本身并不包含物体之间的深度关系信息。换句话说仅仅依赖屏幕坐标排序无法准确判断哪个物体应该被绘制在前面或后面特别是在有重叠或层次关系复杂的情况下这就导致排序结果不符合预期。因此单纯通过屏幕空间坐标排序并不能满足需求需要结合世界空间坐标的信息来进行更合理的排序处理。
Blackboard讨论为什么不能仅用屏幕空间坐标而不考虑 Z 进行排序
问题在于如果在绘制三维场景时比如有一个平台瓦片和一个放在平台上的物体从顶视图来看这些物体在屏幕上的投影可能会重叠但仅凭屏幕上的二维坐标无法判断哪个物体实际是在前面。举例来说假设有两个物体A和BB应该先绘制A后绘制这样A会遮挡B但单纯用屏幕坐标看不出来哪个先绘制。
因此至少需要在屏幕坐标的基础上加入Z轴深度信息也就是说排序时不能只用XY屏幕坐标还要用Z坐标。但即便如此问题依旧存在。比如有些物体在视角下会重叠但它们的Y坐标大小关系并不能直接决定绘制顺序。举个例子一个物体放在四个瓦片上面人站在物体上这时如果只用Y坐标排序会出现顺序错误因为人可能在Y坐标上小于某些瓦片但实际应该绘制在瓦片前面。
所以不能简单用Y值或Z值单独排序而是需要使用物体在Y轴上的范围y-min和y-max以及Z轴信息结合起来判断。只有知道每个物体的Y轴上下界才能正确判断它们的遮挡关系解决排序的“平局”问题。仅凭屏幕坐标没有Z信息的排序根本不够因为这没法体现物体间真实的空间关系。
综上要正确处理2D画面中的深度关系必须用物体在空间中的范围尤其是Y轴的最小最大值和Z轴信息综合考虑而不是简单排序Y或者Z坐标。这个结论是通过反复推敲和测试得到的单用屏幕空间坐标无法解决实际问题。
我有个关于开发方法的问题我没见过你单独写程序或环境测试东西总是在运行中的游戏里做你觉得这样“嘈杂”吗
关于开发方式通常根据具体情况而定。大多数时候并不会专门创建独立的程序或环境来测试功能而是直接在运行中的游戏环境里进行开发和调试。只有在处理纯理论算法或者需要专门测试某些算法时才会单独搭建一个测试环境。
曾经有过类似的经历比如之前做3D算法时写过一个叫做“math viz”的小工具用来快速测试和可视化3D算法的效果。这个工具类似一个小型的实验平台可以方便地绘制和试验各种算法。它曾经存在于以前的开发工具源码树里但现在已经找不到了可能是机器清理文件时被删掉了。
虽然独立测试环境在理论上很有用但实际操作中往往会因为切换环境而降低效率所以通常直接在游戏内部调试更方便。不过当预计会在某个算法上花很多时间时会花心思制作类似的工具箱方便快速原型开发和测试。
总体来说制作单独的测试程序是一个有用但不常用的手段更多时候依赖于游戏内部的调试和开发流程。过去的这些实验工具是内部开发用的不是随SDK发布的公共资源现在基本已经丢失或无法找到。
好吧我说谎了Witness Wednesdays 里见过你做过类似操作
在开发过程中虽然尽量在直播或录制中真实展现实际的游戏开发过程不事先准备解决方案遇到问题时现场思考和调试但也有些复杂的算法和工作是不会在公开环境中详细演示的。
例如曾经在一个复杂的算法像是曲线拟合算法上花费了大量时间可能达到200小时以上这种级别的工作几乎相当于整个项目的大部分时间。这样的深度研究和长时间调试公开展示是不现实的因为会非常枯燥且难以让观众持续关注甚至大部分时间都在沉默思考。
因此公开的开发内容更多是中等难度或者常规的程序设计而超难问题的研究性工作基本不会在公开内容中出现。公开内容的目的是尽量真实地表现游戏开发的常态展现现场调试和思考的过程但不会展示那些耗时长且枯燥的算法攻关。
总的来说虽然有时候会做一些新算法和研究工作但大多数难题和复杂算法的攻关不会在公开场合展示因为这些内容既难以观看也不适合直播或系列内容的节奏这也是制作公开开发内容时必须面对的现实限制。
数学可视化演示
我们曾经使用一个叫做Math Vis的程序作为开发工具主要用于开发Granny引擎里的各种算法。这个程序包含了一个比较原始但实用的用户界面虽然不像现代即时模式UI那么先进但它有一些简单的拖拽滑块控件可以快速调整参数方便调试和测试算法。
Math Vis允许我们插入各种可视化模块大概有十多个能够同时编译进程序里根据需求点击切换不同的可视化界面。它支持画箭头、操作移动控制器等功能类似于游戏开发中调试时常用的工具。通过这些工具可以直观地看到算法的执行过程比如GJK碰撞检测算法的具体步骤。
在GJK算法的调试中我们用这个工具显示了不同点构成的简单形状simplex标注并动态调整点的位置验证算法在不同情况下是否正确识别碰撞区域和计算结果。通过拖动数值可以实时观察算法对点的选择和转换是否合理确保逻辑的准确性。
另外Math Vis还实现了一些空间划分和剔除culling功能的可视化用来测试空间数据结构的效果。比如有一部分是轴对齐包围盒AABB的显示和交集测试涉及Minkowski差分的计算这些都是用于碰撞检测的重要数学工具。通过颜色和图形表示能够帮助判断是否存在重叠或包含关系。
该程序支持各种可调参数通过滑块调整显示内容比如是否显示网格调整视角等方便根据需要观察不同的细节。虽然有些参数具体作用不太清楚但整体设计非常灵活支持快速迭代和调试。
Math Vis不仅仅用于GJK算法也用于曲线拟合、基于姿态的信息处理等多个复杂算法的原型制作和测试是一个综合性的数学和算法开发平台。在开发过程中任何非简单的算法都会先在这个测试环境里做原型确保逻辑正确和性能可行然后再集成到游戏或引擎中。
总之Math Vis是一个非常重要的工具通过简单直观的可视化界面帮助我们理解和调试复杂算法提高开发效率确保核心技术的可靠性。