回顾并为今天的内容做准备
我们正在进行游戏开发中的精灵(sprite)排序工作,虽然目前的实现已经有了一些改进,情况也在逐步好转,我们已经实现了一个图结构的排序算法,用来处理精灵渲染顺序的问题。然而,这一部分还远未达到理想的状态,当前的排序仍存在明显的问题,排序顺序不总是正确的,显然还有很大的优化空间。
在前一次的工作中,我们还临时加入了一个用于检测图中是否存在循环的代码块,这段代码是在上一次直播结束前匆忙写入的。我们并没有对其进行任何调试,因此现在根本不确定它是否真的有效、是否按预期工作、或者是否存在错误。
实际上,我们只是简单地把它写进去了,然后运行了一下程序,程序没有崩溃,所以至少它不是完全不可运行的垃圾代码,但除此之外我们几乎一无所知。我们也没有验证是否能正确进入这段逻辑或检查其状态。
运行游戏,展示我们上次留下的进度
我们可以看到目前的开发状态,也就是我们上次留下的进度。现在可以看到,在界面中我们在上面叠加了第二个房间。为了方便调试,有一件事情我们可能想要做的,就是给那些方形区域绘制边缘线。
在 OpenGL 的渲染过程中,我们之前没有把这些边缘绘制加入排序逻辑中,这样可以暂时不去担心它们对排序的影响。但由于当前绘制出来的方块颜色相同,叠在一起后很难区分它们,因此从调试角度来看,给这些方块加上边框会更清晰直观。
所以我们考虑在默认状态下,临时性地为那些实心矩形添加描边。之后如果不需要,也可以很容易地将其移除。
这个改动在 OpenGL 的渲染流程中非常容易实现。在渲染代码中,当检测到渲染对象是一个矩形时,我们可以在原有基础上为它添加一层轮廓线,这样就能清楚地看出哪些矩形是堆叠在一起的,也更便于我们识别当前图形结构是否合理。这个处理主要是为了临时可视化目的,不会影响到最终的逻辑结构。
修改 game_opengl.cpp
:让 OpenGLRenderCommands()
给渲染项绘制黑色边框
我们打算临时为矩形添加描边,以便在调试过程中更容易分辨重叠的图形。由于我们使用 OpenGL 进行渲染,理论上只要调用之前已有的 OpenGLLineVertices
方法,就可以轻松实现这一目标。这个方法接受矩形作为输入,并输出用于绘制的线条顶点,我们打算用这个功能来绘制矩形的边框,颜色暂时设为黑色。
我们尝试在渲染矩形之后立即绘制线条,从逻辑上讲,这样应该不会受到深度排序的影响,因为线条会在矩形之后被绘制,所以边框应该总是可见的。然而,实际运行时却看不到任何线条出现,这不符合预期。
我们检查了绘制流程,发现用于绘制线条的那部分是通过 glBegin(GL_LINES)
和 glEnd()
包围的,按理说这样写是没问题的,因为我们在其他调试工具中,比如绘制调试边框时,这套绘图流程是可以正常工作的。
我们开始排查可能的问题点:
- 检查是否禁用了纹理映射,确认了纹理功能已关闭;
- 检查设置颜色是否正确,使用的是
(0, 0, 0, 1)
表示黑色全不透明; - 检查是否顶点数据传递错误,验证了
OpenGLRectangle
的实现,发现没有异常,理论上应该是可以画出线条的; - 于是我们尝试手动绘制一条对角线,看是否能显示出来,但依然没有效果。
这个时候我们意识到可能是 OpenGL 的固定功能管线的问题。因为我们是在 glBegin
和 glEnd
之外设置颜色的,而这在固定功能管线中可能不被允许,即颜色设置并不会“粘住”(不具有持久性),所以必须在 glBegin
和 glEnd
之间设置颜色。
一旦我们将颜色设置移到 glBegin
和 glEnd
之间,就可以看到线条成功绘制出来了。这说明在固定功能管线下,颜色设置确实需要在绘图状态激活(即在 glBegin
之后)时进行,否则不会生效。
这个问题看似微不足道,却是固定功能管线使用过程中的一个易错点,提醒我们在使用老式 OpenGL 的时候,需要格外注意这些细节。总之,经过这番排查,我们找到了问题的根源,并确认了解决方案,下一步可以着手修复并应用到实际的矩形描边逻辑中。
如果把图显示打开会覆盖上面的黑色
运行游戏,观察这些轮廓线显示的信息
我们已经为场景中的矩形添加了边框描绘功能,现在可以更清楚地看到各个图形之间的排序关系。通过这些轮廓线,我们观察到一些有趣的现象,并开始分析当前的排序算法是否存在问题。
初步观察显示,从 Z 轴(即深度)进行排序的精灵之间的排序表现看起来是正确的。例如我们在下层房间时,可以看到上方的房间与其物体被正确排序。当我们移动到上层房间,所绘制的那些小矩形板(作为测试用的对象)也能正确随位置移动并显示,说明 Z 轴排序本身在相同维度下是合理的。
但当引入 Y 轴排序的精灵(Y-sprites)时,就会出现明显的排序错误,尤其是在 Y 精灵与 Z 精灵交错出现的情况下。这说明我们当前的排序算法在某些情况下表现是合理的,但在混合不同排序维度的情况下(Z 与 Y 混合),可能存在逻辑问题。
这引导我们思考:或许排序的整体算法结构是没有问题的,只是具体的比较函数存在缺陷,也就是说,可能是排序时的判断逻辑出了问题,而不是整个排序流程错了。这为我们接下来定位 bug 的方向提供了一个思路。
不过在深入修复排序之前,我们更关心的,是我们在之前临时添加的“循环检测”功能是否真正生效。因为排序图中若出现环(cycle),会使拓扑排序失败,从而引发显示错误。
目前我们并不确定循环检测功能是否有效,代码是临时在上一次开发会话结束前匆匆写下的,还没有经过调试,也没有验证算法是否正确。因此,我们打算暂时先不深入排序细节,而是优先检查循环检测的代码逻辑是否合理、是否能捕捉到图中的循环。
为了确认循环检测是否正常工作,我们可以手动构造一个确定存在环的测试案例,然后观察检测是否能够正确识别这一点。但在那之前,我们计划先仔细检查目前的实现代码,因为当时写得非常匆忙,甚至都不记得使用的具体算法是否正确,因此有必要认真过一遍这段逻辑,确认它的基础正确性。这样我们才能保证后续调试是建立在一个可靠基础之上的。
查看 game_opengl.cpp
和 game_render.cpp
中的碰撞组绘制流程,分析可能存在的问题
我们目前正在排查排序图中是否存在环(cycle),并验证我们的循环检测机制是否生效。为了帮助我们可视化这些循环,我们在调试逻辑中设定了一个规则:遍历所有边界信息(bounds),如果某个边界在排序过程中被检测到存在循环(即设置了 SpriteCycle
标志位),就会被绘制出来。否则,就不会绘制。
现在的问题是,我们在屏幕上没有看到任何代表循环的调试图形。这有两种可能:
- 可能场景中确实没有任何循环产生;
- 更可能的,是我们并没有正确设置这个
SpriteCycle
标志,或者我们在递归检测算法中存在逻辑漏洞,导致循环没有被识别出来。
为此,我们需要回顾并验证我们实现的循环检测算法。
在当前的代码结构中,每次对精灵图进行遍历时,会初始化一个本地变量 HitCycle
,它的作用是在本次递归过程中记录是否发现了循环。这个变量不是全局的,而是属于每一次图遍历过程的局部状态。
每当我们开始遍历一个节点(即一个精灵或边界信息)时,会将这个变量初始化为 false
。在递归遍历图的过程中,如果我们在路径中再次遇到一个已在当前路径中出现的节点(即形成了一个回路),我们就会将这个变量设置为 true
,表示存在循环。
这个变量会在每一个递归调用之间传递,因此一旦在某一层级中检测到循环,整个遍历过程都会被告知这一信息,并最终在遍历结束时用于设置是否应该绘制该节点的调试信息。
我们的当前任务是检查这部分代码是否实现正确:
- 遍历逻辑是否能够完整访问图中的所有节点和边;
- 状态变量是否能正确传递并反映循环情况;
- 一旦检测到循环,是否确实将相关节点打上了
SpriteCycle
标志; - 绘制逻辑是否只针对打了标志的节点进行可视化。
如果以上任一环节出错,都可能导致我们现在无法看到任何调试图形。接下来的步骤是细致地检查递归逻辑是否准确,特别是循环检测路径是否能正确维护“当前访问路径”这一状态,用于判断重复访问是否构成循环。只有验证这些机制没有问题后,我们才能确信当前场景中确实不存在循环,而不是因为检测失效导致“看不到”。
在 game_render.cpp
中将 RecursiveFromToBack()
重命名为 RecursiveFrontToBack()
我们在检查排序图的循环检测逻辑时,发现了一处可能是笔误的地方:递归函数的名字似乎被写错了。本应为 RecursiveFrontToBack
的函数,可能在某处被误写成了 RecursiveBack
或类似名称,而一直没有被注意到。
目前我们回溯地确认了:排序过程中,确实应该是从前向后递归(front-to-back),以确保正确建立依赖关系。如果函数名称写错,可能会误导后续对递归流程的理解,甚至可能导致逻辑调用错误。
在分析过程中,我们确认这个递归函数的核心逻辑是这样的:
- 进入一个节点(即一个精灵或图中的元素)之前,先判断它是否已经被访问过;
- 如果已经访问过,就直接返回,不再递归处理;
- 如果没有访问过,就标记为已访问,然后递归处理其所有依赖节点;
- 在整个过程中,如果再次访问到当前递归路径中已经存在的节点,就说明形成了一个“回路”,此时设置循环标志(如
HitCycle = true
)。
这个“是否访问过”的判断逻辑,是防止死循环的关键——只有未访问过的节点才会继续深入递归。否则,如果不断重复访问,程序将陷入无限递归。
我们需要进一步确认的是:
- 函数的命名是否在所有地方保持一致;
- 是否真的调用了正确的递归函数;
visited
状态是否正确地标记了所有已经遍历过的节点;- 当检测到回路时,是否真的有正确设置标志,并反馈到上层逻辑中用于调试显示。
总的来说,这段逻辑在技术上看似合理,但由于存在函数命名不一致的问题,有可能引发调用错误或逻辑偏差。因此,必须彻底检查函数调用路径和变量命名是否统一一致,避免因笔误导致检测流程失效。
修改 RecursiveFrontToBack()
的逻辑,确保设置和检查顺序正确
在排查循环检测算法时,发现了一个关键性的逻辑错误,属于实现过程中的一个笔误,但影响了整个循环检测功能的正确执行。
具体问题在于:递归遍历图中节点时,对于已经“访问过”的节点,原本的代码是直接跳过不再处理的。然而这是不对的。因为循环的本质就是“当前访问路径中再次遇到了已访问的节点”,所以“已经访问”恰恰是检测到循环的核心依据。
因此,正确的逻辑应为:
先判断是否已经设置了循环标志(Cycle Flag)。
如果当前节点的循环标志已被设置,说明已经检测到循环,应立即设置全局的HitCycle = true
。然后再判断是否访问过(Visited):
- 如果访问过并且未标记循环,说明这条路径走过一次,没问题,直接返回;
- 如果未访问过,才进行递归遍历。
遍历时要设置访问状态并加入当前递归路径标记(在调用栈上设置),以便后续检测回路。
遍历完成后,如果没有检测到循环,要清除当前节点的循环状态标志(从调用栈中移除)。
也就是说,之前的逻辑因为“先判断是否访问过再退出”而跳过了循环判断的关键步骤,导致所有可能存在的循环都没有被检测出来,HitCycle
也始终为 false
,调试信息自然也没有任何反馈显示。
修复方式就是:
- 把“是否设置过循环标志”的检查提前,在判断访问过之前进行;
- 保证即使某节点已经被访问过,只要存在回路标记,就能立即识别并正确反馈为“检测到循环”。
这样才能让整个图遍历和循环检测逻辑真正发挥作用,确保排序算法不会在图中存在依赖死锁或者图结构错误的情况下继续执行。这个修复是至关重要的一步,会让我们后续在调试排序错误时能够看到循环结构的可视化信息。
运行游戏,查看排序图中的循环并分析它们
在修复了循环检测逻辑的关键bug后,我们成功确认了确实存在一些拓扑排序中的循环依赖问题。每次屏幕上有元素高亮,实际上就是提示我们在那个区域内检测到了一个循环。
接下来对当前现象和结果进行了分析,总结如下:
已修复的问题:
我们原先在递归中错误地提前判断了“是否访问过”,从而阻止了对循环的检测。这一逻辑已被修正,现在循环检测能够正常运行,并能在存在循环时提供可视化反馈。
当前现象观察与分析:
部分区域出现大量循环提示:
- 可以看到当某些区域有大量重叠的图形(如物体、地板等)时,这些元素会整体被“拉入”循环中,提示大量元素处于循环关系内。
- 这种现象可能出现在多个元素互相遮挡、交错排列,导致依赖关系构成了一个环。
某些区域明明也很复杂却未检测到循环:
- 例如在视图中某处中央区域,逻辑上看起来应该也会产生循环,但检测结果显示没有任何循环,显得不太合理。
- 这引发了怀疑:是否还有其他潜在的逻辑错误尚未修复?
可能存在的两种解释:
- 逻辑正确但依赖结构恰好不成环:有可能某些组合的排列顺序恰好没有产生循环,即虽然看起来重叠很多,但排序顺序能理顺,故不会触发循环提示。
- 还有bug未修复:也有可能是循环检测逻辑仍存在漏洞,比如图结构构建不完整、排序依赖未完全加入遍历过程等,导致某些应当检测到的循环被漏掉了。
进一步方向与推论:
- 当前结果更倾向于**“仍然存在潜在bug”**的可能性,尤其是同样复杂度的区域有的触发循环提示、有的没有,几率上显得不太合理。
- 推测可能是图结构在生成过程中,部分节点或边未被正确加入,导致遍历不完整。
- 另一种可能是遍历顺序、依赖标记、甚至是“是否参与排序”的判断条件出现了遗漏,导致某些元素被“排除在循环检测之外”。
当前成果总结:
- 循环检测机制已经成功激活,并能对多数明显的循环结构进行可视化提示;
- 修复了关键逻辑漏洞后,系统具备了基本的错误反馈能力;
- 发现新的疑点与异常分布,有助于后续深入排查排序依赖结构中的隐藏问题。
现在的状态是:循环检测工具可以工作了,但结果显示出一些新问题。下一步建议深入排查图的构建与遍历路径,确认是否存在节点遗漏或排序规则异常。
在 game_world_mode.cpp
中修改 AddStandardRoom()
,只生成一个房间,运行游戏观察循环的表现
我们决定先回到一个更简单的场景来观察循环的问题。当前的测试环境中之前设置了两个堆叠的房间,是为了测试排序逻辑而搭建的临时结构。现在我们简化场景,只保留一个房间,目的是更清晰地了解循环的触发条件。
简化测试场景后的发现:
单个房间下初始无循环:
- 仅有一个房间时,初始状态下未检测到任何循环,说明在简单场景下排序图结构并不会自动产生问题。
跳跃动作引发循环:
- 当进行跳跃动作时,在两个物体之间的位置上突然检测到了循环。并且这个循环在逻辑上不太容易理解,看起来有些诡异。
- 特别的是,这个循环的影响范围极大,会将屏幕上几乎所有对象都“卷入”循环中。
测试进一步验证:
- 嘗試减慢模拟速度,使画面进入“慢动作”,以便更容易观察到循环的出现时机。
- 确认是在角色跳跃过某一特定位置时,才突然引发了循环。
异常与怀疑点:
循环出现的不合理性:
- 看起来只有在跳跃过程中,处于某个特定位置时才会产生循环,而其它类似情况却不会。
- 按照逻辑推理,这种现象不太符合预期,似乎在某种特定情况下触发了极大的循环关系。
循环范围异常广泛:
- 循环并非局部产生,而是影响到了屏幕上所有元素,这不太可能是正常的排序依赖结构,极可能是某种逻辑错误或数据结构问题。
关于暂停调试工具的不足:
- 当前系统的暂停功能无法在暂停状态下显示上一次的渲染帧,这使得调试变得更加困难。
- 合理的做法应是,当暂停时,仍然保留上一个渲染列表并用于当前帧显示,这样就可以静态分析画面。
目前的思路总结:
已简化场景,确认问题确实与复杂场景无关,而是出现在某些动态行为过程中;
循环触发机制存在异常,看似只在特定状态或位置才会激活;
目前怀疑:
- 有可能图结构构建时引入了错误的依赖;
- 某些对象状态更新中出现了不合理的排序逻辑;
- 有可能某些对象在跳跃中错误地建立了双向依赖,导致形成闭环。
接下来的目标是进一步深入分析这类在跳跃中触发的异常循环行为,尝试验证图结构构建是否稳定,是否有状态变化过程中不应存在的依赖边被加入,从而引发整个排序图的崩塌。
修改 game_opengl.cpp
:让 OpenGLRenderCommands()
绘制碰撞组时默认颜色较浅,若存在循环则提高透明度
我们目前仍在探索如何更好地可视化排序图中的循环问题。当前的调试图形虽然已经展示了一些信息,但直观性不强,因此我们决定对可视化手段做进一步改进。
可视化策略调整:
不确定当前绘制内容是否合适:
- 目前用于调试的图形内容并不一定是最有效的,虽然暂时还没有更好的替代方案,但我们意识到需要更直观的方式来表达“哪里有循环”。
准备引入颜色和透明度变化来突出循环区域:
- 初步计划是将整体图形用非常淡的颜色表示;
- 一旦某个区域存在循环,就会显著提高该区域的透明度(alpha)值,让该区域变得更加明显;
- 这样可以更清楚地区分出循环的影响范围。
预乘Alpha的技术问题:
- 当前渲染管线使用的是预乘Alpha模式,这意味着颜色值在混合前已经乘上了Alpha值;
- 因此,为了让提高Alpha的操作生效,需要在绘制完成后重新进行乘法操作,确保视觉上呈现出应有的亮度变化。
总结阶段目标:
- 利用颜色和透明度的动态变化,更直观地表示哪些区域被循环结构影响;
- 克服预乘Alpha带来的技术限制,确保可视化效果正常;
- 在缺乏更好调试思路的情况下,先从视觉角度加强问题感知,为后续逻辑分析打好基础。
这一步虽然主要是辅助性工作,但它能显著提升我们对当前复杂问题的认知效率。下一阶段可能会结合这些可视化结果,进一步排查排序图中的循环形成原因。
运行游戏并查看循环的表现
目前我们正在利用可视化手段观察排序系统中的循环(cycle)现象,但所见结果令人有些困惑,因此进行了进一步的确认与分析。
当前现象分析:
可视化显示外部区域存在循环:
- 从可视化结果来看,外围区域出现了循环;
- 而内部区域则没有显示出明显的循环;
- 当前图形以“淡色”呈现正常状态,而当检测到循环时,对应区域会变亮或突出显示。
受到编辑器或调试工具选区的干扰:
- 注意到有些可视化变化其实并非排序系统导致,而是由于**选择工具(env selection)**的显示效果;
- 这导致最初的观察有一定的误判。
当前分组与循环状态:
当前存在多个排序组(sort groups),但这些组暂时都表现正常,即并未发生循环;
在我们角色跳上某个平台时,会触发一个显著的视觉“闪烁”或颜色变化现象;
- 通常这个变化只影响角色自己;
- 但在某些位置(如特定跳跃节点),整个排序图都被“污染”成了循环状态,所有对象看起来都被牵连。
问题判断与推测:
- 从现象上看,这种“偶发性的全局循环触发”似乎并不合理;
- 按理说只有局部发生几何重叠时才应触发有限循环,而不是牵连整个图;
- 所以当前我们仍然怀疑存在某种隐藏的逻辑或代码缺陷,导致排序图在特定条件下被误判为整体循环。
阶段性结论:
- 当前排序图在正常情况下运作良好;
- 某些跳跃或状态切换引发的非局部大范围循环可能是异常行为;
- 可视化手段正在有效帮助识别这些潜在问题;
- 下一步需要针对特定“全局循环触发点”进一步分析其几何、排序依赖或数据结构状态,找出异常的根源。
这个阶段我们正逐步剥离调试干扰,并逐渐锁定排序逻辑中的深层问题。
思考我们是如何进行排序的
我们突然想到一个问题,关于我们目前在做的排序系统的根本思路——也就是我们最初对于精灵(sprites)是如何分类和处理的方式。这个思考引发了对整个系统设计是否存在不必要复杂度的质疑。
问题的核心:
我们开始反思:
是否我们从一开始就过度复杂化了排序问题?
我们原本的设想是:
精灵分为两类:
- 竖直立起的精灵(upright sprites),比如角色、树木;
- 贴地的平面精灵(flat sprites),比如地板、阴影。
正是这种划分导致我们实现了不同的排序逻辑和特殊规则,进而导致了许多依赖判断、碰撞排序、循环检测等机制。
新的思路:
我们提出一种简化假设:
如果我们 把所有的精灵都视为“切片状的 Z 层级精灵(z-sliced sprites)” 会怎样?
也就是说:
- 每个精灵根据其在 Z 方向(深度)上的位置被分为几个切片;
- 地面一个 Z 值,身体一个 Z 值,头部一个 Z 值;
- 然后 不再有“按 Y 值排序”的特殊逻辑,而是所有精灵都只依据 Z 值堆叠排序;
- 所有图层都只是简单的“从底到顶的绘制堆叠”。
我们自问:如果这么做,是否一切就能自然工作?
可能的结果与反思:
- 如果这个简化模型成立,那我们之前实现的大量复杂排序逻辑(包括判断精灵是否“站在地上”或“被另一个挡住”)就都变得冗余了;
- 我们甚至承认:如果这种方式真的可行,会有一种“白忙一场”的愚蠢感。
不过,我们又冷静下来:
- 即便在这个 Z 切片模型下,Y 值堆叠问题依然存在;
- 就算精灵被视为具有深度的堆叠切片,若两个精灵在屏幕空间 Y 方向有交叉且 Z 值相近,排序冲突依然无法避免;
- 曾经我们做过类似的测试,尝试将排序方式改为基于 Z,而不考虑 Y,结果也发现无法解决所有的遮挡问题。
深层理解:
我们进一步反思,也许我们之前的判断失误,部分来自于视觉认知差异:
- 屏幕显示给人的透视效果比实际上的投影更“夸张”;
- 也就是说我们以为需要复杂排序,是因为我们过度理解了视觉上的层级。
但说到底,即使有一些误判,目前的逻辑依然在处理某些复杂场景时显得必要。
总结:
- 我们试图简化排序问题,通过纯粹的 Z 值堆叠方式去替代现在复杂的 upright/flat 分类;
- 虽然这个方向看似有吸引力,但从之前的实际测试来看,它可能无法涵盖所有视觉遮挡逻辑;
- 当前的结构虽然复杂,但它是在特定视觉需求与玩法设定下被逐步逼出来的;
- 然而,这一思路值得我们后续再次以更科学的实验方式验证,也许其中的部分机制可以简化或融合,带来更简洁的实现。
我们需要警惕“惯性复杂化”,也不能轻信“过度简化”,本次反思是非常有价值的一步。
继续分析循环的问题
我们暂时搁置前面的思路,试图进一步观察和确认当前关于循环检测的问题。在实际操作中,尝试寻找一个可以站立并触发循环的位置,然而发现只有当鼠标悬停在某些特定区域时,才似乎会触发显示循环,这种依赖鼠标位置的行为让调试变得困难。
我们考虑实现一个机制:在检测到循环时自动暂停程序,这样可以静态地观察当前状态,分析是否真的存在循环。因为目前我们也不确定:在这些跳跃操作中到底是否真的会产生逻辑上的循环。
目前看起来比较合理的一种可能性是:
- 在跳跃过程中,角色的精灵与地面或其他对象的 Z 深度可能出现穿插;
- 可能由于跳跃高度或碰撞体模型导致Y 方向或 Z 顺序错乱,从而误判为存在循环;
- 但我们不理解的是,为什么这些异常状态会导致整个屏幕边缘发出闪烁提示(说明被认为整体进入了一个大循环)。
这点仍是一个未解之谜,因此目前我们倾向于认为:这其中可能存在某种排序或状态更新上的 Bug。
接着我们注意到另一个值得怀疑的问题:
- 当前测试中使用的一种对象,似乎无论放置在哪里都会引发循环检测;
- 举例来说,当我们将该对象放置在角色脚下时,就立即触发了循环提示;
- 进一步观察发现,这个对象其实是由 4 个在角落略微重叠的矩形构成;
- 这些边角的重叠可能足以构成一个图结构中的有向环,从而被循环检测逻辑标记。
这提供了一个新思路:
- 循环检测机制很可能是基于图的遍历算法(比如 DFS);
- 一旦节点之间存在双向路径或互相依赖关系,即可能导致“图中环”的误判;
- 如果对象本身的物理模型或绘制区域有细微重叠,那即使是静态状态下也可能被标记为循环。
当前结论和后续方向:
- 需要更好的调试机制:每次检测到循环时暂停程序,允许逐帧审查状态;
- 对重叠检测的算法做进一步精细化处理:当前系统可能将合法的微重叠误判为死循环;
- 跳跃期间的排序与穿透需特别关注:Z 值突变可能造成临时的逻辑不一致;
- 屏幕边缘的闪烁提示机制本身也需验证:它可能反映的并非全局循环,而是某种级联错误。
我们暂时还不能完全排除 Bug 的存在,但已识别出几个可能的成因,并建立了下一步的验证方案。后续将继续深入这些观察点来精确定位问题本质。
修改 game_render_group.cpp
:调整 PushRectOutline()
绘制四条边界矩形的位置
我们现在意识到导致循环检测触发的可能原因之一,是在渲染时矩形边框的绘制方式引发了不必要的重叠。当前渲染逻辑中,为了显示轮廓或边界,我们在执行“矩形边界绘制推送”时使用了一种厚度偏移(thickness offset)策略。这个策略会将边框从中心向四周扩展,从而可能导致多个边框部分重叠,触发错误的循环检测。
问题分析:
- 在执行“push_rect_outline”时,绘制的边缘不是严格位于原始矩形边界上,而是因为厚度处理而向内外扩展;
- 当前厚度值作为维度使用,被用来从中心对称扩展;
- 这样处理后,不同的边可能因为厚度扩展而相交,特别是上下相邻的边框或对象重合区域之间;
- 这类交叉会被图遍历算法检测为“存在连接关系”,从而在某些排序路径中构成逻辑环路(循环)。
解决思路:
我们希望这些轮廓不相交,因此必须更准确地控制它们的位置:
将厚度计算提前融入偏移逻辑:
- 在生成绘制偏移量时,将厚度考虑进来;
- 而不是在原始坐标之外再加厚度;
- 例如,如果想让矩形轮廓向外偏移
thickness
,则将offset
设置为+ thickness/2
的方式,而不是默认从中心扩展。
分别处理每条边的绘制起点和终点:
- 对于顶部和底部边,需要显式将其位置错开;
- 例如:顶部边界绘制在
y - thickness
,底部绘制在y + height + thickness
; - 这样可以彻底避免边与边之间重叠。
确认厚度不参与组间排序逻辑:
- 渲染用的“视觉粗细”不应干扰图结构中的逻辑排序;
- 如果厚度被错误纳入了计算,那就需要调整,使其仅为渲染层使用的偏移参数。
操作建议:
- 将厚度值作为偏移参数,在构造坐标时就考虑进去;
- 避免在绘制逻辑中直接使用 thickness 扩展原坐标尺寸;
- 若使用了偏移中心的策略,确保各边框的投影区间互不相交。
当前结论:
通过更合理地使用 thickness 偏移,我们应当能够避免多个绘制矩形之间因边缘重叠而被误判为图中存在循环的情况。这个问题本质上属于视觉辅助元素引入了逻辑干扰,只需理清厚度使用的边界即可解决。我们将继续调整渲染逻辑以验证该推论。
黑板讲解:如何使用不相交的矩形绘制轮廓
当前我们正在解决绘图时矩形边框重叠引发错误排序或循环检测的问题。为此我们调整了矩形的绘制逻辑,使得顶部与底部边框不再互相覆盖,同时还修正了判断矩形是否相交的函数逻辑,以避免相邻边界被误判为重叠。
绘制调整过程详解:
边框内移:
原来绘制上下边框时,坐标是从矩形中心对称扩展的,厚度从中心向外延伸;
为了避免重叠,我们将上下边框在 Y 方向上微调:
- 顶部向内移半个厚度;
- 底部同理,向内移半个厚度;
这样可以让两个边框在视觉上接近但不会重叠。
尺寸缩减:
在边框绘制时,我们将绘图的尺寸缩小
thickness
:- 例如,原来绘制的高度是
dimension.y
,现在变为dimension.y - thickness
; - 避免在边缘处绘图超过实际边界,导致邻接区域重叠。
- 例如,原来绘制的高度是
居中矩形自动修正偏移:
- 由于当前绘制矩形是以中心为基准居中扩展的,所以当我们减少厚度并修正偏移后,
实际上矩形会自动保持在不重叠的区域中,无需额外边界偏移。
- 由于当前绘制矩形是以中心为基准居中扩展的,所以当我们减少厚度并修正偏移后,
恢复原始绘图逻辑:
- 一旦厚度和边框绘制的修正生效,就不需要原先为解决重叠而引入的“临时偏移”逻辑了;
- 恢复为更干净的绘图逻辑结构。
判断矩形是否相交的逻辑调整:
原来的问题:
- 当前的矩形相交检测函数
rectangles_intersect
使用的是严格包含判断(即,端点相等也认为是相交); - 这会导致即使两个矩形边缘只是刚好接触,也被误认为发生了重叠。
- 当前的矩形相交检测函数
修改方法:
- 改为开区间判断,例如:只有在
A.max_x > B.min_x
且A.min_x < B.max_x
时,才认为相交; - 如果
A.max_x == B.min_x
,说明两个矩形恰好紧贴,但没有真正重叠,因此不应视为相交; - 修改函数逻辑,允许矩形**“相邻但不相交”**的状态存在。
- 改为开区间判断,例如:只有在
结果与目标:
通过以上绘图逻辑与判断逻辑的修正,我们期望:
- 解决因矩形边缘交错导致排序错误或产生伪循环的问题;
- 保持图形的渲染精度与逻辑一致性;
- 清晰分离“视觉边界”与“排序边界”的语义,避免因渲染细节影响逻辑行为。
后续将继续观察排序与循环检测行为,确保这些修改生效并没有引入新的视觉或逻辑问题。
黑板讲解:RectanglesIntersect()
当前的实现逻辑
当前矩形相交判断的逻辑是这样的:
我们检查两个矩形的最大值和最小值,比如判断矩形 B 的最大 X 值是否小于或等于矩形 A 的最小 X 值。如果满足这个条件,就说明它们不相交。
具体来说,判断的是“是否不相交”,如果判定不相交,结果取反就能得到“相交”的结论。
这个判断方式实际上已经是非包含型的,也就是说,如果两个矩形恰好边缘相接(最大值等于最小值),它们并不会被判定为相交。
这本来是符合预期的,因为相邻的矩形不应该被误判为重叠。
不过,实际情况中可能因为浮点数计算精度的原因,中心坐标差计算出来的矩形边界会有细微重叠,导致判定为相交。
但整体逻辑上,这套判断是严格的非包含判断,理论上不会把边界刚好接触的矩形误判为重叠。
因此,现有的相交检测逻辑已经实现了“开放区间”式的判断,满足想要的效果。
修改 game_render_group.cpp
:为 PushRectOutline()
加入一个微小的偏移(epsilon)
意思是,如果想让两个矩形不被判定为相交,我们就需要让它们之间保持至少有一个非常小的间隙,也就是一个微小的 epsilon 值,防止它们实际边界重叠。
这样做的目的是为了避免因为浮点数计算误差或者边界恰好接触导致判定为相交。
现在需要确认这套逻辑是否确实有效,确保没有其他隐藏的错误存在。
目前测试来看,这样处理是可行的,没有发现其他问题。
运行游戏,发现轮廓线不再导致循环,但英雄角色依然会
目前发现给矩形检测加上一个很小的epsilon值之后,移动时不会再产生循环。由此看来,之前产生循环的原因就是那些描边轮廓自己形成了一个环路。现在解决了这个问题,调试时用的循环检测也可以保持这样就好。
不过,这又引出了另一个问题:为什么在其他一些情况下仍然会出现循环?观察屏幕上的情况,发现当角色在场景底部时,不属于上方树木那组,但当角色移动到上面时,角色会被判定为属于那组。推测是因为角色的躯干精灵和头部精灵部分穿插在阶梯区域,导致排序时形成循环,但还不能完全确定。
目前对这个逻辑的理解还算有些信心,因为这些现象看起来至少有一定合理性。只不过屏幕上的精灵边界比较模糊,没有严格包围它们的边界框,也没有考虑缩放因素,导致视觉上有些混乱。缩放因素本来就应该纳入考虑,接下来也会处理。
同时,觉得目前的排序逻辑总体还算达到预期,尤其是多房间的情况,精灵会分开在不同绘制层排序,不太担心这些问题。
但有几个疑问比较困扰,比如为什么角色头部会排在某些地砖前面,这感觉不太符合直觉,头部理应在那些元素后面。也许是z值设置不合理导致的,尤其是头部位置好像漂浮得比阶梯还高,但实际中并不会用xy偏移,缩放等效果也需要调整,避免头部和身体精灵位置异常。
整体来看,广泛使用的精灵排序表现还算不错,没有看到特别异常的行为,这是好事。
但是有些具体细节还是不太理解,比如为什么某些“宽精灵”(wise sprites)总是排在0层以上,理应它们应该非常靠前,不可能被其他精灵覆盖,但现实中却出现了不符合预期的排序。具体表现为某些调试文本的阴影层竟然排在文本后面,这明显是不对的。
总的来说,感觉z排序系统存在某种奇怪的问题,不只是广泛精灵和窄精灵之间的相互排序异常,即使单独看z轴精灵排序,也有问题。
再加上在启用额外房间时,这些z轴精灵确实大部分都会排在其他精灵前面,但偶尔又出现不一致。
目前还不确定具体原因,整体排序逻辑仍显得很奇怪,令人困惑。
修改 game_debug.cpp
:让 DEBUGEnd()
只绘制一个字符,运行游戏并逐步调试 SortEntries
现在想做的是关闭画面上的所有内容,只保留文字显示,这样就能简化画面,方便调试。比如把显示内容改成只画一个字母,这样画面上总共只会有两个文字元素,方便一步步追踪它们的排序过程,看看有没有明显的bug。
具体操作是把显示内容临时改成“h”,这样理论上只会有一个元素在画面上。接下来观察排序流程,尤其是在排序条目(sort entries)阶段,看看画面上到底画了几个元素。
目前看系统显示有三个元素,但不清楚具体是哪些。想深入了解这些元素具体是什么,以便进一步确认它们的相互关系和排序情况。
在排序时,会对元素两两做碰撞检测,判断它们是否相交。现在查看对a和b元素的检测,确认它们是否相交。
总之,计划通过极简化画面内容,减少元素数量,逐步跟踪和观察排序过程,寻找排序异常的线索和根源,理清当前排序问题的具体表现和产生原因。
修改 game_render_group.cpp
:让 GetBoundFor()
使用 SortBias
突然想到可能是因为信息在经过排序偏差(sort bias)这条路径时,排序偏差已经不再起作用了,这很有可能就是问题的根源。
所以我们需要在渲染组(render group)内部检查,现在已经不再使用之前的那个牺牲(sacrifice)机制了。
当实际进行渲染操作,比如推进渲染压力(pressure act)等处理时,在获取边界框(get bound)的时候,应该把排序偏差(sort bias)考虑进去。
具体来说,我们在获取z轴最大值(z max)时,很可能需要将排序偏差加进去,以确保渲染顺序正确。
总结就是:目前渲染时没有正确使用排序偏差,导致排序逻辑出现问题,需要在计算边界和z值时加入排序偏差来修正。
运行游戏,确认调试文本现在被正确排序
目前我们已经把之前依赖 sort bias 的部分恢复回来了,理论上这些功能现在应该可以正常工作。我们尝试打开视图进行检查,发现界面现在干净整洁,排序组(sort groups)显示的信息看起来也比较合理。
在查看构建排序图(build sort graph)的过程中,尽管其中涉及到大量的数据组合和排列,但现代计算机运算速度极快,仍能轻松完成这些复杂计算,令人惊叹。
从当前的表现来看,z 方向的 sprites(z sprites)彼此之间的排序现在是正确的,尤其当屏幕上只有 z sprites 时,排序逻辑没有出现错误,这是一个积极的信号。
不过,在检查这些 z sprites 的边界矩形时,发现它们的对齐似乎有些问题。推测可能是因为当前还没有正确处理缩放(scaling)相关的逻辑。
尽管如此,这是一个阶段性的进展。
但仍存在一个没有解决的问题:当混入 y 方向的 sprites(y sprites)时,排序逻辑就不再正确。y sprites 仍然排在 z sprites 之上,尽管从理论上讲,z sprites 的“体积”或“深度”应该比 y sprites 要大得多。
这可能意味着我们当前的排序规则(sort rule)还不够完善,需要进一步改进,以确保在同时存在 y 和 z sprites 的情况下,排序逻辑依然正确可靠。
查看 game_render.cpp
中的 IsInFrontOf()
函数
我们回头查看了当前的排序规则逻辑,重新理解它的结构是否合理。可以确认的是,当前排序在仅处理 z 方向 sprites 的情况下是能正常工作的,也就是说,在它自己的应用范围内表现是正确的。
但当涉及混合类型的排序,比如 y 方向 sprites 和 z 方向 sprites 同时存在时,问题就出现了。
在当前的排序逻辑中,首先检查的是是否存在包含关系,即“a 是否包含 b”或者“b 是否包含 a”。如果存在包含关系,我们就依据 z 值排序;如果没有包含关系,我们则退而用 y 值排序。
这种设计背后的逻辑推理是:如果两个对象没有包含关系,则根据它们的 y 值决定先后顺序——y 值更小的排在前面,符合直觉。
但问题恰恰出在这里:我们现在面对的实际情况是,即便一个 z sprite 具有很大的 z 值,也会因为它的 y 值稍微靠前而被排在另一个 sprite 前面,从而导致排序错误。这说明排序规则本身并不适合当前混合 y/z 类型对象的场景。
进一步思考,“包含关系”这一判断本身也存在不清晰的问题。我们回顾逻辑发现,我们之前设定的是在“屏幕空间已经确认重叠”的前提下再进行 z 或 y 的判断。然而这会带来混乱,因为我们并没有明确定义“包含关系”在这种上下文中到底该如何判定。
尤其对于 z sprites 而言,它们确实有明确的 y 范围(y min 到 y max),可以用于包含关系判断;但 y sprites 只是一个具体 y 值,并没有范围信息。因此在包含判断中可能永远无法满足包含条件,导致默认走 y 排序路径,从而忽略了 z 维度的重要性。
总结几点问题:
- 当前排序规则默认使用 y 排序作为兜底方案,这在 z 值差距极大时是不合理的。
- 包含关系的判断可能失效,特别是当 y sprite 只有一个 y 点而 z sprite 是范围时,这个判断变得无效。
- 排序规则并没有真正考虑“视觉优先级”或“深度遮挡”的语义,仅仅依赖二维坐标做决策,这是当前问题的根源。
接下来我们应该考虑重新设计排序规则,明确不同类型 sprites 如何进行有效比较,特别是在存在遮挡或视觉前后关系的情况下,是否可以引入更合理的深度判断逻辑,而不仅仅是 y 值或 z 值排序。也可能需要修正 sprites 本身的范围表达,使得包含判断能更准确执行。
黑板讲解:IsInFrontOf()
的工作原理
从理论上讲,我们得出的结论是:当前这种 z sprite 和 y sprite 混合排序时出现的问题,可能其实并不是程序逻辑上的错误,而是我们对其视觉表现与排序规则之间关系的理解有偏差。
我们重新从概念上做了推导。例如:一个 z 方向的 sprite(代表地面或平台)上方站着一个 y sprite(代表角色),这在屏幕的投影中会出现重叠。然而从侧视角观察时,两个 sprite 实际上是错开的,y sprite 是站在 z sprite 上的,并不构成严格意义上的“包含关系”。
这就解释了为什么当前排序系统无法判断它们是包含关系。因为虽然它们在二维平面上(屏幕空间)有重叠,但从实际深度(z)来看,它们根本就没有交错、包裹、覆盖——只是视觉上的对齐。
因此我们意识到这种排序现象并不是“bug”,而是我们之前的假设对包含关系的理解过于绝对。在三维空间中,某些 z sprite 与 y sprite 会表现为屏幕重叠但实际无交集,导致排序逻辑自然地落回 y 值比较,从而产生了我们觉得“不合理”的视觉排序。
进一步推导后我们确认,目前的算法流程基本是自洽的。排序规则并没有完全出错,而是在一些具体情况下不再满足我们想要的视觉直觉——但这并非错误,而是三维到二维投影中某些视觉假象的结果。
结论如下:
- y sprite 站在 z sprite 上时,虽然投影重叠,但深度上没有包含关系;
- 当前排序规则在处理这类场景时选择按 y 值排序,其结果虽然“反直觉”,但逻辑是合理的;
- 因此,我们目前并没有真正的 bug;
- 如果我们希望在视觉上修复这一排序效果,可能需要额外引入更多的上下文信息,比如角色所“站立”的表面深度,或者自定义的视觉优先级因子。
总之,现阶段的排序系统在逻辑上已经基本通顺,后续优化方向应该集中在“视觉表现与排序期望之间的映射”上,而非继续从算法结构本身寻找错误。
运行游戏并说明当前的排序系统可能已经没问题
目前的系统行为正是我们预期的表现,并且在处理“单房间渲染”的场景中是完全合理的。在“单房间”这一逻辑下,我们的设计并没有定义某个物体可以在另一个物体之上进行 X/Y 平面方向上的“堆叠”或“交错”,因此不存在在 X/Y 平面上互相覆盖却仍要进行深度比较的复杂情况。
即便某些画面上看起来像是存在“叠放”关系的情况,例如一个物体似乎在另一个物体上方,这种视觉印象在实际的系统中不会发生。因为根据我们当前的设计,如果存在这样的逻辑,那上面的物体必须在某个明确的层级位置上,例如贴合地面某一处而非浮动在空中。因此即使从屏幕空间上看似有交集,但从排序或系统数据结构的角度,它们并不构成真正的“交错”或“包含”。
根据目前的情况,如果我们开始将渲染系统拆分为多个图层(layer)——每个房间一个图层,每个层级分离管理,以及额外设置一个用于调试信息的图层——我们可能可以得到非常清晰且可控的渲染流程:
- 每个房间层(layer)只负责渲染当前房间内的内容;
- 每个层可设置独立的排序策略,比如房间层使用 Z 或 Y 排序,而调试层不启用排序或使用最简单的策略;
- 各层之间不会发生穿插或互相遮挡的问题;
- debug 层可随时启用或禁用,且不影响正常渲染。
从目前的分析来看,我们可能已经进入了一个“稳定可控”的状态,也就是说,在不增加更多复杂性的前提下,现有系统没有发现明显的错误或者需要立即处理的问题。我们可以继续按这个结构扩展,后续如果加入更多元素,再动态应对可能出现的新排序逻辑或交互冲突。
总的来说,目前状态表现良好,排序逻辑与图层结构看起来都是合理的,暂时没有急需修复的错误,系统已处于可进入下一阶段的开发状态。
修改 game_world_mode.cpp
:重新启用 AddMonstar()
和 AddFamiliar()
我们确实有些想念之前那些角色,比如蛇形怪、漂浮头之类的角色。曾经为了简化系统我们暂时移除了这些内容,但现在感觉也许是时候把它们重新加回来了。
当前系统已经趋于稳定,排序和渲染的机制基本清晰,在这种基础上重新引入这些具有特色的角色,不仅不会带来混乱,反而可能丰富整体的表现。尤其是在这样一个设定中,有蛇、有怪物,还有那个总是惹人烦却又忠实跟随的“熟人”,这让整个世界设定开始变得有趣起来。
更重要的是,我们还有一个更深层的动机 —— 想让新的美术资源真正投入使用。Anna 最近完成的美术包实在太酷了,尤其是她为地下城区域绘制的部分,现在甚至还有了孤儿院和一批孤儿角色,整个内容完整度非常高,视觉表现也非常吸引人。我们非常想让这些内容出现在游戏中。
这些新的资源不但能提升游戏的美术质量,也会丰富场景表现与角色互动,让世界更有生命力。这一切让我们对接下来的开发充满期待,相信加入这些内容之后,游戏将迈入一个更加成熟有趣的阶段。这将是一段非常有趣的旅程。
Q&A
看起来主角的躯干在上下楼梯时偶尔会绘制在头部上方
目前的确存在一个现象,即角色的躯干图像会绘制在头部图像之上,但这是预期内的行为。在现有的排序逻辑中,这是能够被接受的表现形式。但随着系统的进一步完善,接下来我们需要做两件事来改进这个机制,虽然这些工作可能会等到完成美术资源包之后再进行。
首先,需要为角色的躯干等部分提供真正的精确图像边界(sprite bounds)。目前这部分的边界信息可能是简化处理或者临时的,要实现更合理的深度排序和碰撞检测等功能,必须提供准确的图像边界信息。通过这种方式,系统在判断两个图像是否重叠或包含时会更加精确,进而提升整体排序的准确性。
其次,当前图像的正确绘制顺序主要是通过调整 z 值来实现的。这种方法虽然能起到效果,但并不是最理想的方式。为了提高系统的健壮性与灵活性,我们计划为特定图层关系(比如头部与躯干)引入明确的排序规则。这意味着排序逻辑不仅仅依赖于简单的 z 值,而是可以通过自定义的规则来确保在复杂场景中也能始终如一地呈现出正确的视觉层次。
这些改进将为后续更复杂角色结构和动画系统的支持打下基础,也为更丰富的场景构建提供了技术保障。同时,它们也有助于将新的美术资源(例如即将加入的地下城和孤儿院内容)与现有系统更好地融合,确保画面呈现的逻辑性与美观性兼具。
黑板讲解:英雄角色是如何构建的
目前图像的构造方式存在根本性问题。所有图像的精灵(sprite)实际上都被放置在了同一个位置,也就是说,多个图像彼此叠加,完全重合,只是外观略有不同。例如,一个图像位于某个位置,另一个图像又正好叠加在其上,表现出不同的外形,但本质上它们的位置完全一致。
理想情况下,更合理的处理方式是让这些精灵在视觉上呈现出一定的分层结构。例如,头部、身体和其他部分的精灵并不应该重合,而是应该有各自明确的相对位置关系。通过这样设置,排序算法才能够正常地对它们进行合理排序,从而保证渲染结果的正确性和一致性。
即便排序系统本身在某些情况下仍然无法完全自动处理这类重叠问题,我们也不打算将这部分复杂度强加到排序系统中。因为从语义上讲,这种图像结构本身就不合理——我们并不希望出现“头部在躯干上漂浮”的表现,而是希望无论角色如何移动,头部始终绘制在躯干之上。
因此,更可取的做法是引入明确的规则,强制规定某些图层的渲染顺序。例如,设置“头部始终在躯干上方”的逻辑规则,确保即使在复杂动画或动作中,渲染顺序依然不被打乱。这样一来,排序算法无需处理过多的特殊情况,也避免了因位置重合导致的视觉错误,提高了整个渲染系统的健壮性和可维护性。
总之,当前的问题根源在于图像构造方式过于简化,未来应当优化精灵位置结构,并引入强制性图层优先级规则,从而获得更正确、更易控制的渲染效果。
英雄与楼梯之间的碰撞是否正常?身体在下方、头在上方的情况应该不能出现
当前游戏中,开区域的实体之间尚未实现完整的碰撞系统。目前几乎所有元素的碰撞功能都被关闭了,准确地说,几乎没有任何物体具备真正的碰撞检测功能。
不过,仍然有一部分可视化的碰撞信息被绘制出来。画面中可以看到一些蓝色矩形,这些蓝色矩形代表当前存在的碰撞区域。比如围绕树木构成的圆环区域,蓝色的矩形框显示了它们所占据的碰撞区域。这是目前唯一具备碰撞属性的场景元素,只有树木和主角之间存在有效的碰撞检测。
然而,这些碰撞可视化信息的绘制似乎存在一些位置异常。部分蓝色矩形出现在视觉上“漂浮”的位置,即显示得比预期更高一些。这种现象可能是由于绘制代码已经过时所导致的。未来在清理Z轴信息并完成图层系统之后,将会对此部分逻辑进行修复和调整。
就整体设计而言,碰撞系统未来的角色可能也不会太重。大多数情况下,游戏将主要通过“占位检测”的方式来限制角色的移动——即根据一个方格是否可被占据来判断能否前进,而不是依赖复杂的物理碰撞。这意味着,例如防止角色的头部穿过障碍物的逻辑,可能不会通过传统的碰撞判定来实现,而是通过格子状态判断,例如某个方向上没有开放格子时就不能移动过去,或者限制不能越过某个中线位置。
因此,碰撞系统在未来可能更多地用于处理诸如投射物击中、敌人攻击等战斗相关的交互,而不是承担大部分移动阻挡的功能。角色移动将更多依赖离散的网格逻辑,而不是连续物理模拟。这种处理方式在逻辑清晰性和可控性上具有更高的稳定性,但是否会进一步演变成更复杂的碰撞系统,还要看后续开发的具体需求。
你认为类似的图排序逻辑是否会在游戏中被用于其他用途,而不仅仅是精灵排序?
我们目前所构建的图结构,尽管复杂且功能完善,其主要用途实际上仅限于精灵排序系统。在当前的实现中,它被用于确保不同图层、实体在屏幕上的正确渲染顺序。这类排序图结构针对的是渲染逻辑,因此其设计是非常特化的。
虽然理论上,这种图结构也许可以被用于游戏中其他系统,比如世界生成之类的机制,但实际上并不会直接复用这个图。因为世界生成需要的图结构与精灵排序所需的并不完全相同,后者偏向于处理空间上的视觉遮挡与层级关系,而前者更可能依赖地形、连接性、规则性等维度的信息。
若未来在世界生成中确实需要使用图结构,我们更可能是将当前的实现“剪切粘贴”并进行调整,以构造一个更适合其需求的变体,而非直接复用原有结构。当前这个图系统不太可能被抽象出来作为通用组件进行多处使用。
简而言之,这个图系统在本项目中属于一种高度定制的实现,专门服务于精灵渲染排序的需求。其他系统比如世界生成虽然可能会用到图,但几乎可以确定不会以“完全相同”的方式来使用,顶多会基于这个实现思路进行简化或改造。我们更倾向于根据具体需求定制相应的图结构和逻辑,而不是强行复用不完全匹配的模块。
这是什么语言?
我们使用的是 C++ 语言,但主要编写风格更偏向于 C,也就是说我们大多数时候只使用 C++ 的一个子集,并不广泛采用现代 C++ 的复杂特性。我们不会大量依赖类、模板、STL 容器等 C++ 高阶语法,而是以更接近 C 的方式进行开发。
这套开发方式总体上是偏“老派”的,语言使用上追求简洁和直接,避免引入诸如解释器、垃圾回收机制等现代语言特有的运行时系统。我们的编程风格虽有一定现代感,比如在结构、资源使用上遵循一些新式设计理念,但总体而言,更接近老派的系统级开发方式。
我们不会刻意做那种“4K Demo”式极致资源优化的老派场景渲染,但我们确实会手动管理内存、显式调用硬件资源,比如使用 GPU 等硬件加速功能,这也是一种“老派精神”在现代硬件上的延续。简单来说,是一种结合了老派语言风格与现代系统设计理念的混合型开发方式,注重对底层的掌控和性能表现,同时不过度依赖现代语言抽象。
你会给初学者哪些编程建议?
作为新手程序员,我们认为有几条重要建议:
首先,要多写代码,反复编程实践是提升编程能力的核心。光知道理论没用,必须不断动手,写程序,解决问题,积累经验。
其次,不能只满足于让代码“能用”,而是要深入理解代码背后的原理。要问自己“为什么代码会这样工作”,“它是如何运作的”,不只是追求表面上的功能实现。这样才能真正掌握编程的本质。
另外,建议从简单的高级语言开始入门,比如 BASIC 或 JavaScript,这样容易上手。但同时也要挑战自己,逐渐学习更底层的语言,比如 C 语言,甚至汇编语言。这样能更清楚地了解计算机的工作机制和程序是如何被执行的,理解底层细节,避免只停留在调用库函数的层面。
还要不断反思自己写的代码。问自己:有没有更好的写法?代码为什么要这样写?这是否是语言本身的限制?是否有其他语言或技术手段能做得更好?这种自我质疑和探索能推动持续进步。
最后,保持持续学习的态度,永远不要满足于“差不多能用”就停下。编程是一条不断深挖和提升的路,要不断拓宽知识面,了解更多底层和原理,积累更深厚的技术实力。
总之,练习+深入理解+不断反思和学习,是成长为优秀程序员的关键。
你预估完成《Hero》这款游戏还需要多长时间?与最初估计相比有何不同?我猜你现在完成了一半?
最初估计完成时间大约是600天。我一开始误说成两年,但那显然不准确,因为我们每周只工作五天,还会有休息时间,所以600天实际上大约等于三年左右。
这个600天的估计是基于总工作时长约600小时,也就是每天工作一小时左右的节奏。
目前还不确定这个估计到底有多准确,因为很多因素都会影响进度。游戏引擎编程部分差不多快结束了,这部分工作预计会在600天内完成,时间比较确定。
更难估计的是游戏功能的开发,比如具体实现游戏里的怪物类型、世界生成等内容。这个部分可能需要100天,也可能是200、300天甚至更多。假如需要300天左右,那么总时间和原先的估计基本吻合,可能多出50天或100天也在预料之中。如果超出太多,那就得重新评估。
大约一百天后,当游戏引擎部分基本完成,开始真正构建游戏内容时,我们会有更准确的时间评估。那时能看出实现足够多的游戏内容需要多久。
我们也希望游戏能够支持后续扩展包,不一定是付费的,但可以不断更新,添加新内容,给玩家持续的新鲜感。这种计划会给开发带来一定的灵活性,不必把所有内容都塞进最初的版本里。
不过最初版本必须保证游戏足够有趣和完整,至少要包含一套最基本的元素,比如不同的怪物种类、多个区域类型等,让游戏有一定的深度和多样性。
总结来说,目前的时间估计是600天左右,分为引擎开发和游戏内容制作两部分,后续会根据进度调整和更新估计,目标是先做出一个有趣的初始版本,之后通过扩展包持续丰富游戏内容。
题外话:你说每个游戏程序员都会像你一样调试时用 Visual Studio,编写时用其他编辑器,为什么?
大部分游戏程序员都会用类似我们用的调试器,在Windows上用Visual Studio进行调试,但编辑代码时会用不同的编辑器。至于为什么这样,主要是因为Visual Studio是Windows上唯一真正可用的、适合游戏代码调试的调试器,几乎没有什么更好的选择。
虽然Visual Studio的调试功能是目前最实用的,但它的代码编辑器并不是每个人都喜欢。有很多原因让人们不愿意用Visual Studio自带的编辑器,所以不少人选择用外部编辑器写代码,比如Vim、Emacs、Sublime Text、VSCode等,大家各有偏好,根本没有一致认同哪个编辑器最好。
目前缺乏其他更好用的调试器是造成这种现状的主要原因。尽管Visual Studio调试器不被很多人喜欢,但它基本是唯一的可用选项。如果有更好的调试器出现,大家很可能马上转用。目前的状况是大家不满意Visual Studio,但没有别的选择,只能将就用它。
所以总结来说:大家用Visual Studio调试是因为实在没有更好的调试器可用,编辑代码用什么编辑器则根据个人喜好,市场上缺少优秀替代品。若将来出现更好的调试器,游戏开发者们会迅速切换。
你更享受作为编程挑战来开发这项目,还是更注重教学意义?
我们对这个项目的享受主要不是因为编程挑战本身,而更多是在教学和讲解编程的过程中。制作一个二维游戏,真正不同于三维游戏的部分主要是图形方面,游戏代码本身很多逻辑和三维游戏类似,艺术管线相关的编程也是一个有趣的方向。
我们不会在直播时去刻意挑战特别难的编程问题,因为复杂的问题需要高度集中精神,难以边讲解边解决。即使是相对简单的二维图层排序问题,也在直播中花了不少时间,复杂度更高的问题更不适合现场解决。
虽然不会追求特别高难度的挑战,但对编程本身仍然很喜欢,不需要困难才觉得有趣。项目中也有以前没做过的事情,或用不同方法实现的部分,带来探索和学习的新鲜感,依然觉得很有意思。
总之,这个项目的乐趣更多在于教学和探索,而不是纯粹的编程难题挑战。
你怎么看待在 C++ 中使用单元测试和测试驱动开发?这种方法是否真的有用、值得尝试?
关于单元测试和测试驱动开发,我们认为这是一个非常常见但也复杂的问题,实际上每种编程方法论都是基于权衡取舍的,没有绝对的对错。是否采用单元测试或测试驱动开发,关键在于评估编写测试所花费的时间,是否能够节省调试代码的时间。
我们要问自己几个问题:
- 写测试能发现多少bug?
- 写测试需要多少时间?
- 如果没有测试,这些bug是否能被发现?
- 如果能被发现,发现这些bug需要花多少时间?
如果写测试既花费不多时间,又能有效捕捉难以发现的bug,那么单元测试就是一个好主意。反之,如果测试不能很好地覆盖关键bug,写测试反而浪费时间,那就没必要强求。
这种权衡和思考并不依赖于你写的代码语言,也不依赖于你写的是游戏还是网页代码,根本上是看项目的具体情况和需求。比如,代码输入输出明确且可以被具体测试时,测试驱动开发可能非常适合。但游戏开发中很多逻辑是基于用户体验或视觉效果,比如某个粒子效果是否在打击时出现,这类内容根本没法用单元测试覆盖。
因此,在游戏开发中,单元测试的应用受限,很多东西还不适合单元测试。相比之下,Web开发里很多功能都是明确的输入输出关系,单元测试更容易实施,也更有意义。
我们举个例子,以前在一个大型项目中,曾经写过单元测试来测试角色动画的系统。因为这段代码会被成千上万的游戏用到,调试成本高且后期发现bug很麻烦,所以写单元测试很划算,能大幅节省后续维护和调试时间。这个时候,写测试的成本是值得的。
总之,任何开发决策都要基于现实的权衡:
- 当前工作是否能通过写测试节省整体时间和精力?
- 写测试是否真的能捕捉到关键bug?
- 测试成本是否合理?
如果你陷入“我总是写测试”或“我从不写测试”的极端,基本上就是没找到平衡点,做法很可能不理想。没有一种方法是万能的,灵活应对项目需求和个人情况最重要。
这个思维方式也适用于所有开发实践,不能盲目迷信某个工具或方法,比如代码覆盖工具(Code coverage)并不总是能帮你找到bug,有时它的价值非常有限。不同的人犯不同类型的错误,有些人写测试会特别有效,有些人则不然,因人而异。
在团队协作中,如果某个成员容易犯特定错误,写单元测试就能帮团队整体省事,提高效率。反过来,如果没有这种需求,写测试就是时间浪费。
总结来说,编程中的所有实践都必须基于“这件事能否节省整体时间和精力”的权衡判断,而非固守某种教条。灵活运用,结合项目实际情况和团队需求,才是最佳做法。
因为你是唯一的程序员!当你在团队中写代码时就会用 const
了
关于在团队中使用类型转换(casting)的问题,我们的观点是不完全认同“团队合作时必须使用类型转换”的说法。团队合作时,确实需要考虑代码的安全性和易用性,但简单依赖类型转换本身并不是解决问题的最佳手段。
在实际大型项目中(例如非常庞大的团队项目),我们设计接口(API)时会采取防御性设计策略。针对团队成员可能犯的错误,会在接口层面设计得更健壮,防止错误的发生,而不是单纯依赖类型转换来限制数据管理。这样既允许使用类型转换,也允许不使用,给团队成员灵活度,鼓励写出好的代码。
类型转换本身是一个非常薄弱的防护措施,它只是告诉编译器某个数据的类型不能被管理,但并没有真正解决使用过程中的错误问题。如果真担心团队成员使用不当,需要从更根本的设计角度入手,设计更清晰、更安全的接口,避免团队成员容易犯错误。
如果团队协作中代码划分不清楚,大家都在随意修改代码,那么仅靠类型转换解决不了问题,反而更容易出错。此时如果还要依赖类型转换,那更是麻烦,因为这意味着团队内部信任度低,代码管理混乱。
总的来说,虽然类型转换有一定作用,但不应该被过分依赖。更重要的是通过合理设计API和明确职责划分来防止错误,提升代码质量和团队协作效率。遇到不负责任或常犯错误的团队成员,依靠类型转换也帮不了多少,反而应该加强团队管理和代码规范。
所以,在团队合作中,合理设计接口和清晰分工比单纯依赖类型转换更为关键。类型转换可以作为辅助手段,但绝不是解决问题的核心。
我坚持了 5 年 100% const
正确性,再也不干了
完全使用类型转换(casts)进行编程持续了大约五年时间,但实际证明对工作几乎没有任何帮助,感觉完全是浪费时间,没有带来任何实质性的好处。