回顾并为今天的工作设定基调
我们正在继续昨天对代码所做的改动。我们已经完成了“脑代码(brain code)”的概念,它本质上是一种为实体构建的自组织控制器结构。现在我们要做的是把旧的控制逻辑迁移到这个新的结构中,并进一步测试和验证它是否真正满足我们的需求。
目前我们只实现了一个用例:主角角色(hero)。接下来我们需要为其他类型的实体也构建一些用例,看看这个架构是否能广泛适用。因为我们还不能确定它是否真的稳固,所以当前的目标就是反复打磨这个实体系统,把它调整到一个我们认为能满足大部分设计目标的状态。
我们打开了项目,确认“脑”系统的基础逻辑是正确的。比如朝向(facing direction)这些信息现在已经可以由脑模块统一控制,并且适用于多个实体,这是很酷的一点。
不过我们还没有完成对跳跃控制器(hop controller)的迁移。所以接下来的工作就是清理旧逻辑,确保:
- 该属于“脑”的代码都被移入脑中;
- 不应该属于“脑”的逻辑要从中移除。
这是一个组织结构上的优化,使系统更清晰、更符合职责分离的原则。
我们现在会查看 game_world_mode.cpp
,并继续完成代码的迁移和清理工作。
关于何时创建新文件的一些建议
在 game_world_mode.cpp
文件中,我们目前的代码大致可以分为三部分。接下来我们可能需要将这些部分进一步隔离和模块化,以提升代码的可读性和可维护性。
例如,我们可以将“脑”相关的逻辑全部拆分到一个独立的文件中,这样在查找或编辑脑相关功能时会更方便。同样地,渲染相关的代码等也可以单独拆出来,形成自己的模块。这种拆分能帮助我们更清楚地看到各自负责的部分。
很多人经常会问:“怎么判断该不该创建一个新文件?” 对我们来说,其实判断依据非常简单 —— 当我们希望同时查看两个逻辑区域的代码,但又不希望它们混在一起时,就可以把它们拆成两个文件。也就是说,这是出于可见性和组织性的考虑,是对当前开发环境和流程的一个顺应,与程序架构本身没有本质关系。
我们认为文件拆分只是代码可视化和导航的辅助工具,不是架构设计的核心。对于“文件该怎么拆”这种问题,其实并没有什么标准答案。完全是个人偏好,只要代码对自己或团队来说是清晰易用的,就可以了。
我们建议不要被“必须如何拆文件”这样的说法所限制。相反,如果有人告诉你拆文件有唯一正确的做法,我们会对此持怀疑态度。
基于这个逻辑,我们决定接下来创建一个新文件,把脑相关的逻辑提取进去。这么做是为了更清晰地管理功能区域,让我们在查看或维护代码时更加直观、简单。
# 创建 game_brain.h
和 game_brain.cpp
文件,并将脑相关代码从 game_entity.h
和 game_world_mode.cpp
中迁移过来
我们决定为“脑”(brain)模块创建一个新的文件。命名方面我们选择使用单数形式 brain
,即创建两个新文件:game_brain.h
和 game_brain.cpp
。接着,我们打开这些文件,并为其加上标准的头部注释,方便在使用版本控制系统时查看和管理。
接下来的任务是将与“脑”相关的逻辑从其他代码区域中提取出来,放入这个新的模块中。
起初,我们的脑相关代码位于 entity.h
和 world_mode.cpp
中。由于脑模块本身逻辑较为独立,基本上只依赖于实体(entity)的存在,且通常仅通过指针引用实体,因此它相对孤立,不会引起太多耦合或编译问题。
因此,我们可以将“脑”的定义和核心结构直接移动到 game_brain.h
中。然后将执行逻辑(比如针对不同脑类型的处理)迁移到 game_brain.cpp
。
为了确保这些新文件能在整个项目中被正确使用,我们需要在编译系统中包含它们。在主头文件中加入 #include "game_brain.h"
,并在项目源文件中添加 game_brain.cpp
,这样它们才会被编译和链接进游戏中。
迁移的第一步是从 world_mode.cpp
中定位我们之前实现的脑处理逻辑——也就是运行所有脑代码的部分(比如 RunAllBrains
或 ExecuteBrains
的调用处)。找到这些之后,我们可以将包含不同脑类型的 switch
语句整块拷贝出来,直接转移到 game_brain.cpp
中。
这样做的目的是让脑逻辑集中管理,模块职责更加清晰,便于未来维护、扩展和调试。例如,如果要新增一个脑类型,只需修改 game_brain.cpp
中的逻辑,而不需要触碰 world_mode.cpp
,这显著提升了系统的清晰度和可扩展性。
新建两个文件
先把brain 的三个结构体移动到game_brain.h中
再挨着检查game_entity.h 中报错的哪些需要移动到game_brain.h 中
把Brain->Type的switch 移到game_brain.cpp 中
在 game_brain.cpp
中引入 ExecuteBrain
函数
我们将脑逻辑进一步抽离,构建一个名为 ExecuteBrain
的函数。这个函数的职责是对单个脑实体执行更新逻辑。虽然目前这个函数只会在一个地方被调用,我们仍然选择将其提取到独立文件中,以便能更方便地与其他相关代码(例如具体实体的更新处理)同时查看,提高可读性和可维护性。
该函数的主要输入包括:
brain
:要被更新的脑对象。simRegion
:模拟区域,表示当前更新逻辑作用的物理或逻辑空间。gameInput
:游戏输入信息。gameState
:游戏全局状态。dt
:时间增量(delta time),用于按帧推进更新逻辑。
我们明确了这些依赖项之后,开始修改调用点。在主更新逻辑中,也就是之前位于 world_mode.cpp
中的逻辑,我们替换原先脑处理的 switch
调用为对 ExecuteBrain
的调用,并正确传入上述参数。
在这个过程中还考虑到一个优化点:理论上只有某些特定的脑类型(如玩家控制的角色)才需要访问输入控制器。也就是说,不是每种脑在执行时都需要 gameInput
。但我们决定不去特意做这种分支优化,因为这种判断逻辑带来的复杂度远大于其节省的一点性能,尤其是在当前阶段我们只希望把系统先构建清晰和稳定。
最后,我们再次强调,脑的执行逻辑只由这一函数统一处理,而该函数又被明确集中在独立模块中,使得所有与“脑”相关的行为和逻辑都聚合在一处,方便扩展与调试。这是我们当前逐步将实体系统构建得更加健壮与模块化的关键一步。
函数ExecuteBrain 声明放game_world_mode.h
在 game_world_mode.cpp
中考虑如何处理 HeroesExist
标志
我们遇到了一个未声明的标识符错误(undeclared identifier),这个错误本身很明确,确实是变量没有被声明。但真正的问题是我们要如何以更合理的方式处理这个逻辑。
之前代码中通过一个布尔值,比如 HeroesExist
,来判断是否继续维持当前世界模式(world mode),这种方式并不令人满意,显得很零散、不可靠。现在我们希望用一种更清晰、更结构化的方式来表示“世界模式是否应该退出”。
我们更倾向于引入一个明确的状态标识,比如 WorldModeExited
或 ExitToTitle
,来代替 HeroesExist
这样的临时性标志。这样当游戏状态需要切换回标题画面时,可以直接判断这个更语义化的标识。
在代码逻辑中,我们计划在每一帧逻辑执行结束时进行判断,比如:
if (WorldModeExited) {
GameMode = GameMode_TitleScreen;
}
这种方式更具扩展性,未来即使需要加入更多退出条件或者其他退出模式(比如暂停、存档退出等),也会更清晰易管。
同时我们也打算在代码中用更直观的命名,例如 ExitToTitle
,而不是模糊的 Exit
,这样代码含义更加明确。
总之,这一改动的核心是将原本“英雄是否存在”这种临时变量驱动的控制逻辑,转变为“世界模式退出”这种更具抽象语义的状态控制,使整个架构更稳健、清晰、易维护。
回到游戏逻辑中,原本我们通过设置 HeroesExist
这个布尔值为 true
来判断是否存在英雄(hero),以此作为是否继续世界模式的依据。但我们决定不再采用这种“死男人开关”(Deadman Switch)风格的处理方式。这种方式不够健壮,依赖外部状态判断并不是理想结构。
我们更倾向于用显式计数的方式来管理英雄的存在状态:记录当前有多少个英雄仍然存在于世界中。这可以通过在每个更新循环中检查各个脑(brain)是否是英雄类型来实现,并进行计数。
当某个英雄退出世界时(例如被删除、死亡等),我们就减少这个计数;相反,英雄进入时就增加。这样,如果在一帧中检测到英雄数量变为 0,就可以明确地知道需要切换到标题画面等其他状态,而不需要额外维护一个真假标志位。
这种基于数量的判断方式比原来的布尔逻辑更具弹性,也更容易扩展,比如未来引入多个英雄、多人游戏,或控制不同类型角色的状态时,不会出现混乱。总之,我们正在逐步把所有控制逻辑从临时标志转向更明确、结构化的状态管理机制,使系统更清晰、可维护性更强。
在 game_world_mode.cpp
中,通过遍历控制器设置 HeroesExist
标志
我们决定进一步简化判断“英雄是否存在”的方式。与其依赖某个布尔值标记,不如直接遍历当前所有的控制器,查看是否有任何一个控制器对应的英雄具有有效的脑(brain)。只要发现一个存在,就认为英雄依然存在。这个逻辑可以通过遍历 GameState.ControlledHeroes
来完成,如果任意一个条目的 BrainID
是有效的,就将 HeroesExist
设置为 true
,然后跳出循环。
我们还顺带调整了一些旧代码,像之前手动维护的控制器数量不再需要,可以通过数组长度 ArrayCount(GameState.ControlledHeroes)
来获取,增强了代码的通用性和安全性。
在完成了脑模块的独立封装之后,我们已经将所有实体 AI 的逻辑(例如英雄或怪物的行为控制)集中到了 brain 文件中。这个文件已经可以单独打开并处理逻辑,提升了代码结构的清晰度,也方便了调试和未来的扩展。
物理部分的逻辑则被保留在原来的世界模式(World Mode)模块中。我们先执行脑逻辑,为实体设置运动或行为参数,然后才进入实体本身的更新阶段,比如处理物理模拟。
在处理实体更新之后,我们还看到了目前的代码中有一部分是“后物理”处理部分,其实它做的全部工作就是负责绘制实体。这个部分是一个 switch
语句,针对不同的实体类型选择不同的渲染方式。
不过,这个渲染逻辑还不够理想,因为我们未来希望实体的形态更灵活,不再局限于固定的类型。例如一个实体可以因为状态变化而改变外形或动画,这就意味着我们不能再依赖硬编码的类型来决定绘图方式。
由于我们使用的是手绘风格,而不是完全程序生成图形的美术形式,因此不可能为每种可能的状态都准备一张独立的位图。这会给我们的绘图逻辑带来挑战:如何根据实体的属性推导出合适的绘图资源。
接下来我们会重点考虑如何实现实体“外观”的动态组合与渲染,比如通过组合、状态映射、标签系统或规则匹配机制,根据实体的实际属性选择最贴近的图像资源进行渲染。这将成为接下来的一个技术重点。
此外,我们也提到了可能删除 Updateable
这个标志位,因为当前的地图已采用“房间锁定”机制而非“自由滚动”,这让实体是否更新的判断变得更简单。将来可以进一步清理这类标志,让逻辑更加纯粹。
在 game_world_mode.cpp
中重组 UpdateAndRenderWorld
并清除多余代码
我们决定进一步清理和重构现有的游戏主循环代码,以提升结构清晰度和可维护性。之前在主循环中存在一些混合逻辑,尤其在模拟阶段和实体更新阶段之间的界限不清晰,使得我们很难准确判断每一部分的职责和作用。因此我们开始着手将其分离、简化,并理顺更新流程。
首先,我们识别出整体模拟逻辑是由几个阶段组成的:
- 脑(Brain)阶段:所有带“思考”逻辑的实体会在这一阶段执行 AI 或控制逻辑。例如怪物 AI 或玩家输入控制。
- 物理(Physics)阶段:统一对所有实体进行物理处理,比如速度应用、位移更新、碰撞检测等。
- 实体(Entity)阶段:这个阶段曾包含一些剩余处理逻辑,但我们发现其功能越来越趋向于绘制。
我们注意到所谓的“后物理实体处理(PostPhysicsEntityWork)”这一部分,其实已经完全变成了实体渲染代码,所有逻辑都是绘制相关。它通过一个 switch
语句根据实体类型选择对应的绘图操作,完全没有其他实体逻辑。这与函数名本身不符,因此我们决定将这部分明确重命名为渲染阶段,并清除掉误导性的注释和函数名。
我们也简化了脑和模拟阶段之间的标记注释,不再保留那些冗余的 timing block(计时代码段),因为当前我们更专注于结构化和逻辑理清,而非性能分析。
在进一步分析实体更新逻辑时,我们发现其结构是先判断实体类型,再处理该实体类型特有的逻辑,比如设置移动方式,随后再根据移动方式进行统一的处理。最后才是绘制实体的图形表示。这个逻辑虽然还算清晰,但放在一个函数中混杂使用了过多的 switch
语句,不利于未来扩展,因此这也成为我们接下来要拆分重构的目标。
整体来看,我们正在逐步朝着模块分离的方向进行改进,将不同阶段的逻辑拆成清晰的职责块:脑逻辑、物理模拟、图形渲染,三者各自独立,避免交叉污染。我们希望通过这种方式,使得代码既方便理解也更具可维护性,为后续添加新功能和调试打下坚实基础。
在 game_brain.cpp
中将(Planted mode)逻辑从 game_world_mode.cpp
移入
我们目前将重心集中在实体移动逻辑的整理与优化上,主要目的是简化并清除冗余结构,确保移动逻辑语义更加清晰、系统设计更合理。
首先,分析了当前实体的移动逻辑函数 MoveEntity
,这是所有实体在场景中发生移动时被调用的主要函数。我们注意到这里存在一个 movable
的布尔标志位,但我们开始怀疑这个标志的必要性。
之所以怀疑,是因为我们当前的移动系统已经引入了更具语义性的移动方式(movement mode),其中包括“静止”作为一种合法的状态。因此,如果一个实体的移动模式已经是“not moving”(不移动),这本身就表达了它无法移动的事实,完全没必要再额外依赖一个单独的 movable
标志。换句话说,movable
与 movement mode 是重复表达了实体是否能动的状态。考虑到这一点,我们计划很快去掉 movable
这个标志,以简化数据结构和判断逻辑。
接着查看了实体更新时所使用的物理量,发现当前逻辑涉及两个主要变量:
ddtBob
:可能表示一种与摆动或附属物(如头部)相关的加速度或速度变化;ddp
:实体在模拟过程中的实际加速度。
我们认为 ddtBob
的使用语义上更贴近脑逻辑(brain logic),比如实体处于“植根”模式(planted)且存在一个“head”的时候,它表示身体与头部之间的偏移摆动关系。这种处理逻辑更像是实体“思想”层面上的处理,而不是物理处理,所以我们将其归类为属于脑逻辑的一部分。
基于这个判断,我们将对 ddtBob
的计算方式做如下调整:
- 只在脑逻辑中根据需要累加;
- 积累的值会暂存在实体结构中,但只在模拟周期内使用;
- 不会永久保存在实体状态中(因为 packing 阶段时会重置);
- 和
ddp
的用法保持一致,作为临时的模拟状态数据,不参与持久存储。
这一改动与之前对 ddp
变量的处理方式保持一致,我们正在逐步形成一个统一的、清晰的模拟状态管理机制:只有在模拟期间使用的物理量才会被保存在实体上,完成模拟后即被清除,避免数据污染和逻辑混乱。
总之,我们现在正在构建一个更明确区分“脑逻辑”、“物理逻辑”以及“图形渲染”的系统架构。在当前阶段,我们专注于将冗余标志清除、状态表述语义化,并以更模块化的方式管理临时模拟数据,为整个引擎系统的未来维护与扩展打下稳固基础。
在 game_entity.h
中为实体添加 ddtBob
和 MoveSpec
字段
目前我们正在持续优化实体的移动系统,并对模拟数据进行结构化整理与清洗,以确保系统的逻辑性与可维护性。
首先,我们定义了一个临时的变量 ddtBob
,虽然这不是最终的命名,但它代表了某些骨骼(如头部与身体之间)的弹性或“弹簧感”特性。这类变量主要用于实体内部结构的动态模拟,例如头部与身体间的距离变化。在设计上,这种弹性行为属于“脑逻辑”范畴,即不是实体受到外部物理作用力后的反应,而是内部结构自动进行的调整。我们通过判断实体是否处于“植根”状态(planted)并且具有头部,来控制这段逻辑是否执行。
其次,为了避免某些临时性变量在下一个更新周期中错误地被保留,我们在实体的打包环节中(packing,即状态持久化前),主动清除那些不应被保存的字段。这样做的目的是防止未来维护过程中误判这些字段仍然有效,从而出现逻辑错误。这个清除动作作为一种“预防性”措施(prophylactic),可以让系统行为更加明确和可控。
然后,我们进一步清理了实体头部与身体之间距离的计算逻辑。之前存在一些复杂的判断,现在我们统一将其简化为头部与身体位置的直接向量长度,这也使得整体结构更清晰简洁。
接下来进入实体的加速度处理逻辑(ddp
部分)。这是控制实体如何移动的核心变量之一。我们使用一个“movement spec(移动规范)”结构来决定速度、加速度和阻力等参数。这个结构原本是局部变量,但我们发现它应该是附属于实体的非持久性字段(不打包存储),因为它是由多个系统在一个周期中动态更新的结果,而非固定状态。
我们计划将 movementSpec
添加为实体结构中的一个临时字段,整个模拟周期中,各种逻辑可以往里写入数据,模拟结束时再丢弃。这样就能达到以下目标:
- 所有控制实体移动的逻辑可以在一个统一位置读取/写入速度、加速度等参数;
- 避免原有散乱的设置方式,提高可维护性;
- 不需要将这些临时数据存储到持久化状态中,避免状态污染。
具体调整上,我们原本是直接从函数内部创建一个默认的 moveSpec
,而现在将改为从实体结构中获取。我们将所有需要用到 moveSpec
的代码位置(例如身体的移动逻辑)修改为读取实体的临时字段。
值得一提的是,我们发现在原有实现中,头部实体并未真正使用 moveSpec
。这表明之前的实现中头部并不依赖规范进行运动,或者相关代码已被删除。考虑到我们当前并不打算实现熟练体系统(如宠物、分离的头部行为等),因此暂时不处理头部的 moveSpec
逻辑,仅集中处理身体部分即可。
总结如下:
- 移除了
movable
标志,转而依赖 movement mode 的语义; - 将
ddtBob
和ddp
明确归类为模拟周期内的临时数据,并在打包时清除; - 把
movementSpec
从临时局部变量上升为实体级临时字段,便于多处共享和更新; - 精简了头部与身体之间的距离判断逻辑;
- 暂时忽略熟练体(familiar)相关功能,专注于基础实体行为逻辑。
这一步的工作整体目标是将移动与模拟系统从分散、不一致的实现,过渡到统一、结构化、易于维护的形式,为后续的行为扩展、物理响应与渲染逻辑奠定坚实基础。
在 game_brain.cpp
中让 ExecuteBrain
给头部实体添加移动行为
目前我们继续对实体运动系统进行结构性调整,并试图理清加速度(DDP)的传递路径与实际生效机制,目标是让系统更加一致、语义明确。
一开始我们明确了,身体部分暂时不需要关心,但头部(hero head)必须使用某种运动参数(spec)。虽然目前仍然使用 movementSpec
来表示速度、阻力等信息,但我们逐渐意识到,这套系统的设计并不理想,它可能在未来扩展时难以维护与扩展,因此也在考虑将其改为更函数式的模式,具体实现留待后续讨论。暂时将与熟练体(familiar)相关的代码丢弃,聚焦当前核心逻辑。
接着我们审视了整体结构,发现当前并没有成功地将 DDP(加速度)传入实体头部,导致无法产生预期中的运动。于是我们开始从“大脑逻辑”代码入手排查,这部分代码中确实设置了 DDP,即控制器发出的加速度请求。此前我们将其直接累加到实体头部的速度向量中,但这并不合理,导致了处理逻辑的混乱。
为此我们决定将 DDP 作为加速度值直接赋值给实体头部的加速度字段,而不是通过间接方式影响速度。目标是建立一个明确的加速度传递机制,即:
- 大脑模块计算出加速度(DDP);
- 该加速度赋值到实体结构中的头部字段;
- 后续物理模拟根据此加速度计算速度、位移等。
然后我们排查为何实体没有任何运动反应。初步判断是因为加速度未生效或未传递。为更方便测试,我们打算临时为实体添加一个持续的推力(例如始终向右加一个力),以避免每次都手动按键,并方便设置断点调试。
同时我们意识到可以使用 was_pressed
变量来判断输入状态,从而控制实体是否接收到加速度指令。这也为后续更复杂的交互逻辑提供了基础。
总结如下:
- 将加速度(DDP)从间接的速度影响方式改为直接赋值;
- 放弃临时、混乱的处理方式,建立清晰的输入-控制-动作链路;
- 抛弃
movementSpec
的部分实现逻辑,准备逐步替换为更合理的设计; - 移除熟练体相关逻辑,简化当前目标;
- 通过添加恒定推力与输入检测机制简化调试流程;
- 当前的目标是修复实体头部不运动的问题,确保加速度逻辑实际生效。
通过这一轮重构,我们正逐步把系统从“零散指令式逻辑”过渡到“语义明确、结构合理”的现代实体行为模拟模型中,为之后支持更复杂的交互与动画行为打下基础。
还是没有动
在 game.cpp
中为 controlled_hero
添加 ddP
字段
我们继续对输入控制系统与实体加速度(DDP)传递逻辑进行排查与改进,并逐步还原系统的历史意图与当前结构的合理性,以下是这一阶段的详细总结:
一、控制器状态的持久化设计
我们最初遇到的问题是加速度(DDP)没有生效,进一步排查发现控制器输入并不是直接作用于实体,而是通过一个状态变量进行中转。原来我们曾经实现过一个机制,用于保存上一次输入的方向信息(如最后一次按下的是哪个方向),从而在按键松开之后仍然保留运动意图。
这种“输入记忆”的机制用于改进用户体验,使得实体在接收到间歇性输入(例如短暂按一下方向键)时也能维持一个持续的动作方向。我们重新认识到这是一个有效且值得保留的特性,因此决定继续沿用,并将其保留在控制器结构中。
二、变量与结构调整
为了实现上述行为,我们将控制器的输入状态变量命名为 con_hero_ddp
(代表控制器决定的方向加速度),并取消了旧的 DDP 变量,重新设置 DDP 的赋值方式为:
ddp = con_hero_ddp
也就是说,每一帧实体的 DDP 都由当前控制器状态决定,控制器状态则依据上一帧的输入情况进行更新与保持。
这种结构让控制逻辑更加集中、清晰,同时也为未来扩展手柄输入、网络同步等特性提供了基础。
三、未来计划:支持模拟摇杆(analog stick)
我们意识到目前的输入逻辑是为数字方向键(D-pad)设计的,对于摇杆类输入未做特殊处理。因此我们提出需要在未来加入如下功能:
- 十字死区(cross dead zone):创建一个死区算法,当输入接近 x 或 y 轴时能“锁定”方向,避免轻微偏移造成方向偏移。
- 方向优先权策略:根据摇杆输入向量判断哪个方向更“优先”,让系统根据最大输入量自动选择方向。
- 摇杆兼容性:使系统既支持数字按键,也能兼容模拟输入。
当前没有立即实现这些内容,但我们已经认识到它们在支持手柄控制时是必不可少的,未来会进行扩展。
四、调试与重启
由于我们变更了数据结构,比如控制器状态的存储与 DDP 的布局,必须重启系统或引擎以确保数据结构被正确加载和初始化。我们记录了这一变化以避免未来混淆。
小结
当前改动的关键目标是简化和规范 DDP 的产生与使用流程,同时恢复与保留之前有效的输入记忆机制,并为后续支持模拟摇杆输入打下基础。调整内容如下:
- 将控制器输入状态持久化为
con_hero_ddp
; - DDP 每帧从控制器状态中赋值;
- 移除旧的 DDP 逻辑,简化数据路径;
- 未来计划支持模拟摇杆的十字死区和方向优先逻辑;
- 由于数据结构变化,系统需重启以确保正确执行。
整体思路是持续推进输入到运动的逻辑闭环,并逐步从简化演进到可扩展的设计。
使用调试器调查为何没有任何加速度
我们继续排查 DDP(加速度向量)无法生效的问题,采取了设置断点并逐步跟踪的方式,发现了一个关键性逻辑错误,以下是具体过程和当前结论的详细总结:
一、排查目标与方法
我们的目标是确认 DDP 没有被正确设置的具体原因。在深入重构代码前,我们选择先进行一次完整的逻辑跟踪,以避免在已有错误未修复的情况下引入新的复杂因素。我们决定从 向左移动(Move Left) 的处理分支入手,因为这是最简单、最容易验证的一种情况(x = -1)。
通过在输入处理的控制逻辑中设置断点,并逐帧查看 DDP 的赋值与流转,来验证值是否如预期那样进入实体的状态更新流程中。
二、调试过程中的发现
成功命中断点后,检查控制器变量
con_hero_ddp
,其x = 1
,符合我们按下右键的预期。然而,当代码执行到设置
ddp_to
(用于传递到实体的 DDP)时,发现其根本没有被正确赋值。ddp_to
的赋值逻辑并未将con_hero_ddp
的值传递进去,因此后续逻辑使用的是一个未初始化或错误的加速度值,导致实体没有任何加速效果。真正应该做的是将
ddp_to
设置为等于控制器输入计算出的加速度,即:ddp_to = con_hero_ddp;
这一步原本预期存在,但实际遗漏。
三、问题总结
- 控制器成功识别输入,
con_hero_ddp
值正确。 - 关键问题出在加速度没有从控制器状态传播到实体 DDP 的目标变量
ddp_to
上。 - 一旦赋值修复为
ddp_to = con_hero_ddp;
,实体应能按预期响应方向控制。
四、未来行动与建议
- 修复上述赋值问题后,再次测试是否实体能够正常响应输入产生运动。
- 修复完成后再考虑下一步对输入、加速度、物理逻辑的结构性整理和重构。
- 后续建议在输入到实体之间的数据路径中明确每一个中间变量的责任,并减少“临时变量”或不透明名称(如
ddp_to
)带来的混淆。
总结
本轮调试发现了导致实体无法获得加速度的根本问题:控制器输出的加速度变量没有被正确传递到实体逻辑中。修复方式为:明确将 con_hero_ddp
的值赋给实际用于计算的目标变量。这是当前最紧急的问题,修复后预计能恢复基本移动能力,后续将继续清理和改进整体结构。
在 game_world_mode.cpp
中修复 UpdateAndRenderWorld
正确设置 ddP
我们继续完善加速度(DDP)在实体系统中的传递机制,过程中发现了几个关键遗漏,并做出相应调整以确保逻辑完整。以下是当前修改与设计思路的详细总结:
一、加速度传递逻辑的问题确认
我们在分析中注意到,虽然加速度 con_hero_ddp
从控制器逻辑中得到了正确计算,也被正确地传递给了 head_ddp
(头部的加速度变量),但 该值并没有真正应用到实体系统中的 DDP。换句话说,加速度传到了某个变量,但没有被用于实体的实际移动逻辑,这使得实体依然无法移动。
二、缺失使用 DDP 的问题修复
为了解决这个问题,我们进行了如下修正:
明确将头部的
head_ddp
设置为实体的ddp
(即 DDP):entity->ddp = head_ddp;
这确保了控制器计算出的加速度可以作用在实体头部,并通过主更新逻辑驱动实体移动。
三、对“familiar”类型的处理方式调整
我们对 familiar
(跟随物或附属实体)的处理方式也进行了简化和修正:
- 暂时将其加速度逻辑统一为普通实体加速度的使用方式,即
ddp = entity->ddp
。 - 后续会将
familiar
类型实体的逻辑迁移到 brain(脑模块)中独立处理,保持逻辑清晰分离。
四、当前设计的逻辑模型清晰化
目前的输入到运动传递路径如下:
- 控制器处理逻辑接收输入(如方向键),生成一个 DDP 向量。
- 如果当前没有输入(未按下),DDP 会被裁剪为零(防止持续加速)。
- 有效 DDP 会传递给 实体的 head 部分(头部)。
- 最终将
head_ddp
应用于实体本身:entity->ddp = head_ddp
。 - 物理更新逻辑使用该 DDP 推动实体更新速度和位置。
五、后续计划与建议
- 后续将
familiar
实体的处理独立放入 brain 系统,避免代码重复与主逻辑混杂。 - 可以进一步优化 DDP 的结构表达,例如合并临时变量,建立统一接口。
- 考虑添加调试工具,用于可视化当前实体的 DDP、速度、位置变化,更便于定位问题。
总结
当前阶段我们已经修复了 DDP 在控制器计算后未真正用于实体移动的问题,统一了实体和跟随物的加速度设置方式,为后续拆分、优化提供了坚实基础。下一步将转向逻辑结构整理,确保系统更易维护和扩展。
终于可以动了
运行游戏并进行移动测试
我们目前的系统状态虽然还不算完全正确,但已经取得了一个重要的进展——我们现在终于能够以预期的方式移动头部(head)了。这标志着加速度(DDP)的传递和使用逻辑基本恢复了功能,控制输入可以顺利地转换成实体的移动行为,这是实现更高级角色控制的基础。
当前状态总结:
实体头部移动恢复
- 头部的加速度已经可以正确响应控制输入,系统恢复了基础运动功能。
- 这意味着 DDP 的生成、赋值和应用路径至少在主实体部分已经是连通的。
系统仍不完整,存在遗留问题
- 仍有大量未完成的代码工作,包括其他实体(如 familiar)、组件系统的封装等。
- 数据结构和更新流程仍待进一步清理,部分逻辑结构尚未合理分离。
- 输入系统对不同设备(如手柄、键盘)的兼容性逻辑尚未补充完整。
外部影响与调试环境
- 当前开发过程中遇到了一些外部干扰(如环境导致身体不适),但整体调试仍顺利。
- 目前我们能看到实体在屏幕上的响应是符合逻辑预期的,这是阶段性的验证成果。
后续关键任务(计划):
- 继续完善 familiar 的行为逻辑,并将其逻辑迁移到 brain 模块,统一逻辑控制。
- 重构 movement_spec 结构,目前的运动规格系统不够灵活,未来考虑更函数式的实现方式。
- 增强输入系统兼容性,支持模拟摇杆与方向键的混合输入,并设计合理的死区处理逻辑。
- 修复和清理未使用或失效代码,清除残留的变量、注释、旧路径,保持逻辑清晰。
结语:
虽然系统还未进入理想状态,但核心运动链路已经打通。我们将以当前成果为基础,继续推动结构清理、逻辑完善和组件模块化,逐步朝着健壮、可维护的架构迈进。
在 game_brain.cpp
中将 Familiar、FloatyThingForNow 和 Monstar 的代码从 game_world_mode.cpp
中移入
我们现在的任务是进一步清理和整理代码结构,特别是将逻辑从 entity_type
中抽离出来,使其尽可能简洁、职责单一,这是当前重构目标之一。
当前清理进展与重构方向
简化 entity_type 结构
entity_type
目前已经变得非常简洁,达到了我们的目标。我们不再希望entity_type
负责控制实体的行为逻辑。- 行为相关的代码将全部迁移到“brain”逻辑中管理。
迁移 Hero Familiar 行为逻辑
- 目前将
hero_familiar
的代码移动到了新的模块中,暂时依然以该名称命名,便于识别。 - 不一定最终还保留这个名字,但短期内可以作为一个 placeholder。
- 将与其相关的行为逻辑统一到 brain 部分,确保逻辑集中。
- 目前将
处理漂浮行为(floaty thing)
- 所有和“行为”相关的逻辑(如漂浮、运动)都将归入 brain。
- 哪怕某些实体看起来只是风景或动画对象,只要其具有行为(如周期性移动),都将通过 brain 模块驱动。
- 后续可将 brain 类型泛化,比如一个 brain 类型可以代表“动画风景”,其具体参数(如移动方式)由实体属性决定。
废弃基于 entity_type 的逻辑分支
- 所有判断行为类型的旧式
entity_type
分支都将逐步废弃。 - 渲染部分、行为部分各自独立,根据实体当前的 brain 配置进行判断和处理。
- 所有判断行为类型的旧式
清理剩余模块依赖
- 渲染代码已被整理,并从
entity_type
中分离出来,移到专门位置。 - 运动逻辑同样独立,剩下的就是将原先在
entity_type
结构中依赖的地方逐步替换掉。
- 渲染代码已被整理,并从
遗留任务和注意事项
- 目前还没有完全迁移
familiar
的编辑逻辑,但很快就要处理它。 - 需要开始记录重构过程中出现的细节问题(例如 buffer position 丢失等),否则容易遗忘,阻碍优化。
- 我们应建立一个更有系统的方式来管理这些“for coder”的边缘问题,保证它们不被忽略。
总结
整体结构已经朝着行为逻辑模块化的方向演进,我们不再依赖硬编码的 entity_type
来控制行为,而是将各类行为(如漂浮、运动、动画)统一放入更清晰、职责分明的 brain 模块中。虽然仍有部分模块未完成迁移,但基础结构的改动已经显现出良好的趋势,后续将继续推进 familiar 编辑器的逻辑整合,并着手系统化问题记录与管理。
先测试一下能不能动
在 game_world_mode.cpp
中思考如何摆脱 Entity->Type
,并移除 Stairwell 和 Floor 类型
现在我们的目标是从整个系统中彻底移除 entity_type
的概念,这是当前重构中最核心的一步之一。我们希望实体本身由一些独立的组件(pieces)组成,通过组合来表达其功能和行为,而不是依赖一个集中枚举或类型来决定逻辑流转。
重构目标
完全取消
entity_type
不再使用entity_type
来描述或区分实体,而是让实体由多个组件组合而成,组件本身携带行为和渲染逻辑。实例化实体时直接组合组件
在创建实体的时候,直接指定组成它的各个组件,并且这些组件知道自己如何进行渲染、交互、运动等。组件式设计
每个组件是功能明确的,例如:- 头部组件:包含头部的渲染和运动逻辑
- 身体组件:处理身体部位相关的绘制和状态
- 浮动行为组件:用于生成漂浮效果
- 控制器组件:响应输入并产生加速度或控制命令
现有实体结构分析
位图管理
当前通过entity_type
来映射资源和位图,例如在渲染时根据类型决定加载哪一组图像。现在打算改为由每个组件自行管理其所需资源,避免通过集中管理的方式绑定资源。Stairwells
该类实体已废弃,不再使用,直接从结构中移除。Monstars
这类实体与普通英雄类似,只是多了一个 torso(躯干)组件。从组件角度看,它和 hero 的区别在于包含了不同的部分,因此不再需要类型区分,而是组合不同组件即可。Floors 和 Floaty Things
这些是静态或具有简单运动行为的物体,可以单独定义组件,控制是否运动、是否渲染、是否响应碰撞。
行动计划
移除 entity_type
- 将所有依赖
entity_type
的逻辑逐步改为使用组件系统处理。 - 把资源加载和渲染逻辑转移到组件中。
- 行为处理(如移动、跳跃)统一由 brain 控制。
- 将所有依赖
标准化实体构建方式
- 创建一个统一的构建函数(如
create_entity(pieces)
),通过传入组件组合的方式构建实体。 - 每个组件定义清晰接口,例如
render()
,update()
,handle_input()
等。
- 创建一个统一的构建函数(如
统一 brain 驱动行为
- 所有具有行为的实体都由某种 brain 控制。brain 类型决定行为逻辑,组件参数控制细节。
清理和简化渲染路径
- 移除所有基于
entity_type
的 switch 分支和分发逻辑。 - 各个组件自己注册渲染逻辑,渲染模块只遍历实体并调用其组件渲染。
- 移除所有基于
当前进度确认
- 已识别出哪些实体类型将被淘汰(如 stairwells)。
- Monstars、Floors 等实体分类已归纳成可以通过组件组合实现的类型。
- 确认组件系统将包括行为组件(brain)、渲染组件、输入组件等。
总结
我们已经准备好彻底取消 entity_type
的使用,采用更具扩展性和灵活性的组件式架构。每个实体将仅由一组功能清晰的组件组成,所有的行为、渲染与状态逻辑都由这些组件自身承担,不再依赖集中式的类型判断逻辑。这为后续扩展更多种类的实体打下了坚实基础,同时也极大提升了可维护性和清晰度。
在 game_world_mode.cpp
中让所有实体都能绘制自己的碰撞矩形
当前的重点是进一步清理代码中冗余、结构化不良的部分,尤其是关于调试绘制的逻辑。我们已经决定彻底摆脱这些过时的调试代码,不再将其绑定于某种特定的逻辑位置或实体类型之中,而是将其变成一个通用机制,任何实体都可以选择性地参与调试绘制。
重构目标
删除旧的调试绘制代码
之前的调试绘制(例如 traversable 区域和碰撞框的显示)是硬编码在特定实体逻辑中的,现在计划将这些调试功能彻底脱离实体类型逻辑。通用调试绘制功能
每个实体都可以自主决定是否绘制调试信息,比如:- 可通行区域(traversable rect)
- 碰撞矩形(collision rect)
- 其他开发时需要可视化的信息
调试绘制为统一模块管理
调试绘制的实现统一交由渲染系统在调试模式下集中处理。实体只需暴露自己的调试绘制需求,例如通过某种回调或配置函数提供调试矩形列表。
实施步骤
移除嵌入式调试绘制逻辑
- 原本嵌套在特定
entity_type
判断或个别函数中的调试绘制代码全部删除。 - 将调试绘制功能剥离出实体类型逻辑之外。
- 原本嵌套在特定
在渲染阶段统一处理调试绘制
- 引入一个调试绘制入口,集中遍历所有实体,收集其暴露的调试信息。
- 每个实体提供自己的调试数据,如
get_debug_rects()
,返回需要绘制的矩形及颜色等。
让任何实体都可自由参与调试绘制
- 不再限制于某些实体或状态,任何实体如果需要调试显示,就返回对应的数据即可。
当前状态确认
- 旧的调试代码已确认冗余,准备移除。
- Traversable 区域和碰撞框的数据结构已独立存在,无需依赖类型系统。
- 只需调用一组绘制矩形的代码就能可视化它们,因此不需要复杂的类型判断。
总结
我们正逐步摆脱以往硬编码的调试绘制方式,转向一种更为灵活和结构化的机制。未来,任何实体都可以通过标准化的方式提供其调试可视化需求,渲染系统会集中统一处理这些请求,从而保证调试功能既强大又不干扰正式逻辑,提升代码清晰度和可维护性。
运行游戏并查看碰撞矩形
目前我们正着手将调试绘制功能模块化,并清除代码中所有依赖实体类型(entity type)的部分,构建更简洁、灵活的系统结构。以下是详细整理:
统一调试绘制控制
我们决定添加一个调试开关,用于控制调试绘制行为。当该开关开启时,系统将自动绘制所有需要调试信息的内容,比如碰撞框、可通行区域等。通过这种方式,我们不再需要将调试代码嵌套在具体的实体类型判断或特殊逻辑中。
完全去除 entity type 依赖
所有关于 entity_type
的判断逻辑已经清除,现在所有实体行为都基于它们自身的组成和属性,而非某个预定义类型。这大大简化了系统结构,使得:
- 实体的逻辑和功能模块(如“头部”、“影子”、“弹簧连接”等)可以自由组合;
- 渲染和运动行为都不依赖“类型”,只看当前实体拥有的功能组件;
- 后续添加新实体变得更灵活,不再受制于类型定义。
简化渲染处理逻辑
剩余的渲染判断变得非常简单。主要就是判断:
- 是否绘制阴影;
- 是否存在多个相互连接的部件(如弹性连接);
- 是否有特殊颜色变换或偏移效果(例如英雄身体的跳动偏移);
这类判断均可通过实体内部状态或配置结构完成,不需要硬编码类型条件。
影子与弹性模块抽离
影子的绘制逻辑将由实体自行声明是否拥有“影子”属性,渲染模块判断是否需要绘制即可。
而“弹簧”连接效果(如头部与身体的弹性关系)也将作为一种功能性模块独立存在,实体可根据需要引用。
暂时保留的特殊处理
当前发现有一处为英雄主体设置偏移(bouncy offset)逻辑时,使用了一个名为 why_color
的参数,看起来可能是之前为跳动效果设定的某种颜色或调试标识。尚未完全清楚其必要性,后续可能会被重构或删除。
当前结构已具备目标特征:
- 更清晰:功能模块被划分清晰,没有被打乱在一堆类型判断中;
- 更灵活:支持实体组件自由组合;
- 更易维护与扩展:调试绘制与核心逻辑完全解耦;
- 更简洁的渲染判断:无需繁复的分支逻辑,仅依赖属性状态。
我们下一步将继续清理历史遗留代码,如不必要的调试参数和旧逻辑,并推进实体模块化组合的实现。
在 game_world_mode.cpp
中压缩实体类型逻辑
我们决定对现有的绘制逻辑进行进一步简化和通用化,以实现更灵活、统一的实体渲染体系,具体思路和执行如下:
通用化绘制逻辑:实体结构扁平化
我们将原本针对具体类型(如“英雄”或“Monstar”)写的绘制逻辑整合成一个通用合成渲染逻辑。这个逻辑中,每个实体都可以包含如下组件:
- 阴影(shadow)
- 主要结构体部分(如身体、斗篷、头部等)
- 动态部件(如弹跳效果)
每个部分都可以独立存在或组合使用,是否存在某个组件完全取决于实体自身的数据结构,而不是依赖“实体类型”的定义。
Monstar 与 Hero 可共用渲染逻辑
“Monstar”和“Hero”在视觉表现上几乎一致,唯一差异只是缺少头部。我们不再专门为 Monstar 编写单独逻辑,而是使用同一套渲染方案,仅根据实体是否存在“头部部件”来决定是否绘制。
动画部件统一处理
所有“上下浮动”效果(如头部 bobbing、斗篷 bobbing)都整合进同一个动画模块中处理。斗篷本身已有浮动逻辑,所以头部直接继承即可。没有必要在结构上区分“头部浮动”和“斗篷浮动”,只要组件支持动画,系统就能自动处理。
Hit Point 通用处理
Hit Point(生命值)显示现在也是统一逻辑:
- 所有实体都可以拥有 HP;
- 如果实体当前没有 HP,就不绘制 HP;
- 不再使用类型判断决定哪些实体拥有 HP,只检查是否存在相关数据字段。
这种方式极大提高了系统的扩展性,任何实体在运行时都可以临时添加生命值、调试状态或其他可视化数据,而不需要修改其“类型”。
合成渲染结构设计雏形
我们在构思一种组合式渲染结构:
每个实体具备多个层级(层可以叠加):
- 阴影层
- 身体主图层
- 头部图层
- 动画偏移层(如 bobbing)
每层根据数据是否存在、是否可见、是否启用动画来决定是否绘制;
例如 Hero 就是一个 “影子 + 身体 + 头部” 的组合,Monstar 就是 “影子 + 身体”。
这样的结构通用、清晰,且非常容易调试与扩展。
高度信息组件化
每个部件现在可以拥有自己的 高度值(z 值),我们把这些值作为实体属性维护,比如:
- 头部高度
- 身体高度
- 阴影偏移量
绘制时直接读取这些值决定图像的渲染顺序和位置偏移,而不是硬编码逻辑或类型依赖。
下一步方向
- 把这种组件化渲染机制进一步抽象出来,可能使用一个通用的“部件表”描述一个实体所拥有的所有可渲染组件;
- 加入更健壮的动画控制接口;
- 清理掉所有类型判断及历史结构冗余字段;
- 把绘制、动画、逻辑完全分离,做到真正意义上的数据驱动。
整体目标是:**任何实体皆由数据驱动组成,不再需要依赖硬编码的类型体系。**只要定义清楚它拥有哪些组件、如何动画、如何渲染,即可自然呈现行为与外观。
在 game_entity.h
中引入 entity_visible_piece
类型
我们开始构建一个更通用、结构更清晰的实体渲染机制,核心是引入一种“可见部件(visible piece)”的概念。具体设计与实现思路如下:
引入可见部件(Visible Piece)概念
每个实体可以由若干个可见部件组成,每个部件代表实体的一个渲染层或动画子部分。我们将其抽象为一组结构体,包含以下核心字段:
资产类型(Asset Type)
表示此部件绑定的图像资源种类,例如是身体、斗篷、头部等。透明度(Alpha)
控制部件的可见度,决定是否做渐隐、渐显等效果。缩放因子 / 高度(Scale / Height)
控制部件在画面上的大小,尤其是在存在 z 轴漂浮或高度动画时。颜色(Color)
使用v4
类型保存颜色信息,可用于染色、状态变化效果等。朝向信息(Facing Direction)
用于选择资源方向,例如角色面向不同方向时贴图不同。
可见部件结构用途和设计限制
这些可见部件的主要用途是用于动画渲染,比如角色头部上下浮动、斗篷随动作摆动等。它们不是独立实体,不具备独立逻辑行为或物理碰撞。
为避免系统复杂化,我们限制每个实体不应拥有过多可见部件。如果需要更复杂的结构,应该将子部件拆分成独立实体再处理。
数据共享与未来优化
虽然现在每个实体都持有自己的部件列表,但在未来可能会引入共享机制:
- 如果多个实体使用相同的部件配置,可以共享这些数据结构;
- 减少内存占用,提升数据缓存效率;
- 当前阶段暂不实现,后续可优化。
可见部件统一绘制逻辑
我们将原先的分散绘制逻辑整理为统一循环处理:
- 每帧遍历实体持有的所有可见部件;
- 依照当前实体的位置和部件的高度、缩放、朝向等信息计算位置;
- 渲染对应图像资源。
这样可以用一段渲染代码完成大部分角色、怪物、装饰物体的绘制,不再需要为不同类型写重复代码。
逐步迁移策略
由于复杂部件(如 Hero 的主身体结构)还涉及多个动画控制与逻辑判断,我们采取分阶段策略:
- 先处理简单的结构部件,如头部、影子、斗篷等;
- 等这些通用机制稳定后,再逐步迁移更复杂的核心组件;
- Hero 主身体部分暂时保留原逻辑,后续再重构接入。
时间限制与开发节奏
由于当前开发时间有限,我们选择优先完成基础功能通用化,逐步验证系统合理性,避免过早优化或过度重构:
- 先验证可见部件渲染的正确性;
- 再评估是否需要改进数据结构或引入共享机制;
- 未来可以围绕该结构做动画分层、特效叠加、性能优化等拓展。
最终目标是让所有实体的视觉表现和动画行为都通过数据驱动完成,而不是依赖硬编码和类型判断。这种机制将为未来系统扩展、编辑器支持、调试可视化等功能打下良好基础。
在 game_world_mode.cpp
中使用 entity_visible_piece
重新实现 Wall(墙体)
我们开始实现一个通用的可见部件绘制机制,首先从最简单的实体 —— 墙体(Wall)开始着手。以下是本阶段的详细思路与实现流程:
遍历并绘制可见部件
我们为每个实体定义了多个“可见部件(visible pieces)”,现在我们要实现一个循环,逐个绘制这些部件:
使用一个索引变量
pieceIndex
从 0 开始;只要
pieceIndex
小于当前实体的部件数量pieceCount
,就进入绘制流程;每个部件执行一次绘制:
获取当前部件结构;
使用
GetBestMatchBitmap
函数,从资源库中查找最合适的贴图(bitmap):- 传入参数包括:资产类型(asset type)、匹配向量(match vector)、权重向量(weight vector);
调用
PushBitmap
将贴图推入渲染队列:- 提供贴图 ID;
- 提供 Z 高度(来自该部件的 height 字段);
- 提供颜色(color 字段);
- 暂时忽略偏移量 offset(还未处理);
这个过程建立了一个非常通用的渲染机制,理论上任何实体只要配置了可见部件都可以被正确绘制出来。
为墙体添加可见部件
接下来我们从创建实体时着手,为墙体(Wall)这种静态实体添加可见部件:
- 在创建墙体时,添加一个新的可见部件;
- 设置该部件的资源类型(asset type)为墙体贴图对应的类型;
- 设置默认颜色、默认缩放(高度)、默认朝向等;
- 这样墙体就能在新的渲染逻辑中正确绘制出来。
修复 asset type 的依赖顺序问题
在实现过程中发现一个问题:
- 实体结构中引用了
asset type
; - 但
asset type
的定义在entity
之后,导致编译或处理出错; - 我们修正了 include 或结构声明的顺序,确保
asset type
在使用前定义; - 问题解决后,绘制机制可以正常运作。
当前实现总结
我们已经完成了如下关键步骤:
- 构建了一个通用绘制循环,适用于所有拥有可见部件的实体;
- 从墙体实体入手,将其迁移到新的通用渲染系统;
- 验证了基础功能可以正常运行,下一步可以继续迁移更多实体;
- 当前偏移量尚未使用,后续可用于动画或位置微调;
- 整体逻辑清晰,结构可拓展,已为后续复杂实体做好准备。
这一套机制极大地提升了系统的可维护性与灵活性,我们可以在不改动底层渲染逻辑的情况下,为任何实体添加新的视觉表现效果。接下来可以逐步迁移如角色、怪物等复杂对象到这个统一框架中。
不让触发断言
运行游戏但未看到墙体
在游戏运行时,虽然墙体的碰撞体积存在,但墙体本身并没有被显示出来,导致看不到墙。这表明墙的可见部件还没有正确地渲染或绑定到显示逻辑中。接下来需要检查和完善渲染流程,确保墙的可见部分能够正确绘制,从而在游戏中真实显示出来。
在 game_world_mode.cpp
中让 AddWall
调用 AddPiece
需要为每个实体创建一个可见组件,比如树这个实体就有一个组件代表它自己,并且这个组件负责绘制树的图像。具体操作是在游戏的构建模式代码里,添加一个“添加组件”的命令,这个命令会创建一个对应的实体组件,指定它的资源类型是树,高度设置为2.5,颜色为默认的白色(不进行任何颜色变换),暂时没有设置偏移量。这样,树的实体就有了一个可见的组成部分,可以被正确绘制出来。
讲解如何让实体自行定义其渲染方式的整个过程
我们正在做一个非常直接的过程,目的是创建一种数据布局,这个布局能够镜像之前的处理方式,从而明确每个实体在渲染时的具体表现。这样每个实体都可以拥有自己独特的渲染方式。这种方法有点类似于一种简化版的脚本语言——我们添加了一些记录,这些记录定义了要调用的函数,函数的调用基于非常具体的参数化。这其实是一种有限的脚本化方式,通过压缩一些代码逻辑成少数几个参数,然后在需要的时候解包执行。换句话说,我们通过这种方式,将对渲染函数的调用简化成参数配置,以实现灵活且高效的渲染控制。
在 game_world_mode.cpp
中引入 AddPiece
函数
在添加实体部件的过程中,我们通过一个简单的函数实现:给实体的可见部件数组增加一项。具体做法是,先检查当前实体的部件数量是否未超出限制,然后为新部件赋值,包括资产类型ID、高度和颜色这几个基本属性。完成赋值后,实体的部件计数加一。这样就完成了一个部件的添加。理论上,完成添加后,这些部件就可以被渲染出来了。总结来说,这个过程非常简洁高效,重点就是管理好每个部件的基础属性,并确保计数准确,方便后续渲染调用。
运行游戏并看到树木出现
现在功能已经恢复,可以看到树木正常显示了。因为实现了这个功能,我们还可以使用世界模式中的随机序列功能。也就是说,基于当前的实现,可以灵活地利用随机生成的方法来控制或调整场景中的元素分布和表现,使游戏世界更加丰富和动态。这说明整个系统架构开始走向模块化和灵活化,方便后续的扩展和调整。
在 game_world_mode.cpp
中暂时让 AddWall
对 Piece->Color
应用随机颜色效果
我们讨论了如何利用效果熵(effects entropy)来给树木增加随机的颜色变化。虽然暂时没有实际实现,但设想中可以通过在效果熵上加入随机变化,使每棵树呈现出不同的颜色,比如随机的红色成分。不过发现由于树的原始颜色里没有红色成分,所以无法看到红色的变化。于是改成随机增加绿色成分,这样变化更加明显,能直观地看到每棵树颜色的不同。这说明通过控制颜色的随机因素,可以使场景中的同类物体更加多样化,更加自然生动。
运行游戏并欣赏颜色随机的树木
我们了解到,通过让同一类实体(比如树)在可见表现上有轻微变化,而不必为每个变化创建不同的实体类型,可以极大地丰富游戏的视觉多样性。这样即使实体的行为和属性保持一致,玩家看到的每棵树也能稍有不同,避免单调和重复,提升整体的自然感和真实感。这种方法不仅适用于树木,也可以推广到其他实体,使得游戏世界更加丰富多彩。虽然目前还没立即实现这种变化,但明确了这一步的重要性,是迈向更灵活、更动态的实体表现方式的重要进展。
问答环节开始
我们发现树木的红色通道其实是没有红色成分的,所以即使将红色调降到零,也只是移除了本不存在的部分。整体进展顺利,目前没有收到任何问题。如果没有其他疑问,这次的工作就可以告一段落。唯一让我们有些担忧的是,接下来要处理模拟区域的边界问题,这部分的实现可能会比较棘手。除此之外,其他方面都发展得很顺利。
提到一个新发现的论文,阅读摘要并发表看法
这段摘要主要讨论了自定义内存分配器在性能提升中的作用,特别是在游戏开发等领域常用的区域(regions)分配策略。研究指出,目前通用的高性能分配器——Douglas分配器,在大多数情况下表现与自定义分配器相当或更好,只有使用区域分配的两种情况性能明显优于其他方案,提升最高可达44%。区域分配不仅提升性能,还能减少程序员的负担,降低内存泄漏的风险,这是我们也认同的。
然而,区域分配无法单独释放对象,可能导致内存使用显著增加,这限制了它在某些常见编程模式中的适用性,降低了区域分配的通用价值。为此,作者提出了一种结合区域和堆的混合分配器“REAPs”,既保留了区域分配的优点,又支持单个对象的释放,从而扩展了区域分配的应用范围。
实验结果表明,REAPs在性能上可与其他具备区域语义的分配器媲美,同时在空间利用和软件工程方面具备优势。结论是,对于快速的区域分配,应优先使用REAPs,而大多数程序员如果要使用自定义分配器,推荐采用Lea分配器。
总体来看,这与我们对区域分配的认识一致:区域分配在性能和管理简化上有优势,但自由释放对象的需求限制了其使用场景。我们通常在游戏开发中偏好区域分配,使用空闲列表管理回收对象,但某些应用(非游戏)对单对象释放需求更高,因此这项研究提出的混合方案或许提供了新的思路,值得进一步阅读和探讨。
颜色是否有上限?
关于颜色数量是否有限制的问题,我们首先需要明确“限制”具体指什么,因为这个问题可以从多个角度回答。
从发送和处理的角度来看,理论上颜色的数量没有硬性限制,可以使用非常多种颜色。但在实际应用中,具体限制可能取决于不同环境或技术,比如硬件支持的颜色深度、调色板大小、内存带宽、渲染性能等。
如果问的是颜色的技术表现范围,比如颜色格式(如RGBA32、RGB565等),那确实每种格式支持的颜色数量有限制,但通常这些限制已经足够满足大多数应用需求。
还有一种可能是问显示设备或软件是否能同时展示大量不同颜色,这也取决于设备能力和设计需求。
因此,回答这个问题时,需要先理解提问者具体关心的“限制”是哪个方面,避免给出既模糊又可能误导的答案。总结来说,颜色数量的限制视具体情况而定,有些场景有限制,有些则没有。
提问:在生成世界、放置树木等环节是否仍需要某种实体类型?
关于是否必须在某个层级有实体类型的问题,在生成世界、放置树木等操作时,我们可能不会严格定义实体类型。虽然生成过程中会知道放置的是什么,比如调用放置树的函数时,就知道是在放树,但这不一定需要用显式的实体类型枚举来标识。实体类型的概念可能更多是代码结构上的约定,而不一定是运行时必须保留的元素。
因此,实体类型枚举在生成器代码中可能会存在,也可以被使用,但在游戏运行时可能不需要,甚至可以考虑删除。核心观点是实体类型枚举在运行时不一定是合适的控制粒度,反而更希望在运行时通过更灵活的方式管理实体,而不是依赖固定的类型标识。这样设计可以让系统更灵活,更易于扩展和维护。
考虑 “reaps” 相关论文
关于那个“颜色数量限制”的问题在等待进一步澄清的同时,我们也快速看了一下这篇关于“reaps”内存管理的论文。论文中描述的结构和我们平时在工作中写的代码结构并没有太大区别,但他们似乎引入了更多的开销,这些开销看起来超出了我们认为必要的范围。
感觉他们的设计目标可能是为了优化大量频繁的 malloc 和 free 操作的性能,这种情况在我们目前的程序中很少发生,所以他们追求的性能提升对我们来说可能并不那么重要。相反,这些额外的内存开销可能并不值得,因为我们的程序通常不会遇到严重的内存管理瓶颈。
不过我们自己也没花太多时间专门去优化内存性能,因为通常不会成为瓶颈。但如果以后想尝试做一些优化,作为一种练习,也许会更清楚他们提出的那些额外开销是否合理,是否真的有必要。现在来说,这部分还不太确定。
关于树、主角、敌人的颜色讨论
关于“树木、英雄、敌人等颜色是否有限制”的问题,我们其实有点不太理解提问的具体含义。我们想知道,问题是指游戏设计上对颜色数量有多少限制吗?还是说是技术层面上的限制?或者是指树木、英雄等角色在游戏中应该使用的颜色数量有限制?
整体来看,游戏设计中颜色数量的多少通常取决于具体需求,没有硬性技术限制,可以根据需要设计多样的颜色组合。但如果有特定的上下文或背景,可能会影响答案的具体方向。总之,我们需要更明确的问题背景,才能给出更准确的回答。
表示最近有些跟不上,要求简要回顾当前对实体系统的改动目标
目前我们的目标是将实体系统中的各个组成部分拆分开来,尤其是那些让实体能够在屏幕上显示和执行动作的部分,做到模块化且灵活组合。比如,英雄实体由头部和身体组成,我们希望写一段控制代码,可以让头部和身体一起运动,但不强制要求英雄必须由这两个部分组成。同时,还能动态地替换实体的某些部件,比如给英雄换不同的身体,或者换装,不需要复制大量数据,只需简单替换即可生效。
此外,我们希望通过这个系统,可以让实体表现出更多样的行为,比如施放法术后改变行为,再恢复,或者动态改变外观,增加丰富的互动和变化。总之,希望摆脱传统的“实体类型”那种固定且死板的方式,避免一切行为和表现都由单一类型硬编码决定。
之前的设计比较粗糙,实体直接绑定一个类型,然后通过大量的switch语句决定控制逻辑、渲染方式和物理属性。现在希望改成一个实体包含多个小信息单元,通过这些组合来重现之前类型所能表达的所有行为,实现模块化和自由组合。
之所以这样做,是因为这款游戏属于生成型、探索类的冒险rogue风格游戏,这类游戏重视丰富的物品和实体组合以及它们之间的交互,不希望被硬性分类限制。希望每个实体都能在A和B之间灵活组合,而不是非此即彼,这样能带来更多玩法上的深度和多样性。
关于避免数据反规范化(denormalize),目前还在思考和权衡,如何设计既方便管理又性能合理的数据结构。
如何在主机平台上避免非标准浮点数(denormals)?这些平台是否都支持 -ffast-math
?
关于游戏主机平台上的浮点运算和是否支持快速数学(fast math),情况取决于具体平台。如果指的是Xbox One和PS4这类主机,它们实际上是基于x64架构的,浮点运算主要通过SSE(Streaming SIMD Extensions)单元完成。因为SSE单元天然不处理非规格化数(denormals),所以实际上不会出现非规格化数的问题,因此也无需刻意去避免。
坦率地说,不太确定这些主机上的SSE单元是否会处理非规格化数,可能它们默认直接将非规格化数视为零。但总的来说,主机上的浮点计算不会产生非规格化数带来的额外负担,因此在编程时不用特别担心这个问题。
道歉并澄清是在问游戏设计中的颜色限制
在游戏设计中,确实会有特定的颜色用来表达某些特定含义,这些颜色在不同场景或元素中有明确的用途和限制。不过,为了增加视觉的多样性和丰富性,游戏中也会允许颜色上有一定的小幅度变化,这样可以让游戏中的对象看起来更加生动、多样,而不会显得单调。总体来说,虽然有颜色的限制和特定用途,但颜色的微小变化是被允许且鼓励使用的,以提升游戏的表现力和体验感。
能否解释工作中与内存管理方式的不同?
关于工作中的内存管理和当前在游戏Hero中的内存管理之间的区别,实际上很难简单说明,因为这是一个比较复杂且长的故事。两者的内存管理方式存在差异,但细节较多,涉及到具体的实现和需求,无法在短时间内做出详细对比或解释。
怪物是否能互相攻击,例如女巫误伤地精之类的友军误伤机制?
怪物和其他角色之间很可能会互相攻击,比如一个女巫意外地缩小了一个哥布林。虽然目前还不确定是否会这样设计,因为需要实际测试看看是否会造成过于混乱的情况,但默认的倾向是允许这种互动发生。
如果希望游戏支持 HDR,会有哪些不同的处理方式?
如果想在游戏中实现HDR功能,我们可能需要在某些地方添加一些额外的数值,以允许颜色的亮度比当前更强。可以直接让颜色设置得更自由一些,但通常做法是将颜色分成一个“强度值”和一个“颜色值”两部分。总体来说,不会有太大变化,主要还是由渲染器来处理,重点是渲染器会更认真地进行色调映射(tone mapping)和相关处理。
补充说他喜欢“提前分配并手动管理内存”的方式,但不确定是否放弃 malloc/free
是个好选择
我们非常喜欢预先分配内存并自己管理全部内存的做法,比如在游戏中那样。但对于是否完全不用malloc和free,有时候会有疑虑。其实,这个问题可以比较直接地思考:
在游戏中,如果内存只是不断往前推(push),且没有空闲链表(free list),那绝对不应该用malloc和free。因为这种情况下,push操作本质上非常快——只是简单地增加一个指针,速度远远快过malloc。free操作只是简单地重置指针,也非常高效。所以在这种场景下,使用malloc/free不但没必要,反而会拖慢性能。
但如果开始用空闲链表管理内存,就不那么明确了。一般通用方案是,内存不是一次性全部分配好,而是当推到尽头时才分配新块,这样就不会用掉过多内存。这样既有push操作,也有free list回收内存。
面对空闲链表的情况,应该思考到底是用malloc/free更快,还是用自定义的free list更快。其实这很容易测试:只要在代码里用条件编译(ifdef)开关,切换用malloc/free或者free list,测一下性能哪个更好即可。因为程序中空闲链表的数量不会太多,测试起来也不复杂。甚至可以做成内存分配器的一部分,内置开关来切换。
唯一棘手的是如果依赖“区域释放”(region free)那种一次性释放整块内存的机制,切换到malloc/free会比较难,因为malloc/free要求逐个释放对象,不能简单一次性释放。但至少可以测试性能差异,再决定是否值得改进释放策略。
总结就是:完全不用malloc/free通常很快,但有free list时要测试才能知道哪种方案更优;通过加开关方便切换;区域释放虽然高效但灵活性差,切换到malloc/free需要改动代码逻辑。测试是关键,且不要过于担心没法切换,随时都能回退。