游戏引擎学习第312天:跨实体手动排序
运行游戏并评估当前状况
目前排序功能基本已经正常,能够实现特定的排序要求,针对单一区域、单个房间的场景,效果基本符合预期。
不过还有一些细节需要调试。现在有些对象的缩放比例不对,导致它们看起来有些怪异,需要调整比例并重新确认排序是否正确。一旦这些问题解决,还需要对角色上楼梯时的排序规则进行微调,确保排序效果合理。
目前遇到一个比较特别的情况,需要手动覆盖排序规则:英雄角色的头部和身体之间的排序。
具体来说,身体部分由两个主要的片段组成,实际上是三个部分(包括阴影),这些部分的排序通过之前手动指定的聚合排序键保证了顺序正确,这部分功能运行良好。
但是头部是一个单独的实体,与身体分开处理。由于头部和身体是两个独立的实体,排序系统无法自动识别它们之间的关系。排序系统只看到一堆精灵,并不知道哪个是头,哪个是身体,因此缺少机制来说明“头部必须总是在身体前面绘制”。
当前没有手段表达这种“始终在前”或“始终在后”的排序规则。结果是,当头部在移动时,有时会出现身体在头部前面的闪烁现象。具体表现为,随着头部位置向后移动,头部会突然被排序到身体后面,这在视觉上显得不自然。
这个问题不是传统意义上的排序错误,因为排序算法确实按照对象在世界中的实际位置进行排序,排序结果是正确的。
但是从艺术表达和语义需求上来看,我们希望头部始终绘制在身体前面,不希望出现头部被身体遮挡的情况。
头部和身体是二维精灵,实际的物理位置导致头部偶尔会落在身体的后面,这和我们的视觉预期不符,因此需要人为介入来解决。
解决方案是要把这类“手动排序覆盖”信息传递给渲染系统,让它在排序过程中知道头部需要始终在身体前面,避免因为物理位置变化而打乱排序。
总之,现在的目标就是研究如何把这种“手动排序优先级”信息在排序阶段正确传递给渲染系统,并让渲染系统据此调整绘制顺序。
Blackboard(黑板):手动边缘指定
我们提出了一个解决方案,虽然还不能完全确定最终就采用它,但目前的想法是这样的:
渲染系统中可以维护一张小型的表,用于记录“边”的指定关系(也就是谁应该在谁前面渲染)。在这张表中,我们可以对一些特殊的对象进行标记。比如我们可以为某些对象分配一个ID:0、1、2、3、4、5等。
当我们将这些对象写入渲染命令列表时,同时给它们打上对应的标签。在排序阶段,渲染系统就可以根据这些标签进入表中查找关联信息,明确哪些对象之间存在手动指定的排序关系。
具体来说,当排序过程进行时,可以读取表中的手动边信息,通过标签找到对应的对象,再明确哪一个应该排在前,哪一个排在后。
这种做法有些零散琐碎,稍显“笨拙”,因为它涉及不少记录和查找的工作,但考虑到使用这种手动排序的对象数量是很少的,仅限于某些需要明确排序的特殊对象,因此我们认为这是可以接受的。
我们不会对每一个精灵都进行这种处理,不会每一帧对成千上万个实体执行这样的查找。只会对特定有明确排序要求的实体做处理,所以性能压力应该不会太大。
另外我们注意到,在排序系统中我们已经有一个叫做 DebugTag
的字段,现在考虑将它变成一个“真正的标签”,用来支持手动排序逻辑。这个字段本来只是为了调试用的,现在可以赋予它实际意义。
由于我们是在构建渲染列表时就设置这些信息,其中一个端(可能是被指定总在前或总在后的那个)可以直接存储一个指向对应 sprite_bound
的偏移地址,所以表格的另一端才需要通过标签来定位目标对象。
因此,整张表只需维护一边的映射关系,而不是双向都通过查找,从而减少了复杂度。这让实现变得更可行。
game_entity.cpp:修改 UpdateAndRenderEntities() 只在 PieceCount > 1 时调用 BeginAggregateSortKey()
我们接下来要处理的复杂部分,可能会出现在 world 模块中。每次处理这类逻辑时,都会涉及一个额外的实体配置。我们经常忘记,这段相关的逻辑已经被移动到了一个独立的文件里,每次都要重新适应这一变化。
目前在代码中,我们可以看到使用了 BeginAggregateSortKey
和 EndAggregateSortKey
,这两者用来对多个片段组成的角色(例如身体的各部分)进行聚合渲染。这种机制可以确保多个部件在渲染时维持一个统一的排序键,避免错位和闪烁。
这里还有一个可以优化的点:当前无论片段数量多少,都会调用这些函数来创建聚合排序键,但如果片段数量其实只有一个,这样做就没有意义。
因此,一个合理的优化策略是:只有当 PieceCount > 1
的时候才调用 BeginAggregateSortKey
和 EndAggregateSortKey
。这样可以避免在只有一个片段时也去打扰渲染系统,不必要地创建聚合排序键。
通过这种方式,我们可以减少渲染系统在没有聚合需求时的额外负担,使得整体渲染逻辑更加高效合理。这是一个比较安全且有效的优化点。
考虑如何表达一种意图——一个精灵(例如头部)必须总是在另一个精灵(例如聚合的身体)前面绘制
我们现在来到主循环这一段代码,遇到的问题是在调用 EndAggregateSortKey
这个阶段,我们希望能够在此时添加一条排序边(edge),也就是指定当前的渲染对象要排在谁的前面。但难点在于:我们无法直接知道此时“在谁的前面”这个目标对象是谁,尤其是当我们要排在某个特殊对象的前面时,例如头部需要始终排在身体前面。
这个问题之所以复杂,是因为当前是在构建世界实体,而不是直接管理它们之间的逻辑关联,我们在此阶段无法轻松获取与当前实体配对的另一个实体的信息。
幸运的是,当前的系统具有一定的灵活性,只是我们过去还没有特别深入使用它。比如,我们实际上可以通过 ID 查找特定的 “brain”(即负责管理实体逻辑状态的数据结构),而不是依赖于当前实体本身携带的状态信息。
可以看到,在实体初始化流程中,我们已经实现了根据 ID 查找或添加 brain 的能力。例如在执行 GetOrAddBrain
时,可以通过已有的 ID 找到对应的 brain 并进行操作。这表明我们完全可以在当前渲染逻辑中去查找该实体所对应的 brain,从而获取到其他需要的信息。
现在的问题是我们有两种方式可以处理这个“谁在谁前面”的问题:
-
直接在当前逻辑中查找相关 brain 并解析目标对象 ID:如果当前是一个“头部”实体,可以去查找自己的 brain,进而找到关联的“身体”实体 ID,然后根据这个 ID 添加一条排序边(edge)。
-
更合理的方式:在 brain 中直接记录这个手动排序信息:将要排在前面或后面的对象 ID 提前写入 brain 内部的数据字段,在渲染阶段读取这些信息即可。这样比起在渲染阶段临时去查找 brain 会更简洁清晰,并能保持职责分离。
从系统结构上来看,把这种“头部排在身体前”这一类的逻辑提前放入 brain 并由其管理,是更合理和干净的做法。这可以使渲染阶段更加纯粹地专注于执行排序,而不是在那时临时解析实体间关系。
因此,比较明智的做法是在 game brain 内部添加一个字段,比如 ManualSortTarget
或类似的内容,用来显式指明当前实体需要在哪个对象前或后渲染。这样就可以更方便地构建手动排序的机制。
game_entity.h:在实体结构体中添加 ManualSortInFrontOf 和 ManualSortKey
在英雄的代码中,如果我们能在结构中引入一个类似的概念,比如添加一个字段,用来表达“手动排序在谁前面”的意思,就可以让我们把这种信息明确地写进去。也就是说,brain(行为控制部分)可以直接往这个字段里写入数据,指定某个具体的目标,让渲染系统在排序时参考这条信息。
这样做的意义在于,我们就可以让像“头部应该永远排在身体前面”这种规则,由 brain 在逻辑阶段就写好,不需要等到渲染阶段再去推断或查询。渲染器只要读取这个字段,然后据此构建一条“排序边”即可。
这个机制的核心在于引入一个类似 ManualSortInFrontOf
的字段(或其他合适命名的结构),这个字段由逻辑层(brain)填充,而渲染层据此做出正确排序决策。这样一来,各部分职责清晰,也更容易维护、扩展,渲染阶段也无需额外逻辑判断谁该排在谁前面。整体思路非常清晰合理,是当前系统下最自然的做法。
game_brain.cpp:使 ExecuteBrain() 在头部和身体连接时设置这些值
我们认为这可能是实现目标最稳妥的方式。因此,在处理英雄逻辑的 brain_hero
类型中,当处理流程中确认我们既有“身体”又有“头部”的时候,可以在这里插入一个逻辑:
如果存在“头”和“身体”的组合,那么可以让“头部”记录下一个“我应该在谁前面”的引用,例如记录“身体”的实体 ID。这样我们就能建立一条明确的手动排序规则,确保渲染时“头”始终排在“身体”之上。
这样的判断也具备未来拓展的空间。比如如果将来引入“头部可以脱离身体”的特性(如漂浮头),我们可以在判断中引入附加条件:仅当“头”附着在“身体”上时才添加这条手动排序边。如果“头”是自由状态(未附着),则不添加。这样逻辑就变得更灵活、可扩展。
同时还需要处理“排序标签”的预留问题。也就是说,“身体”在被加入渲染列表时,需要被赋予一个特殊的排序标识,这样“头部”就可以引用这个标识,并向渲染器声明:“我应该排在这个对象之前”。
这些排序信息其实是暂时性的,仅对当前帧渲染有效,不应该被持久存储。所以在进行数据打包(pack)和解包(unpack)阶段时,特别是将实体状态写入存储块之前,需要确保这些手动排序信息不会被保留下来。为此,我们在解包逻辑中可以显式地将这些字段清零,确保它们不会在帧之间保留。这样就避免了意外的跨帧依赖,保持了渲染状态的正确性和独立性。
整体上,这套机制允许我们在逻辑层指定特殊排序规则,同时保证渲染阶段轻量、纯粹,并支持未来更多复杂角色结构的需求。
game_brain.cpp:使 ExecuteBrain() 能够与 RenderGroup 通信
我们继续推进这部分的实现。
现在的需求意味着“大脑逻辑模块”虽然此前从未与“渲染组”有任何耦合,但从现在开始必须建立联系了。之前这两者之间是完全分离的,因为“大脑逻辑”没有任何需要向“渲染器”表达的信息。但随着我们需要手动指定排序关系,“大脑逻辑”必须能够与“渲染器”对话。
因此,我们决定在调用 ExecuteBrain
函数时,开始传入 render_group
。实际上我们早就可以传这个参数了,因为那时渲染器已经配置妥当,但由于此前不需要,我们出于简洁性没有这么做。现在确实有需求,所以开始传递它是合理的。
在参数列表中,我们选择将 render_group
放在 input
参数之后,作为一个逻辑清晰的位置。同时也预示着当前帧的输入处理之后,可能会影响渲染行为。
接下来,我们需要引入一个“保留的排序键”(reserve sort key)的概念。目前这个部分还没正式实现出来,但很快会补上。这里会涉及到我们为特定对象(比如身体)保留一个特殊的排序标签,然后在另一个对象(比如头部)中使用这个标签,以建立明确的前后顺序。
这个保留的排序键将作为沟通两个实体之间渲染顺序的桥梁,我们会在接下来的步骤中设计它的分配方式、存储结构,以及如何在渲染阶段引用。总之,这一步是整个手动排序系统中关键的联系点,确保逻辑模块能准确告知渲染模块哪些对象在视觉上需要覆盖另一些对象。
game_entity.cpp:思考如何在 UpdateAndRenderEntities() 中指定渲染顺序
我们继续理清这一部分的逻辑。
现在,我们正在考虑的是:如果我们引入了“保留排序键”(reserved sort key)的机制,那么在具体渲染流程中,应该如何使用它。我们希望这些关键排序信息能被放置在最恰当的位置,这样渲染系统就能正确地理解哪些对象应该在另一些对象之上显示。
回顾一下之前的结构,我们注意到在渲染角色时,我们按顺序推入了各个部位的图层(如影子、身体、头部等)。通过观察 AddPlayerOther
相关代码可知,第一个被推入的图层通常是最上层(比如头部),而最后一个推入的图层是最底部的(比如影子)。也就是说,第一个索引为 0 的部件是视觉上最重要、最上层的部分,而后面的部分在视觉上逐渐向下排列。
因此,如果我们需要手动指定某个对象要“在另一个对象之上”,那么这个“另一个对象”就很有可能是这一组中的第一个被渲染的图层。换句话说,我们会把那个保留的排序键关联在第一个被推入的图层上。这可以为后续的手动排序指定提供锚点。
接下来我们也思考了一个潜在的问题:我们之前的逻辑是,只有当实体由多个部分组成时才使用聚合排序键(aggregate sort key)。如果一个实体只有一个部件,我们其实不希望强制它使用聚合排序,因为那样会给渲染系统增加不必要的工作量。
但现在,我们需要为排序锚点保留信息,而最自然的做法似乎就是将这些排序信息附加在“聚合排序起始”的位置上。但这和我们“不对单部件实体使用聚合排序”的原则相冲突。
也就是说,排序信息的结构设计必须兼容两种情况:
- 实体由多个图层组成时,可以通过
BeginAggregateSortKey
来处理,并在开头绑定排序锚点。 - 实体仅由一个图层构成时,也需要某种机制,在不强制聚合的前提下保留用于手动排序的锚点。
此时就需要我们决定是否接受一个折中方案,或者调整整体结构,以确保手动排序功能的灵活性与渲染系统性能之间的平衡。
因此,这部分仍有设计上的不确定性,需要进一步权衡。但总体的方向是清晰的:为特定部件保留显式排序信息,并允许其他部件使用该信息来控制前后渲染关系。
game_render_group.cpp:考虑让 BuildSpriteGraph() 添加一些额外的边以强制排序
我们现在需要深入理解渲染系统内部的实现方式,特别是 BuildSpriteGraph
阶段,来决定如何干净地引入“手动指定排序”的额外边(edges),从而使整个排序系统工作得更合理。
在 BuildSpriteGraph
中,现有逻辑会遍历所有节点,并根据网格信息建立默认的边连接。也就是说,这一阶段已经把所有自动生成的图层之间的依赖顺序建好了图结构。
但我们的问题在于:有些对象的渲染顺序并不能仅靠网格位置判断,而是需要人为指定某个对象必须在另一个对象之上渲染。比如头部要在身体上方、或者某种UI组件强制置顶等情况。这些边是“手动的”或“逻辑上的”,并不由自动流程得出。
为了解决这个问题,我们准备在自动边连接完成后,额外插入一批手动定义的边,来强制某些对象之间的排序依赖。
考虑到这类“手动边”的数量应该是非常有限的,因此不适合像普通边那样参与每帧成千上万次的图构建流程。最合理的做法是:
- 在自动图结构完成后,追加处理这些“手动边”;
- 对每个需要手动排序的条目,查找其在图中的节点ID;
- 然后用一个简单的
for
循环,将这些边逐个加入图结构中; - 每条边代表的是:A 必须在 B 上方渲染,即 B → A 的图边。
这个思路的优点在于:
- 不影响默认自动排序流程;
- 手动边数量很少,不影响性能;
- 插入顺序可控,逻辑清晰;
- 可以与已有的图结构合并,不需要大改结构。
目前来看,这种方式就是比较干净、灵活、低成本的做法,能够在图构建的末尾自然插入需要的手动逻辑,从而解决“手动指定渲染顺序”的问题。我们可以继续往这个方向推进,配合前面所说的 reserved sort key
数据,在图生成阶段完成整合。
考虑简化方案
我们现在重新审视手动排序机制,开始意识到之前设想的处理方式可能过于复杂。其实可能不需要建立额外的表结构、图边列表或复杂的引用机制,可能存在一个更加直接、自然的方案。
当前我们已经有了两个关键对象:sprite bound A
和 sprite bound B
。如果我们只是希望表达“A 必须排在 B 的上面渲染”,或反之,其实只需要在排序数据中显式声明这个关系即可。
由此我们开始设想一种更简单的实现方式:
简化方案核心思路:
我们可以在每个 SortSpriteBound
结构中添加一个新的字段,比如 manualSortAfter
或者 manualSortBehind
,这个字段可以是一个指针、索引或者是某种轻量化的引用,指向另一个 SortSpriteBound
,表示当前这个元素应该排在它的前面或后面。
例如:
struct SortSpriteBound {...SortSpriteBound* manualSortBehind; // 或者是 ID 或 offset
};
这表示:当前对象应该在 manualSortBehind
指向的对象之后渲染(即 manualSortBehind
会被先渲染,自己后渲染,也就是自己在视觉上覆盖它)。
这样做的优势:
-
非常直观
不再需要维护一整套手动边结构或额外表,只需设置一个引用即可。 -
性能无负担
只在真正有特殊需求的对象上设置该字段,对常规对象完全没有影响,不增加任何处理负担。 -
集成简单
不需要调整渲染图的构建过程,只需在排序比较函数中读取这个字段并适当处理即可。 -
拓展性强
后续如果有多个依赖(例如 A 要在 B 和 C 上方),我们也可以扩展成列表或结构体。
可能的处理方式:
在排序阶段中加入判断:
if (a.manualSortBehind == &b) {return a应该排在b之后;
}
或者在比较排序函数中,优先考虑这种明确指定的“依赖”,再回退到普通的 z 值排序等其他标准。
总结:
我们无需再额外维护一个图结构、边表或关联表,我们完全可以将“谁应该排在谁前面”这个信息,直接体现在 sprite bound
自身的数据结构上。这个方式不仅代码量少、逻辑清晰,而且极易实现和调试,不容易出错。
这是一个极大简化的方案,相比之前设想的复杂机制,简单直观得多,也许这正是我们应该采用的方式。
Blackboard(黑板):将某些内容放入 SortKey 以标记为 AlwaysInFrontOf 或 AlwaysBehind
我们将在渲染过程中只在头部和身体发生重叠的情况下进行排序判断,如果它们根本没有重叠,那就完全不需要考虑谁在谁前面的问题,因为它们不会互相遮挡,也就没有必要计算它们的相对渲染顺序。
基于这个前提,可以得出一个明确的优化策略:
优化判断的基础前提:
- 只有当两个元素 发生空间重叠 时,我们才需要判断它们之间的渲染优先级(即谁在谁前面)。
- 所以我们在判断是否“在前面”的逻辑(如
is_in_front_of
)里,只会针对那些彼此可能产生视觉遮挡的对象进行处理。
核心思路:在排序键中标记前后关系
我们已经在使用 sort key(排序键)机制决定渲染顺序,现在可以考虑在排序键结构中加入一种标记机制,用来显式表达“我必须永远在另一个对象前面或后面”的意图。
这种方式包括:
- 在
sort_key
中添加一个字段,类似always_in_front_of_code
或sort_relation_tag
。 - 在两个具有重叠的元素都被送入排序阶段后,通过匹配这些标记进行强制排序。
比如:
struct SortKey {...uint8 sortRelationTag; // 可以是 small integer,比如 1 表示该对象属于组1uint8 sortRelationRule; // 例如 0: 无强制关系,1: 在组内优先,2: 在组内最后
};
判断逻辑举例:
当两个对象需要被排序,且它们的 bounding box 相交,我们进入 is_in_front_of(a, b)
函数时,如果它们的 sortRelationTag
相等,就可以参考 sortRelationRule
来直接判定前后顺序。
例如:
if (a.sortRelationTag == b.sortRelationTag) {// 按规则比较if (a.sortRelationRule == 1 && b.sortRelationRule == 2) return true; // a 在前if (a.sortRelationRule == 2 && b.sortRelationRule == 1) return false; // b 在前
}
这一方案的优点:
-
低开销、高效率
只在真正重叠的情况下介入排序逻辑,不会拖慢正常流程。 -
灵活可配置
可以随时在需要指定前后关系的对象之间启用,不干扰其他对象。 -
结构清晰,易于实现
不需要引入额外的数据结构或依赖链分析,只是对已有的排序键进行补充。 -
良好兼容性
保留默认排序逻辑,仅在有明确需求时介入,方便渐进集成。
总结:
我们可以将“谁永远在谁前面”这种强制关系直接编码在 sort_key
里,并在 is_in_front_of
比较阶段通过标记进行处理。这个方法利用现有的结构,保持了系统的简洁性,同时大大增强了控制力,避免了不必要的复杂度,是一种更为自然和高效的解决方案。
game_render.h:向 sort_sprite_bound 结构添加 AlwaysInFrontOf 和 AlwaysBehind
实际上,我们可以使用一个极其简单的版本来实现这个“谁应该永远在谁前面”的逻辑。
核心机制:使用两个 uint16
字段表示强制前后关系
我们设想在排序键(sort key
)结构中加入两个字段:
always_in_front_of
(永远在某个对象前)always_behind
(永远在某个对象后)
这两个字段都可以是 uint16
类型,只要值匹配,就可以强制决定渲染顺序。
基本逻辑:
假设我们有两个对象 A 和 B:
- 如果 A 的
always_in_front_of
字段值 = B 的always_behind
字段值
→ 那么 A 应该永远在 B 前面
在 is_in_front_of(a, b)
判断函数中,我们只需要加上如下逻辑:
if (a.always_in_front_of != 0 && a.always_in_front_of == b.always_behind) {return true; // A 在 B 前面
}
if (b.always_in_front_of != 0 && b.always_in_front_of == a.always_behind) {return false; // B 在 A 前面
}
使用方式:
-
在需要指定前后关系的对象中,分配一个匹配编号(如 1001)。
-
比如:
- 头部的 sort key 中:
always_in_front_of = 1001
- 身体的 sort key 中:
always_behind = 1001
- 头部的 sort key 中:
-
其他未设置该编号的对象不会受影响。
优势:
- 实现极简:只需在已有结构中加两个
uint16
字段,无需其他复杂依赖。 - 不干扰正常排序逻辑:只有匹配到时才强制介入,否则按照正常顺序继续执行。
- 非常明确:通过显式匹配表达“谁必须在谁前”。
- 非常高效:完全避免了复杂的图结构或排序链条插入,仅在比较阶段判定即可。
- 完全可控:编号可以在对象生成时手动或自动指定。
总结:
我们可以用两个简洁的 uint16
字段实现一种“明确指示性”的前后排序机制,只要 always_in_front_of
和 always_behind
匹配,就可以直接确定渲染顺序。这种方式直观、清晰、易实现,不引入任何额外的复杂性,非常适合在需要对对象进行特殊排序控制的场景下使用。
game_render.cpp:使 IsInFrontOf() 检查 sprite_bound A 是否 AlwaysInFrontOf 或 AlwaysBehind B,从而避免手动指定边
我们现在的思路是,利用两个字段——“always in front of”和“always behind”来控制渲染顺序,这两个字段的值是一个数字标识,最多可以有六万五千多个不同的配对排序规则。
具体做法是:
-
在排序逻辑中,如果“always in front of”的值大于零,并且一个对象的“always in front of”字段值等于另一个对象的“always behind”字段值,那么就能确定前后关系。
-
这种方式非常简单直接,不需要复杂的手动添加边或者构建复杂的排序图。
-
在渲染时,只要给对象的排序键里分别设置“always in front of”和“always behind”的值,比如头部设置为“always in front of”某个代码,身体对应设置“always behind”同样的代码,就能实现头永远在身体前面的效果。
-
这个方案比之前构思的复杂系统简单许多,灵活性可能稍微低一点,但节省了大量复杂性,维护起来也更方便。
-
这样只需要在排序阶段添加简单的判断逻辑,既满足了特殊排序的需求,也保证整体性能和代码简洁。
-
目前计划在平台层面的排序代码中加入对这两个字段的支持,使其成为排序的基础部分。
总结来说,这是一种高效且直观的解决方案,通过标记“永远在前”和“永远在后”的配对关系,实现特殊渲染顺序的强制控制,避免了复杂的图结构和手动排序,显著简化了实现过程。
game_platform.h:向 game_render_commands 结构添加 LastUsedManualSortKey
我们设计了一个“手动排序键”的机制,用于管理和分配排序键的使用。具体来说,这个机制包含以下内容:
-
“手动排序键”用于标识特定的排序优先级,能控制某些元素在渲染顺序中的位置。
-
有一个“下一个可用手动排序键”指针,用来跟踪当前可分配的排序键值,确保不会重复使用已经分配的键。
-
还有一个“最后使用的手动排序键”记录,帮助管理和更新排序键的分配进度。
通过这样的机制,可以有序地分配和管理手动排序键,确保排序的准确性和高效性,同时避免排序键冲突或混乱。这为后续的排序控制和渲染顺序管理提供了基础支持。
game_render_group.cpp:引入 ReserveSortKey()
我们设计了一个非常简单的函数“预留排序键”(reserve sort key),它的逻辑如下:
-
该函数不会执行复杂操作,主要是进行一些断言检查,确保排序键的分配安全。
-
具体来说,会断言“最后使用的手动排序键”小于一个最大值(这里设定为16),保证不会溢出。
-
排序键从0开始,0表示不启用复杂排序功能。
-
每次分配排序键时,使用“最后使用的手动排序键”的后继值作为当前分配的键值,保证分配的排序键递增且唯一。
-
这样,首次分配的排序键是1,后续递增,保证了有序管理。
-
这个简单机制确保了在限制范围内,能够安全分配排序键,避免排序冲突和溢出。
-
之后会将这个机制结合“总是在前”和“总是在后”的标签一起使用,方便标记渲染顺序关系。
-
最后,通过修改对应代码,使得排序逻辑和排序键分配整合,确保整个流程能够正确运行。
整体来说,这是一个轻量且安全的排序键分配方案,支持排序顺序管理并保证代码的稳定性。
game_entity.cpp:使 UpdateAndRenderEntities() 设置 AlwaysInFrontOf 和 AlwaysBehind
现在一切准备就绪,只需要把预留的排序键值实际传递到渲染流程中去。我们计划在进行“大批量映射推送”(push big maps)时,确保这些排序信息能够正确流动。
具体做法是:
-
利用实体的变换信息(entity transform)作为承载排序键的合适位置,因为这是渲染组传递数据的关键部分。
-
在实体变换(object transform)结构中增加两个字段,分别存储“总是在前”(always in front of)和“总是在后”(always behind)的排序信息。
-
这样一来,排序信息就成为实体变换数据的一部分,随着渲染组传递下去。
-
在实际拷贝到排序键的时候,直接从这两个字段读取对应的“总是在前”和“总是在后”的标记。
-
这两个字段形成一对,总是相互对应,方便在排序过程中做匹配和判断。
-
从设计上看,这两个字段应该被捆绑在一起管理,以保证数据一致性和逻辑清晰。
总结而言,就是通过在实体变换数据中携带排序的“总前后”标记,使得渲染时能正确识别和使用这些排序关系,保证渲染顺序符合预期。
game_render.h:引入 manual_sort_key 结构
我们考虑到排序信息的存储和传递,觉得可能应该设计成类似“手动排序对”(manual sort pair)或者“手动排序加滑动键”(manual sort slip key)这样的结构。这样能更好地组织和管理排序相关的数据。
另外,在渲染部分,可能不直接用之前设计的字段,而是用一个类似“抑制标志”(inhibit)或类似的控制字段,来管理排序行为,确保在渲染组(render group)内部处理实体变换时,能正确应用排序规则。
在渲染组处理实体变换数据时,注意到这里面有个默认的排序字段或者默认值,不太清楚它具体起什么作用,但这个默认值可能是排序流程中的一个基准或备选项。
整体上,就是在设计排序信息的存储结构时,要考虑到它的组织形式,比如成对存在或者带滑动键,方便渲染组在处理实体变换时使用,同时注意默认值的作用,确保排序流程顺畅且有一定的容错和默认行为。
game_render_group.cpp:删除 ComputeSortKey(),使 GetBoundFor() 将 ObjectTransform.ManualSort 复制到 SpriteBound.Manual
我们目前的逻辑是:在某些情况下,值会逐渐接近零,这是我们期望的行为。
在设置排序键的过程中,特别是当我们调用 compute_sort_key
或类似函数时,我们会对排序键进行计算。这其中有一个步骤是创建 sprite_bounds
,而现在可以在这个过程中,将 object_transform
中的手动排序信息(manual sort)直接复制过来:
sprite_bound.manual_sort = object_transform.manual_sort;
这样,在 sprite_bound
中也就拥有了正确的手动排序信息。这使得我们可以使用这个信息来影响最终渲染的顺序。
我们还注意到可能存在一个叫 sort_bias
的字段,但现在我们已经有了更清晰、结构化的手动排序字段,因此可以考虑将原来的 sort_bias
删除,而用新的 manual_sort_key
来取代它。这有助于代码结构的简化和一致性。
此时,手动排序功能基本上已经可以正常工作,因为排序信息已经被正确传递和设置。
接下来需要检查代码中是否还有遗漏或错误。发现当前定义的某些数据可能范围过大,需要确认这些字段大小是否合适。另外,为了让这些排序信息在系统中被所有模块可见,可能需要将其移动到作用域更广的位置,例如将其放到一个公共的头文件或者共享结构体中,以便其他模块可以访问和使用。
整体来说,我们实现了一个简单但有效的手动排序机制,只需要:
- 在
object_transform
中定义manual_sort
字段。 - 在计算
sprite_bounds
时将其复制进去。 - 在排序逻辑中使用该值进行判断和排序。
这一过程简洁高效,大大减少了原本冗长复杂的排序依赖逻辑。现在只剩下最后的代码整理与验证步骤。
game_platform.h:从 game_render.h 移动 manual_sort_key 结构,并进行语言上的简短吐槽
我们非常不喜欢 C 和 C++ 中的这一点,这是最糟糕的设计之一。在其他语言中,也许有些也存在类似问题,但至少不像这里这么糟糕。令人沮丧的是,在这些语言中,开发者需要在意东西放在哪里,而文件的存在本应是为了提升代码可读性和组织性,为人服务,而不是为了让编译器工作得更顺利。
比如,现在在实现排序键(sort key)相关功能时,我们希望能把这个结构体或变量放在语言表达上最合理的位置上——也就是说,从逻辑和组织的角度,哪里放着最方便阅读、最合理,我们就希望放在哪里。然而实际情况并不是这样,我们必须将它放在编译器能识别和接受的位置,这极大地限制了结构设计的自由。
不过,经过前面一系列逻辑调整,目前手动排序功能已经基本完成,现在只剩下一点收尾工作:在特定情况下设置 manual
这一字段。实现上其实已经非常清晰简单,代码逻辑也很直观,但因为 C/C++ 的限制,我们仍然得处理一些令人烦躁的细节,比如头文件的位置、作用域的组织、可见性的设置等问题。
总的来说,这种人为的语言机制障碍拖慢了开发节奏,并没有给设计带来任何实际好处。这种问题正是我们在进行系统设计时经常不得不“妥协”的部分。尽管如此,我们还是逐步推进完成了所需功能的集成。
运行游戏,查看效果
现在理论上,这些对象已经被正确地标记了,虽然我们刚刚做了一系列变更,可能还存在一些细节没有完全处理到位。但总的来说,当前应该已经具备了基本功能。下一步就是进入调试阶段。
我们尝试通过调试来验证当前的排序逻辑是否按照预期工作。尤其是,我们希望首先能够复现错误排序的情况,这样才能观察并确认修复是否有效。如果我们能看到渲染顺序错误的情况发生,那就说明验证机制是生效的。
当前目标是:即便在某些特定情境下,例如空间位置或其他条件会导致渲染顺序颠倒,我们也要能够“强制”某个部分(例如角色的头部)始终渲染在另一个部分(如身体)之上。这正是我们新增的“手动排序”机制要解决的问题。
这种机制的核心在于添加了一个明确的、可配置的前后优先级逻辑:无论一般的排序计算如何,只要有了 always_in_front_of
和 always_behind
这组标签,渲染系统就能够识别出这些“手动设定”的渲染顺序,并以此为准,从而避免不正确的遮挡。
因此,我们的重点就是通过调试确认:在有手动优先级指定的情况下,渲染顺序能否始终保持一致,即角色头部是否无论何种位置关系都始终渲染在最上层。这正是我们这整套改动的最终目标。
调试器:在 ExecuteBrain() 处断点,查看是否设置了 SortKey
目前我们正在调试一个手动排序系统,目的是确保某些游戏实体(例如角色的头部和身体)在渲染时遵循特定的显示顺序,即使它们的空间位置可能导致自动排序顺序出错。
首先检查了 game_entity
或者更准确地说是 game_brain
中是否正确设置了手动排序键值,这是整个流程的第一步。在代码中,当角色拥有“头部”和“身体”两个部分时,会调用 reserve_sort_key
来分配排序编号。由于调用者唯一,因此首次分配的排序键值应该是 1,经确认确实如此。
随后进入实际的排序逻辑,目的是验证手动排序是否被正确触发。尝试查看 is_in_front_of
这个排序比较函数是否有命中,但发现并没有进入这段逻辑,也就是说当前运行过程中并未触发手动排序规则。
接下来通过查看是否正确设置和使用了 always_in_front_of
与 always_behind
两个手动标记键来进一步调试。当前的初步结论是,虽然排序键值被成功分配,但在排序逻辑中并未被触发,可能存在以下几种情况:
- 排序时未读取或未使用
manual_sort_key
。 - 设置排序键的代码逻辑路径未覆盖当前测试实体。
- 渲染流程中未正确传递或赋值
manual_sort_key
至sort_key
中。
此时还提到曾尝试使用 Live Code Reloading(热重载)作为调试手段,但意识到这可能并不适合此类状态验证,决定回退使用传统调试方式。
下一步将是检查排序键的使用是否覆盖到 compute_sort_key
或类似函数,确保手动设置的排序优先级被实际用于排序判断中。调试流程进入验证“是否用到了设定值”的阶段,目的是最终确保在视觉上观察到的渲染顺序符合预期,尤其是在头部与身体之间的层级关系。
断点没进来
game_entity.cpp:在 UpdateAndRenderEntities() 中当 EntityTransform 不是 AlwaysInFrontOf 或 AlwaysBehind 时添加断点
在处理实体(entity)代码时,我们设置了 entity_transform.manual_sort
。为了便于调试,我们添加了一个判断逻辑:如果 manual_sort.always_behind
不等于零,就进入特定的处理流程。这个判断的目的是为了精确地筛选出那些手动设置了排序的实体——理论上只有两个实体符合这一条件,例如角色的“头部”和“身体”。
通过这种方式,我们能够聚焦到确实使用了手动排序键的对象,方便验证是否按照预期将其写入并传递到渲染系统中。下一步是进入实体更新循环(entity loop),继续观察这些实体在更新和渲染过程中是否维持了正确的 manual_sort
值,并进一步验证手动排序逻辑是否已经开始生效。这个调试流程是手动排序系统验证的重要步骤,确保排序信息确实从设置阶段一直传递到了最终渲染阶段,且能正确地影响渲染顺序。
调试器:断点处检查值
我们已经确认,目前确实成功将手动排序相关的数据传递了下去。
首先,manual_sort
中的 always_in_front_of
被正确设置为 1。从上下文判断,这是角色的“头部”部分。我们检查了对应的实体,brain_slot
是 0,brain_id
是 1,类型和索引都是 0,确认这是头部实体。因此,这部分设置是正确的。
接着我们观察渲染时的流程。在调用 PushBitmap
时,我们检查了实际传入的参数,包括尺寸设置、渲染元素的构建过程。最终,确认 sort_key
已经正确附加到了渲染元素中。
不仅如此,我们还检查了“身体”实体,对应的 manual_sort.always_behind
同样被正确设置为 1。再次通过 PushBitmap
观察,验证它的渲染流程也包含了正确的排序信息。
因此,我们已经可以明确地得出结论:
- “头部”和“身体”两个实体的手动排序信息都已经成功设置;
- 这些排序信息通过实体逻辑传递到了
object_transform
; - 渲染逻辑中成功读取了这些信息,并附加到
sprite_bound.sort_key
上; - 渲染系统的输入数据(渲染元素)具备了正确的手动排序状态。
理论上,这些信息已经足以让排序逻辑正常判断“谁应该在谁前面”。接下来,只需验证排序判断函数在运行时是否真正使用了这些信息,并根据它们做出正确的排序决策。也就是说,在进入 is_in_front_of()
判断时,是否真的匹配到了 always_in_front_of == always_behind
的情况,并让头部在渲染上总是显示在身体之上。
game_render.cpp:修正 IsInFrontOf() 中的拼写错误
看起来目前的问题可能只是一个拼写错误,而这可能是目前唯一存在的 bug。
我们在检查排序逻辑时,发现原本应该是 always_behind
的地方拼错了。如果这个拼写错误是导致手动排序失效的唯一原因,那就意味着整体设计和实现流程本身是正确的,逻辑是严谨的,只是因为细节处的笔误导致排序关系没有正确生效。
一旦这个错误修复:
- 系统就能正确识别哪些元素应该“总是在前”或“总是在后”;
is_in_front_of()
函数能够准确匹配always_in_front_of == always_behind
的情况;- 渲染系统将实现预期效果:例如在角色的头部和身体重叠时,头部始终显示在身体上方;
- 整个手动排序机制无需进一步复杂化即可满足当前需求。
如果真的是这样,那这个问题的解决将非常高效,调试过程也已接近尾声。接下来只需确认修复拼写错误后,渲染排序是否按预期运行即可。这样我们就完成了对整个手动排序机制的构建、传递、应用以及调试的完整闭环。
运行游戏,发现这不是唯一的错误
我们原以为只是一个拼写错误导致问题,但实际情况并非如此。修复拼写后问题依旧存在,说明还有其他 bug。
game_render.cpp:使 IsInFrontOf() 分别且正确地检查 AlwaysInFrontOf 和 AlwaysBehind
我们在排序逻辑中深入检查了“始终在前(always_in_front_of)”和“始终在后(always_behind)”的判断条件,发现之前的实现存在一些遗漏。
主要问题在于:比较两个渲染实体时,只判断了单方向的“始终在前”关系,忽略了另一方向的情况。为确保逻辑完整,必须在比较中双向判断:
- 如果实体A标记为“始终在前”于实体B,则A应排在B前;
- 同理,如果实体B标记为“始终在前”于实体A,则B应排在A前;
- 所以必须在排序时判断两者之间是否互相关联。
此外还考虑了一种情况:如果双方没有任何排序标志,不应因默认值相同(如都为0)而意外进入上述逻辑导致误判。因此,我们尝试了一种方式:
-
将默认未设定状态用特殊数值表示,例如:
always_in_front_of = 0
always_behind = UINT16_MAX
- 这样永远不会匹配到错误的 ID,避免不必要的排序判断。
尽管这种方式可以有效防止误匹配,但我们权衡之后还是倾向于保留默认清零行为(0初始化),因为统一的初始化策略可以在其他情况下减少 bug 的产生。
最终,我们决定暂时保留原来的0初始化方案,并在排序判断逻辑中加入完整的双向判断逻辑,以确保在有设置手动排序关系时,判断是可靠和对称的。
接下来将继续观察该修改是否解决了排序问题,并在需要时再尝试其他更加严谨的默认值设计方案。当前重点是验证现有数据流是否在排序中生效。
运行游戏,情况有所改善,但仍不完美
目前整体情况有所改善,核心排序逻辑已经生效,先前的主要 Bug 也成功修复,排序系统能够正确处理“始终在前”和“始终在后”的手动排序标志。不过,在进一步测试过程中仍观察到少量异常现象,主要集中在以下几点:
-
Bug 修复成功:目前系统可以在正确条件下将应当在前的对象(例如角色头部)渲染在应当在后的对象(例如身体)之前。也就是说,
always_in_front_of
和always_behind
的手动排序机制正在生效。 -
小范围的视觉异常仍然存在:在某些特定时刻(例如渲染对象处于移动过渡状态时),出现了渲染顺序错误的“闪烁”或“跳动”现象。这种现象并不持续,而是瞬时性的,且仅在某些状态下触发。
-
可能的原因初步分析如下:
- 渲染边界(sprite bounds)数据不精确,导致在排序计算过程中位置误差,从而影响最终 Z-Order 判断;
- 排序图(sort graph)在某些边界状态下未能准确评估依赖关系,导致瞬时排序失效;
- 图元在动画或移动中存在插值帧误差,导致实际视觉上表现出跳变;
- 某些实体之间虽然设置了“始终在前/后”关系,但它们在排序中未严格对齐或重叠,因此触发了模糊状态。
-
下一步修复思路:
- 检查并修正 Sprite 的边界计算,确保在 Push 渲染时提供了精确的尺寸和对齐信息;
- 明确判断所有可能被比较的实体是否总是具有实际的重叠区域,否则判断前后关系无意义;
- 可能需要引入额外调试信息以可视化排序图,便于追踪排序依赖链是否中断或出现循环;
- 若使用动态排序数据结构(如有向无环图 DAG)则需检查是否存在异常路径跳过手动排序规则;
- 评估是否可以对关键对象的排序逻辑进行锚点处理,强制它们在逻辑帧内稳定排序。
-
当前总结:
- 手动排序机制已搭建完成并发挥作用;
- 数据流向正确,排序键的设定和传递无误;
- 排序边缘问题仍需修复,当前可能集中在边界或动画状态下的微妙错误;
- 后续应优先对 Sprite 边界(bounds)进行验证和调试,确保排序在视觉和逻辑上完全一致。
目前系统基本框架稳固,排序系统已具备实际可用性,后续主要是细节与视觉一致性的修正。
开启 GlobalShowSortGroups,发现排序错误时存在循环依赖
目前发现排序问题表现得非常不稳定,无法稳定复现预期的错误,说明代码中存在某些隐藏的 bug,但具体原因不完全如预期。虽然错误现象确实存在,但它的触发条件和表现方式都较为模糊,不容易通过直接测试稳定复现。
主要观察和分析如下:
-
排序错误现象不一致:在大量元素同时渲染时,排序出错的情况偶尔发生,表现为部分对象被错误排序,顺序混乱。
-
排序循环(Cycle)问题:通过调试发现,当渲染对象形成排序依赖循环时,会触发错误排序。排序循环指的是对象 A 依赖于对象 B,B 又依赖于 A,导致排序逻辑无法正确确定顺序。
-
循环检测机制的作用:循环检测功能有效,当检测到排序循环时,系统会标记出问题。这说明排序出错往往和循环依赖有关。
-
排序循环影响排序结果:当不存在排序循环时,排序表现正常;但一旦进入循环,排序逻辑就会失败,导致渲染顺序错误。
-
后续解决方案思路:
- 先暂时搁置排序循环问题,集中处理精确的 Sprite 边界和数据;
- 继续完善排序数据,确保依赖关系清晰且无环;
- 未来引入更健壮的循环检测和打破循环的策略,避免排序陷入死循环;
- 使用更真实的 Sprite 资源测试,观察排序循环发生频率和具体场景;
- 细化排序规则,尽可能减少对象间循环依赖。
总结来看,排序错乱的根源与依赖关系中的循环有关,虽然排序框架基本正常,但循环问题是导致排序失败的关键。接下来要聚焦数据正确性和循环检测机制的完善,逐步排除循环带来的渲染错误。
问答环节
也许在前后关系对上打破循环?
关键点在于,永远不能打破“始终在前”和“始终在后”这对排序关系中的循环。这个排序关系必须始终保持完整,不能在这里打破循环,否则排序逻辑就会出现问题。这部分的排序规则必须被严格遵守,不能随意破坏。
另外,这天在游戏开发中比较平静,没有出现新的问题或提问。有人提出了一个不相关的问题,但总体上讨论还是专注于排序逻辑和循环检测的问题上。
会不会有角色面向改变时影响排序的情况?例如角色有手臂的情况。当前方法是否考虑到这些?
角色的朝向是否会影响排序顺序?确实可能会,比如角色有手臂的情况。当前的方法是由“脑”(brain)来控制和指定排序规则的,这个“脑”掌握了所有信息,包括角色的朝向、头部和身体是否连接、是否戴手套等等。所以排序规则可以根据游戏状态动态调整,拥有完全的灵活性,而不需要硬编码固定的排序方式。这样可以根据实际游戏中的各种状态切换强制排序规则,实现更智能、更准确的排序。
期待重新观看前面简化部分,我没完全跟上
一开始误以为需要在渲染阶段传递排序图中的边的信息,但后来意识到这是错误的思路。正确的做法是在排序时通过“是否在前面”(is in front of)这个比较函数来生成排序图的边。也就是说,只需修改这个比较测试,让它结合特殊的配对信息调整排序键,这样在比较两个元素时,如果它们需要特定的排序顺序,就能保证排序结果正确。换句话说,强制“是否在前面”的比较逻辑来完成排序边的生成,而不是试图直接传递边信息。这样更简单有效。
我是蒙特利尔一家游戏开发工作室的传播主管,刚刚发现您的直播,非常吸引人。您的预告片传达了游戏“完全手工制作”更优秀的理念。您是否认为使用现成引擎做游戏就不能投入同样的“爱”?您是否觉得重新造轮子是一种浪费?
关于用从零开始还是使用现成引擎制作游戏的爱之所在
这个问题可以分成两部分。第一部分是关于预告片传达的观点:游戏如果完全从零开始制作会更好;第二部分是关于使用现成引擎是否能投入同样多的热爱。
我们的看法是,不认为使用现成引擎的游戏就不能充满热爱。制作游戏的方式本身,并不决定开发者投入了多少热情。如果开发的主要目标是赚钱,而非热爱创作,游戏自然缺少爱。但这和使用什么引擎关系不大。外部因素可能影响热爱的投入,但使用自制引擎还是现成引擎,本质上不决定热情多少。
不过,有一个相关的现象是:如果选择从零开始做游戏,意味着承担更多工作量,通常需要更强的热情和坚持,才能完成。这种情况下,游戏里往往会体现出更多的爱。相反,使用现成引擎虽然方便快捷,但你无法直接从开发方式判断背后的热情程度,可能有也可能没有。
在现实中,这就像烹饪:自己从头准备晚餐,通常会投入更多的用心和爱;而微波炉加热,虽然方便,但很难体现同样的用心。当然,这个比喻不能过度套用,因为即使用现成引擎,制作美术资源、设计游戏内容同样需要大量心血,也同样可以体现劳动的热爱。
总结来说,从零开始制作游戏往往更能反映开发者的热情,因为这是更艰难的选择;但使用现成引擎并不排除对游戏的热爱,很多成功且有爱的游戏也是基于现成引擎开发的。关键还是看开发者对项目的态度和投入,而不是技术选型本身。
关于重新造轮子和教育责任
这个问题其实可以拆成两个方面来回答。首先,这个直播是一个教育性质的节目,目的是展示如何从零开始手写游戏的每个部分,而不是追求效率或者商业利益最大化。所以我们不会在每一步都停下来考虑用现成的代码或者买现成的工具是否更有效率,因为教学的重点就是手把手教大家游戏开发的各个环节。这本身就排除了使用别人已有的东西,因为要做到真正的教育,必须自己一步步实现,这样观众才能学到真正的技术和思考方法。
其次,从更广的角度看,虽然很多人觉得重新造轮子浪费时间,但实际上如果没有人去做底层的引擎开发和核心技术研究,未来就没有人能够维护或者开发新的游戏引擎。游戏引擎需要专门的团队维护,目前全球有几十家公司负责重要的引擎,这些团队背后有成千上万的程序员。他们的存在是游戏产业赖以运行的基础。如果大家都只用现成工具,不学习底层技术,未来几十年可能就没人懂这些核心技术了,整个行业的基础会变得脆弱。
再者,即使最终不打算自己写引擎,了解底层原理依然极其重要。使用别人引擎的时候,如果完全不知道这些系统如何运作,只会被限制在教程里教的内容和工具默认的功能范围内,很多问题解决不了,也不能最大化利用工具。了解底层原理会让开发者更有能力应对复杂情况,甚至能自己绕过限制或修复问题。特别是像Unreal这样开源的引擎,拥有源代码可以深入修改和优化,有这些知识储备的开发者无疑会更强大。
综上,虽然教学和重新造轮子在效率上看似不划算,但从教育和行业长远发展来看,这样做是非常有价值和必要的。掌握底层技术不仅能保证未来游戏引擎的持续发展,也能让使用现成引擎的开发者发挥出更大的潜力和创造力。
关于“有人发明了轮子”的假设
第二部分的回答,假设这不是一个教育性的项目,而是纯粹讨论是否觉得“重新造轮子”是浪费时间,答案是——在某种程度上觉得是,但这背后有个重要的假设。
这个假设是,问题中隐含了“有人已经造出了轮子”的前提。换句话说,提问者把现在能下载或买到的游戏技术,和人类历史上最基础且经过反复打磨完善的发明之一——轮子——相提并论,认为已经达到了某种效率和优雅的极致。
但是,我们并没有真正达到那样的完美状态,游戏技术还远没有达到一种终极的、高度优化且完善的地步,所以“重新造轮子”这件事情,其实还存在很大的空间和必要性。
因此,不能简单地把游戏开发的现状等同于一个已经完善无缺的轮子,而忽视了持续创新和改进的重要性。
Blackboard(黑板):游戏产业目前在发明轮子方面的状况
如果用一个比喻来说明这个问题,那就是有人拿着一堆看起来还不错的轮子跟我们说:“为什么还要重新造轮子?这些轮子已经很棒了,看看它们多棒!”然而现实情况是,如果你经常在坑洼不平的路上行驶,这些轮子其实并不够好。
从更实际的角度来看,现在游戏开发的现状是,没有哪个游戏引擎真正达到了“轮子”的标准。Unity、Unreal、Source这些引擎,离一个真正完美的引擎还很远,它们甚至还做不到理想中的效果。
所以现在从零开始做一个组件、一个完整引擎,其实不是“重新造轮子”,而是在尝试“发明轮子”。因为真正的轮子还没有被发明出来,没人真正做出那个理想中的轮子。
如果一直因为“不要重新造轮子”而放弃尝试,那么很可能永远也造不出真正的轮子。只有不断尝试,才有可能真正发明出那个完美的轮子。
“其实并没有轮子”β
最容易阻止自己发明轮子的方式,就是误以为自己已经发明了轮子,但实际上并没有。未来五十年,游戏开发不会只是启动Unity那么简单,它会远远超越现有技术,只要有足够的人持续努力,比如Unity、Epic和世界各地的开发团队都会不断推动技术进步。
游戏开发基本上分为两类:一类是推动技术前沿发展,这往往需要从零开始编程;另一类是制作特定的游戏,如果游戏本身不需要技术革新,就可以使用现成技术开发。我们更关注的是前者——作为游戏技术研究者,更热衷于开发那些现有工具无法实现的游戏技术,而不是仅仅使用现成工具做游戏。
这也是直播的初衷——分享对技术编程的热爱,鼓励那些也希望亲手构建东西、探索技术极限的人。直播不是为了说服那些已经很满意用Unity等工具做游戏的人改变选择,如果你开心用现成工具,那就继续用。但如果你渴望推动技术进步,直播就是为你准备的。
在“Game Hero”这个项目中,我们不会大幅推动当下游戏艺术或技术的极限,因为这是一个教育性质的直播。重点是展示高水平技术编程的过程,让大家理解如何从零开始搭建游戏引擎的每一个部分。这样,完成基础学习后,大家就有能力去探索更前沿的技术。
虽然不会直接带来革命性的引擎创新,但目的是打好基础,鼓励学习者成为技术探索者,去创造未来的新技术。根据社区的反馈,这个目标已经在逐步实现,这非常令人振奋。
整体来说,这就是对问题的完整回答,希望能帮助理解重新发明轮子和技术创新的重要性与意义。
关于爱与金钱
如果制作游戏的主要目标只是赚钱,那么游戏肯定不是出于热爱而做的。真正带着热情制作的游戏,意味着生活中最重要的目标就是做这款游戏。当然,这并不意味着可以不考虑赚钱,实际上为了生存仍然需要钱,所以游戏仍然会被卖出去,且会尽力确保游戏能赚钱。赚钱的目的是为了能够继续做自己热爱的事情,支持长期的创作和发展。
“你的目标必须是你想要钱是为了做游戏,而不是你做游戏是为了钱”γ
制作游戏的目标不能只是为了赚钱,钱只是为了支持继续制作游戏的手段,而不是制作游戏的最终目的。如果制作游戏的目的是赚钱,那么它就仅仅是一个商业产品,可能是什么都行,游戏只是形式而已,这样的作品里没有热爱,只有对金钱的追求。
过去曾经历过两种工作状态:一种是完全出于热情的项目,另一种则是纯粹为了赚钱的工作。能够以热爱的方式创作任何东西,甚至不仅限于游戏,都是一种奢侈和特权。因为人类首先要生存,制作游戏需要设备、时间和资金支持,设备如电脑本身就是一笔昂贵的开销。
所以能够以热爱为动力,且拥有经济能力去做自己喜欢的事情,是一种非常难得的荣幸和特权。虽然希望每个人都有机会追随内心,去做自己喜欢且富有艺术性的创作,但现实是大多数人无法轻易做到这一点。因此,能够身处这样的环境,既能自由创作又能保障经济支持,是极其珍贵和幸运的事情。