回顾并为今天的内容设定背景
我们昨天开始编写一些游戏逻辑相关的内容,虽然这部分不是最喜欢的领域,更偏好底层引擎开发,但如果要独立完成一款游戏,游戏逻辑也必须亲自处理。所以我们继续完善这部分内容。事实上,接下来的所有开发工作都将越来越依赖于游戏本身的实现细节,所以推动游戏向前发展也能促进引擎功能的完善。
目前,引擎层面的基础功能大致都已具备,接下来的工作将更多以实际游戏场景为驱动进行优化和调整。比如,看看性能瓶颈、查看功能是否符合实际需求,并据此改进引擎组件。
昨天讨论并着手实现的一个核心想法,是让角色的移动系统建立在明确的位置点(traversable points)之上。这些点代表角色可以"站立"的具体位置。和传统的瓦片地图游戏不同,这些点并不一定直接对应于图像的像素或瓦片格子,它们更多是逻辑概念。不过在美术上我们也需要通过图形强化这些位置的可视感,让玩家明确知道哪些地方可以站立。
从技术层面上讲,我们希望角色只能停在这些特定的点上,而不能停在两个点之间。换句话说,角色的运动是在这些点之间进行切换的,停下来的时候必须是在某个明确的点上。虽然在两个点之间移动时会经过中间位置,但不能在中间随意停下。
目前的实现仍然是传统的方式,角色只是以某种方式在屏幕上移动一个精灵。因此现在的任务就是改进这一移动机制,真正地让角色运动与这些可行走点结合。
同时,也将逐步构建小型世界环境,作为后续的移动测试和图形渲染基础。当前我们已经在世界中绘制了这些可行走点,这些点是附着在实体(entities)上的。每一个实体代表一个可站立的位置,这些实体携带了与之关联的可行走点信息。
接下来将修改主角的移动方式,使其真正基于这些点进行逻辑运动。过程可能会有点混乱,但这是必要的一步。
修改 game_sim_region.h 和 game_world_mode.cpp:将主角的头部和身体分离
我们现在要将角色的"头部"和"身体"逻辑上拆分成两个独立的实体,以便能更细致地控制它们的行为。这样做虽然有些复杂,但能帮助我们更好地处理角色与世界交互的逻辑,比如碰撞、渲染、控制等。
在进入游戏世界模式时,我们每当有玩家加入,都会新增一个英雄实体。原本我们只添加一个单独的"英雄"实体,但现在我们将其拆分成两个实体:一个"英雄身体"和一个"英雄头部"。
在代码中,我们将添加两个不同的实体类型:EntityType_HeroBody
和 EntityType_HeroHead
。这两个实体会分别添加到游戏中。当前设想是由"头部"来响应玩家输入进行控制,而"身体"是用于存储例如生命值等信息,未来熟悉系统或 AI 也会可能以"身体"为目标。
同时,为了避免两个实体在物理上相互碰撞并阻止移动,我们重新定义了碰撞体积:
- "英雄头部"拥有较高的碰撞体积(例如高度为 0.7),表示其漂浮在身体上方;
- "英雄身体"拥有较低的碰撞体积(例如高度为 0.6);
- 中间设置了一个轻微的间距(例如 0.1)以避免重叠导致的碰撞冲突。
为了实现这个逻辑,我们添加了碰撞体偏移 OffsetZ
,让"头部"的碰撞体可以浮在空中,而不是贴地面。这样,虽然两个实体在空间上重叠,它们的碰撞不会干扰彼此。
然后我们修改了控制逻辑,使玩家实际控制的是"头部"实体,而不是"身体"。我们还调整了控制器与实体之间的映射关系,确保控制器能准确对应到"头部"实体上。
在渲染方面:
- "身体"会绘制影子、躯干、披风等;
- "头部"只绘制头部贴图,避免重复绘制;
- 粒子效果代码被移除了,以简化调试,并准备日后整理到更合理的位置。
过程中遇到了一些调试问题:
- 初始时控制器仍绑定在"身体"实体上,导致"头部"无法响应输入;
- 修正后,确保
AddPlayer
函数返回的是"头部"的实体索引,控制器才正确作用于头部; - 同时确保碰撞体逻辑也已调整到位,避免默认的地面碰撞逻辑干扰。
目前头部和身体实体已被正确地创建、控制和渲染,两者逻辑上分离但在空间中保持配合。后续将继续清理和重构实体系统,以更系统地支持复杂角色行为。










运行游戏并移动头部
我们目前已经实现了角色的"头部"可以自由移动,这非常重要。接下来的目标是让"身体"自动跟随"头部"移动,但这个跟随不是简单的同步移动,而是有逻辑约束的。
具体来说,我们希望身体的移动遵循"可通行点"的规则。也就是说,当头部移动时,身体不会直接模仿头部的精确位置移动,而是始终朝着离头部最近的一个"可通行点"移动。这些可通行点是我们预先定义好的世界中允许角色站立的具体位置。
为了实现这一逻辑,我们将做以下处理:
- 每帧检测头部当前位置;
- 根据头部的位置,查找离其最近的"可通行点";
- 将身体的位置设定为该最近通行点的坐标;
- 这样可以保证身体只停留在合法位置,符合游戏逻辑设计;
- 同时身体的移动将不会是流畅的连续移动,而是离散跳转到指定点。
这个逻辑的好处在于,它使得游戏角色的运动更明确可控,特别是在一些基于网格或离散位置的交互设计中非常实用,例如固定站位、路径判断、战斗区域限制等。
下一步我们将开始编写这部分的逻辑,让身体按照上述原则实时更新自己的位置,以响应头部的移动行为。
修改 game_world_mode.cpp:让身体沿可行走的路径跟随头部
我们目前正在进行一个模拟系统的开发,重点是让"身体"可以跟随"头部"移动。当前阶段的目标不是追求完美实现,而是尽快完成一个基础可运行的版本,即便实现方式比较粗糙。
具体思路如下:
我们在模拟过程中会让"头部"移动。当"身体"执行模拟逻辑时,它需要在场景中的所有实体中进行搜索,不过这次我们不是在寻找跟随对象,而是在寻找具备"可通行点"(traversable points)的碰撞体(collision volume)。每个实体都拥有碰撞体,因此我们可以安全地遍历所有实体的碰撞体数据。
我们对这些碰撞体中的"可通行点"进行逐一检查:
- 遍历每个可通行点(通过体积的 traversable count 和索引);
- 将每个可通行点转换为世界空间坐标(通过实体的变换信息进行转换);
- 获取"头部"的世界位置;
- 对比该可通行点和"头部"位置之间的距离平方值(以避免开方计算);
- 如果该点是当前距离最近的点,则记录该点的位置;
- 另外需要考虑一个高度限制:只考虑Z轴(高度)低于头部的点,防止"身体"移动到头部之上的位置。不过这部分尚未实现,等坐标系统更稳定后再补上。
一旦遍历完成,我们记录下最近的可通行点坐标,并将"身体"的位置强制设置为该坐标,暂时不添加动画效果,只是直接进行位置设定。这是为了确保功能正常运行,后续再添加过渡效果或插值。
为了让身体知道该跟随哪个"头部",我们在每个实体中设置了一个"head"引用字段,暂时以最简单的方式实现这一机制。每次模拟时,"身体"会读取自己的 head 引用,如果无法找到(比如头部被破坏了),则保持静止。
我们还需要为这个引用字段实现状态保存与读取的功能,因此添加了 load_entity_reference
和 store_entity_reference
的相关调用,以便后续序列化与反序列化工作正常进行。
总之,这一阶段的主要目标是实现:
- 头部可自由移动;
- 身体自动寻找并移动到最靠近头部的、合法的可通行点上;
- 当前实现暂不考虑动画和高度限制,后续迭代会逐步完善这些细节;
- 系统中通过引用机制让身体知道自己的头部是谁,确保能进行位置跟随。
目前重点是保证整个流程"跑通",后续再优化逻辑结构、动画表现和状态管理。





修改 game_sim_region.cpp:引入 GetSimSpaceTraversable 函数
我们正在继续完善身体跟随头部移动的机制。当前阶段重点是通过模拟区域(sim region)来获取可通行点的世界坐标,并将身体实体移动到这些点中距离头部最近的那个。
在目前的实现中,我们尚未引入实体旋转(rotation)的概念,因此变换逻辑较为简单。我们只需将可通行点的局部位置(P)加上实体的位置(entity.P)就能获得其在世界空间中的绝对坐标。虽然未来需要考虑旋转(比如平台旋转后可通行点的位置也要跟着转),但目前我们暂不处理旋转,只做平移。
我们做的具体操作如下:
- 在模拟区域内,我们创建一个方法,根据一个索引获取对应的可通行点;
- 检查索引是否在合法范围内,避免访问越界;
- 获取该可通行点的局部坐标;
- 将其与实体的坐标相加,得到该点的世界坐标;
- 这个过程未来将扩展为一个支持旋转的完整变换操作。
接下来,我们确保"身体"实体能正确找到对应的"头部"实体。我们在构造实体的逻辑中添加设置,即在创建玩家实体时,将身体的 head
引用指向它的头部实体。具体来说,就是在 AddPlayer
过程中,把头部的实体索引赋值给身体的 head
引用字段。这样一来,身体在模拟过程中就能知道它应该跟随哪个头部。
最后,还调试了一个for循环的错误。我们发现一个死循环问题,原因是循环变量没有正确递增,导致程序卡死。这种问题虽简单但常见,需要注意循环控制变量的处理。
总之,本阶段实现了以下几个关键点:
- 可通行点的世界空间坐标转换方法;
- 身体通过引用能识别并关联到自身对应的头部;
- 修复了循环错误,保证模拟逻辑能正常运行;
- 暂不处理实体旋转,后续将加入对旋转后的坐标变换支持;
我们正在逐步构建一个稳定且具扩展性的角色控制系统。

断点没进来


Head.Index 一直等于0吗



还是不跟着移动



运行游戏,看到身体跟随头部
我们已经实现了身体根据头部移动,并且表现效果完全符合预期。接下来调整了摄像机的行为模式,以使其配合新的系统运行方式。
之前摄像机还是按照旧的方式进行移动,但现在我们希望摄像机保持"基于房间"的模式不变,这样更符合当前逻辑。因此我们更新了设置,让摄像机的行为固定为"room based"(基于房间的视角),这意味着摄像机会锁定在当前房间视角,而不会随实体自由移动。这个设置调整在 config.h
或相关区域完成,通过设置 global_render_camera_room_based
为 true 实现。
完成这些设置后,系统整体运行状态良好。特别值得一提的是,当前身体跟随头部的逻辑不依赖于网格系统,也就是说就算没有明确的网格布置,该系统依然能正常工作。身体只会跟随到实际存在"可通行点"的区域,因此不会出现身体跑到无效区域或悬空位置的情况,这一点也很理想。
此外我们重新启用了碰撞系统,也就是说参与模拟的实体现在再次拥有碰撞检测功能,确保角色之间或角色与环境之间的交互行为是合理的。
总结目前已经实现的关键改动:
- 摄像机行为调整为"基于房间"的模式;
- 摄像机设置改为启动时自动保持房间视角;
- 实现了身体根据头部移动,并严格依赖可通行点;
- 移动逻辑不依赖网格系统,适应性更强;
- 重新启用了实体碰撞检测功能;
整体系统更加完善,行为更符合设计预期,也为未来扩展提供了良好基础。
修改 game.cpp:游戏启动时直接进入游戏而不是播放过场动画
当前我们在程序启动时默认进入了过场动画,但由于我们正在开发游戏本体的逻辑,因此希望在启动时直接进入游戏世界,而不是先播放开场动画。这样可以节省调试和测试的时间,提高开发效率。
默认情况下,游戏状态中的 game_mode
被设置为执行播放开场动画的逻辑(play_intro_cutscene
),但我们希望临时将其替换为直接进入游戏世界的模式。为此,我们找到了对应的状态切换函数 play_world
,这个函数应当负责设置并进入游戏世界,因此我们将 game_mode
初始化时直接调用 play_world
。
在修改完后,我们预期游戏启动后应该直接跳转到游戏世界场景中,而不是先展示片头动画或标题画面。
然而,在实际执行中,发现程序进入游戏世界失败,主要问题出现在 world_mode
是空指针(null),导致游戏世界无法被正确初始化和显示。
我们意识到 play_world
函数虽然被调用了,但其中用于初始化世界模式的操作似乎并没有被成功执行。这是一个需要修复的问题,因为从设计上看 play_world
理应可以被作为初始状态调用。如果不能正常工作,说明当前世界模式的创建和配置过程中存在逻辑缺陷或遗漏。
接下来我们将深入排查 world_mode
为 null 的原因,并确保 play_world
能够在启动阶段正确设置游戏世界,使得游戏在调试阶段能直接进入主内容。这个修改是临时的,仅用于开发期间,后续发布时仍会恢复原始的启动流程。

调试器:跳转到 PlayWorld 函数并进行调试
在我们深入分析游戏启动流程并尝试绕过过场动画,直接进入游戏世界的过程中,发现了一个令人困惑的问题:在尝试手动调用 play_world
来初始化游戏世界时,game_state.world_mode
却没有被正确设置,导致游戏世界并没有被正确载入。
我们仔细回顾了游戏初始化过程中的一系列操作。游戏状态(game_state
)会被初始化,期间分配一些任务结构、暂态模式、环境贴图等等,这些都没有问题。但当我们检查 world_mode
时发现,它只是一个空指针,根本没有被分配或初始化。
从已有逻辑看,play_cutscene
等函数中是会主动设置 game_state.world_mode
的,也就是说,在播放过场动画时,它会在内部创建并指向一个 world_mode
对象。但 play_world
函数中并没有类似的逻辑,它只是创建了 world_mode
的内容(通过 push_struct
),但没有将这个创建的 world_mode
实例赋值给 game_state.world_mode
,因此外部始终拿不到正确的指针。
进一步追踪调试时,我们使用数据断点观察 game_state.world_mode
的写入过程,发现在初始化阶段确实没有任何地方设置了它,直到过场动画开始播放,才被写入。这意味着之前游戏能正常进入世界场景的原因,仅仅是因为走了过场动画逻辑,而非因为 play_world
函数本身正确地设置了世界模式。
我们最后发现,这一切困惑的根源在于 world_mode
实际上是个联合体(union)中的一部分------也就是说它是 game_state.transient_storage
里不同模式之一的重用内存块。当 play_cutscene
被调用时,它会通过 push_struct
创建 world_mode
并正确赋值;而我们在手动调用 play_world
时,仅创建结构体而没有设置指针,导致逻辑失败。
因此,我们要修复这个问题,只需要在 play_world
中显式地将新建的 world_mode
指针赋值给 game_state.world_mode
,与 play_cutscene
的处理方式一致。只有这样,我们才能绕过过场动画并在启动时直接进入游戏世界,从而提升开发效率。

这太离谱了,我们终于搞清楚这个问题为什么"居然还能工作"。实际上,它能正常运行完全是一个巧合,根本不是代码写得对,而是"运气好"。
具体来说,在初始化游戏世界时,我们并没有显式地将新建的 world_mode
指针赋值给 game_state.world_mode
。理论上,这样做的话 game_state.world_mode
应该是个空指针,后续逻辑应该直接崩溃。然而,实际运行中它却"神奇地"工作了。为什么?
通过调试我们发现,push_struct
在分配 world_mode
时,恰巧分配在了与之前一个模式(如 cutscene_mode
)完全相同的内存地址 上。这意味着虽然我们没有手动设置指针,但由于内存地址正好一致,game_state.world_mode
实际上仍然指向了那个有效的世界模式对象,仿佛一切都正常。
我们打印出地址验证了这一点:当 cutscene_mode
分配时,它用的是某个地址;后来我们执行 play_world
分配 world_mode
,结果地址完全一样。这说明内存分配器恰好复用了同一个地址,而这个复用行为是不可预测的,也就是一个纯粹的"运气"。
换句话说,代码里少了一行关键赋值 ,只是因为堆栈分配结构体时内存位置刚好一样,才没暴露出 bug。这是非常危险的行为,因为一旦内存分配策略稍有变化(比如增加新的结构体、改个顺序等),就会导致 game_state.world_mode
指针失效,进而引发程序崩溃或未知行为。
总结:我们发现一个完全依赖偶然的逻辑漏洞,根本原因是缺少显式的指针赋值,而目前运行正常只是因为堆栈内存复用刚好让指针"看起来像是对的"。这个问题必须立即修复,以防未来出现更严重的潜在 bug。



修改 game_world_mode.cpp:在 PlayWorld 中设置 WorldMode
我们发现了一个至关重要但原本完全缺失的代码,这一行代码非常关键,必须明确地加入到初始化逻辑中才能保证游戏世界模式(world mode)被正确设置。
原来程序之所以"能跑",是因为之前碰巧通过内存复用的巧合,让 game_state.world_mode
指针误打误撞地指向了正确的对象。现在我们已经补上了这条关键赋值语句,系统行为才真正变得可预测、可控。
验证补丁后,一切看起来正常。然后我们尝试重新切换到原来的意图 ------ 启动时跳过片头动画(cutscene),直接进入游戏世界。我们关闭 cutscene 后,程序理论上应该立即进入 world mode,因为我们已经将 game mode 设置成了 world,所以应该调用启动 world 的初始化流程。
但此时我们注意到,程序并没有进入 world,而是进入了标题画面(title screen)。这说明虽然我们设置的是 world 模式,但游戏逻辑实际上并没有走进 world 的分支。经过分析后我们意识到:
当前没有玩家存在
这是问题的根源。在没有玩家的情况下,世界模式(world mode)并不会进行后续处理和渲染。换句话说,哪怕模式切换正确,如果没有任何玩家实体被初始化,那么整个游戏世界逻辑也不会"激活"。
总结如下:
- 修复了之前缺失的
game_state.world_mode
赋值逻辑,解决了靠运气运行的问题; - 成功地让程序在启动时绕过 cutscene 逻辑;
- 但发现没有玩家实体的情况下,world mode 不会触发渲染或更新;
- 接下来必须显式初始化玩家,否则即使启动进入 world,画面也会停留在 title screen。
整体过程揭示了初始化流程中多个依赖项之间的微妙耦合:game mode 设置、实体初始化、渲染逻辑都必须正确协调,才能让游戏按预期启动和运行。
修改 game.cpp:强制设置开始按钮为已按下
我们遇到了一个逻辑问题:当进入游戏模式后,如果当前没有任何玩家存在,系统会自动回退到标题画面(Title Screen)。也就是说,游戏世界成功加载,但由于没有英雄实体被创建,游戏立即退回到了社交界面。这是一个在开发和调试阶段非常容易被忽视的逻辑缺口。
具体流程如下:
- 我们将初始模式设置为了"世界模式"(world mode),跳过了过场动画;
- 但由于没有任何玩家加入,游戏在检测时发现没有英雄存在,于是自动退回了标题画面;
- 当前代码的行为是,如果没有玩家或英雄,默认就回退到社交界面/标题画面;
- 因此,需要手动或自动创建一个初始玩家才能避免这种情况。
为了解决这个问题,我们考虑强制在启动时创建一个玩家。其实并不复杂,我们可以模拟一个"按下开始键(Start)"的输入事件,通过伪造一个控制器(controller)输入来触发玩家加入:
- 比如我们可以手动写入一个控制器的状态,让它表现得像玩家按下了"Start";
- 这种方式虽然有些"硬编码",但在调试阶段是完全可接受的;
- 当然更理想的方式是将这一行为显式、结构化,比如通过一个统一的"启动游戏并添加玩家"流程来处理。
此外我们还考虑到了:
- 当前只是用于调试目的,所以这种伪造输入的方法也没那么糟糕;
- 长远来看,最好提供一个更加正式和结构化的初始化接口,比如一个明确的
PlayWorldAndAddPlayer()
函数; - 最终结果是,我们通过模拟"Start"输入事件,成功让游戏在启动后保持在游戏世界中,而不会意外回退到标题画面。
总结:
- 原因是进入 world mode 后没有任何英雄,触发了退回逻辑;
- 解决方法是模拟控制器输入,让一个玩家自动加入游戏;
- 虽然这种方法略显临时,但对于开发流程非常有帮助;
- 更优的方案是构建一个结构化的接口来初始化世界并添加玩家。
运行游戏,直接进入游戏界面
我们目前通过一种方式模拟了玩家按下"Start"按钮,从而在游戏启动时自动加入一个英雄角色。这使得即使仍然是从过场动画(cutscene)状态启动,我们也能够直接进入游戏世界,无需显式调用"play world"函数。这种处理方式灵活而高效,非常适合开发阶段快速调试。
具体操作逻辑如下:
- 模拟输入:我们重写了输入系统中的"Start"按钮,使系统认为有玩家按下了开始键;
- 自动加入玩家:这种模拟输入触发了原有的逻辑,即当玩家按下"Start"时,会创建并加入一个新英雄;
- 保留原有流程:尽管依旧从过场动画进入,我们不再强制调用"play world",而是通过模拟的方式让游戏自然地进入世界状态;
- 调试灵活性提升:这种方法大大提高了我们调试时的效率,不用每次都等待过场动画或进行繁琐操作;
- 兼容性好:因为这种模拟输入方式是利用已有逻辑处理的,所以不会破坏整体结构或流程控制。
接下来,我们开始关注一个新的问题 ------ 每帧的Δ时间(Delta Time, 简称DT)表现。这是游戏运行时衡量帧率稳定性和时间同步非常重要的参数。我们希望观察在当前逻辑下,帧间Δ时间的变化趋势。
此外,我们还提到,有人在 GitHub 上报告了与 Δ时间相关的 bug:
- 我们尝试登陆 GitHub,查看相关 issue;
- 找到了"issues"部分,准备浏览用户所反馈的内容;
- 初步线索是用户提到 DT 的某种"错误行为"或"异常表现"。
总之,这一阶段我们完成了以下几项关键操作:
- 利用模拟输入,在启动时强制加入一个玩家,从而顺利进入游戏世界;
- 避免了强制跳过流程的粗暴做法,保留了完整结构;
- 准备排查 Δ时间的相关 bug,提升运行效率和表现稳定性。
修改 win32_game.cpp:修正 GameUpdateHz 的计算
我们发现目前帧时间(Δ时间)计算存在严重问题,系统传入的值与真实刷新率严重不符,误差可能达到两倍以上。这主要是因为系统关闭了垂直同步(vsync),而且在早期阶段为了调试方便,我们人为地绕过或忽略了某些与刷新率相关的机制。
一位开发人员指出了这个问题,并准确捕捉到了这一异常。这属于早期调试阶段留下的遗留问题,最初我们并没有真正实施准确的刷新率控制逻辑,仅仅是"假装"在进行帧同步。这种处理方式在正式开发逻辑前还能容忍,但随着游戏逻辑逐步完善,我们必须切换到真实且精准的帧时间控制机制。
为此,我们进行了以下几个关键调整:
-
启用 vsync 测试环境:我们确认在当前所有测试和开发环境中都应该启用垂直同步(vsync),从而确保帧率与屏幕刷新率同步;
-
修复目标帧秒数(target seconds per frame):我们检查了代码中使用此目标值的位置,并确认该值由一个预设的"game update hertz"推导而来;
-
使用正确的帧率常量 :如果 vsync 始终启用,那么我们应使用其对应的刷新频率(例如 60Hz),这意味着每帧时长应为
1.0 / 60.0
秒; -
为后续游戏逻辑打下基础:这是一个至关重要的前置修复步骤。只有确保帧时间的正确性,我们才能在后续实现准确的角色运动、动画播放、物理仿真等核心功能,否则所有时间驱动的逻辑都将出错。
通过这项修复,我们迈出了游戏性能与稳定性控制的重要一步,为接下来的开发工作打下了坚实基础。
运行游戏并以更正确的速度移动
我们发现之前的运行速度略显不稳定,导致画面表现略微抽搐或"跳帧"。这也间接说明过场动画的时序可能不准确。不过目前观察来看,动画效果还算正常,画面节奏与预期差别不大。
当前的过场动画还没有与任何音频轨道进行同步,因此动画播放速度实际上完全取决于我们设置的时长或者说未来音频的长度。因此,只要最终音频素材确定,动画节奏便可以进行适配。目前看到的播放速度也大致符合我们的预期,所以算是稳定下来了,这点令人满意。
另外,在这次改动之后,我们也顺便对主角的运动逻辑做一个简单的测试。我们尝试在世界模式下对主角的身体进行点对点的移动动画,以验证新系统下实体动画表现的正确性与流畅度。
这次测试主要是:
- 检查英雄(主角)身体在多个坐标点之间的移动效果;
- 验证新修复的帧时间控制逻辑是否能正确驱动动画;
- 初步感受整体逻辑是否能匹配未来更复杂的动作系统。
虽然时间有限,还无法完成完整的运动系统构建,但我们已经迈出了关键的第一步。这将为后续更丰富的角色控制与动画表现打下良好基础。
修改 game_world_mode.cpp:使身体朝向可行走点加速
我们在 dot cpp
中的 sim_region
内进行了一些关于角色移动逻辑的实验。目标是让主角身体根据某种逻辑进行点对点的移动测试,以验证动画系统的运作情况。我们尝试采用先前熟悉的运动方式,通过与主角的相对位置来计算移动向量(DP)和加速度(DDP),从而驱动实体运动。
首先,我们复制了之前的一套基于与主角之间距离的加速度计算方法,但对一些逻辑进行了简化。在处理最近点(closest P)时,我们直接用了原始代码段,而不是以往的更复杂处理。但也注意到,如果最近点没有有效找到或距离太大,平方根函数(用于距离归一化)可能会导致无意义的大值。为了避免这种情况,我们设置了一个上限,比如"如果两个对象相距超过1000米,就不移动",以保证数值安全。
接下来,我们试图通过设置实体的 move_spec
来启动实际的物理运动,并检查是否有加速度和速度更新。过程中发现角色完全没有移动。首先怀疑是碰撞检测导致阻碍,于是将碰撞功能暂时禁用(entity.collision = world_mode.no_collision
),但依旧无效。
进一步调试后,我们检查了 closestP
、mvP
等相关位置数据,发现这些数据本身是有效的,也就是说数学计算是有结果的,DDP
(加速度)也正常。然而最终结果 entity.dp
(速度)却是完全错误的,像是未初始化一样。
经过逐步排查,最终发现是逻辑疏忽:如果两个实体起始位置过近,或者恰好重叠,那么归一化方向向量就会失效(因为模长接近于0或等于0,单位向量无法计算),导致运动方向丢失,因此角色无法获得有效的速度或加速度。
为了解决这个问题,我们重新加回了"最小距离判断"逻辑:如果目标点与实体当前位置距离过近(即在某个极小阈值内),就不尝试进行移动,这样避免了除以零或接近零的问题。
这个过程也再次提醒我们:哪怕是非常基础的物理逻辑,如果缺少边界条件判断,也可能导致整个行为系统失效。因此完善数值处理和特殊情况的容错能力是物理逻辑构建中的关键一环。
运行游戏并看到身体朝头部加速靠近
现在我们实现了基本的角色身体追踪逻辑,并初步测试了角色的自动移动表现。当前虽然动画部分还没有完全处理完善,但我们已经可以观察到追踪行为的基本效果。
最初我们对加速度(acceleration)在整体逻辑中的必要性产生了疑问。因为根据当前的结构,速度的决定因素其实是直接通过位置差计算出的向量,而非由加速度逐帧积分而来。换句话说,我们之前使用的"familiar"代码片段似乎存在冗余,它做了多余的运算,甚至有些部分可能完全没有实际意义,因为底层代码已经自动处理了速度的计算。
进一步优化过程中,我们尝试直接指定 DDP
(加速度)为单位方向向量与目标距离乘积的结果,也就是目标加速度向量。验证之后确认这种方式完全可行,系统能够自动将其转化为合理的速度和位置更新。这一发现表明之前的代码确实设计不合理,存在多余逻辑,我们可以将其精简。
之后我们调整了物理参数中的阻尼系数(drag),增加其数值以使角色身体更加紧密地跟随头部移动。阻尼越高,代表系统会更快地抑制速度变化,从而让身体几乎立即响应头部位置的变化。虽然过高会显得过于僵硬,但适度的提高确实可以带来更紧凑的跟随感。最终达成的运动效果与预期较为一致,能比较自然地表现出"身体跟随头部"的行为。
总结来说,目前:
- 追踪逻辑已经基本完成;
- 冗余的运动代码被剔除;
- 通过调整 drag 参数优化了角色响应;
- 虽未实现动画插值,但运动曲线大体令人满意;
- 角色能够顺畅移动,身体能较好地跟随目标部位。
下一步计划是完善动画系统,使该移动逻辑配合帧动画实现视觉上的连贯动作。此外,可能还需进一步精细调参,以增强整体流畅度和表现力。整体来看当前状态已经进入较为稳定阶段,可作为后续角色控制和动画系统开发的基础。
目前角色的身体追踪逻辑已经基本完成,角色的身体会持续跟随主控制部位(头部)进行移动。由于我们确保了角色只能停留在可通行区域(traversable),因此角色的身体不会在不该停下的地方停止或卡住。这一机制保证了整个移动逻辑的连贯性与物理合理性。
接下来的重点工作将集中在角色动画表现的完善上,尤其是视觉上如何让角色的身体和头部保持自然的连接。例如,目前头部与身体仍可能在视觉上出现"脱节"感(detached head),这是需要解决的视觉问题。为此,需要设计和实现更细腻的过渡动画或骨骼绑定机制,以确保角色各部分运动协调一致。
除动画优化外,还需要增加一个功能:在特定条件下让头部"自动对齐"或"回正"身体的位置,也就是我们希望头部能够自动吸附到合适的状态。实现这个功能的目标是使角色在没有主动输入时,也能逐渐回归到一个合理的站立姿势或者中心位置,从而避免奇怪的姿势或错位。
总结当前状态与下一步目标:
-
当前角色身体只会停留在允许通行的位置,避免非正常停顿;
-
身体可以平滑地追踪头部,移动逻辑基本正确;
-
下一步是完成动画表现,包括:
- 头部与身体的自然连接;
- 动画补间过渡;
- 姿态矫正机制(如自动回正);
-
整体效果已达到可用状态,后续工作将提升视觉表现与使用体验。
继续推进这些方向,将有助于提升角色控制的自然性与游戏整体品质。
"我希望在你停止操作时头部能立刻回到身体上"γ
当前我们已经实现了角色身体在移动时能够平滑地跟随头部移动,这在玩家持续输入控制方向(如按住右键移动)时效果良好,追踪逻辑运行正常。然而,为了使角色移动逻辑更加自然,我们希望在玩家停止输入时,头部能够自动回到一个"中立"或"自然"的位置,也就是靠近身体原本应在的位置。
换句话说,当前逻辑下,头部在玩家操作时可以移动离身体,但一旦操作停止,头部仍保持在先前位置,显得不够自然。我们理想中的效果是:当输入停止时,头部会以一定的速度逐渐回归身体所在的默认位置,实现自然的回弹或归位动作,从而让角色整体姿态始终保持协调统一。
为此我们需要在逻辑中增加一个判断:当没有任何控制输入时(如方向键或手柄摇杆处于静止状态),我们将头部的位置目标重设为身体的中立位置,并启动一个缓动(插值)机制,令其逐帧平滑地靠近这个目标。这种机制有点类似于"弹簧"或"惯性回弹",既保持了物理感,也增强了操控的手感。
总结目前所需调整与优化方向如下:
- 玩家控制输入时,保持当前头部与身体分离状态的追踪逻辑;
- 玩家停止输入后,触发头部"回正"机制,使其逐渐向身体默认位置靠拢;
- 实现平滑插值,避免突兀的跳跃或闪动;
- 这一机制将显著提升角色姿态自然性和游戏整体体验;
- 后续可进一步配合动画系统,让视觉表现更加统一完整。
当我写事件驱动代码时,经常不得不使用动态内存分配来保留状态直到回调使用。你是如何处理这种情况的?
在编写事件驱动代码时,通常需要使用动态内存分配来保持一些状态,直到回调函数需要它们。在这种情况下,动态内存分配并不一定是坏事,关键在于你如何管理内存。
首先,动态内存分配本身并不是问题,问题在于你是否清楚自己在做什么。常见的解决方式之一是使用"空闲列表"(free list)。空闲列表是一种简单的内存管理方法,通常用于处理固定大小的内存块。当你知道大多数内存分配的大小是固定的,而且分配的次数也比较少时,可以创建一个特定大小的内存池。比如,如果你知道大部分内存分配都是64字节,并且最多需要1000个这样的内存块,你就可以创建一个64字节的空闲列表,并且内存池的总大小为1000个64字节。这样,当你需要内存时,直接从这个内存池中获取,并且在不需要时归还给空闲列表。这样就可以避免常规的内存分配过程,降低内存分配的开销。
这个方法的好处是,不需要复杂的内存分配管理,内存分配和释放的速度非常快,通常只有两到十条指令,且不存在内存碎片问题。你知道你只需要这些特定大小的内存,使用起来高效且没有额外的复杂性。
所以,关键的问题不在于是否要避免动态内存分配,而是在于你是否真正需要一个通用的内存分配器。很多时候,我们使用动态内存分配器(如new和delete)只是因为我们学过它们,但可能没有深入思考过代码的实际需求。可能我们并不需要通用的内存分配器,而是只需要针对特定需求进行简单的内存管理,这样效率更高,代码也更清晰。
总的来说,动态内存分配是可以接受的,问题的关键在于如何通过合理的方式管理内存,避免过度复杂的分配机制,确保代码的高效和可维护。
游戏调试视图是否可以查看像 world mode 这样的结构体?
在调试时,可以通过打印出结构体的内容来查看其具体值。例如,若想查看 world mode
中的内容,可以直接打印出相应的值。对于一个布尔类型的变量,当角色在斜对角方向移动时,可能需要额外的判断和处理,因为斜对角的运动通常涉及更复杂的坐标计算,需要在代码中适当地处理这种情况。
当对角移动时,玩家是否能清楚身体将如何走到对角实体?现在看起来似乎是随机的
对于角色在斜对角方向的移动,当前的表现看起来有些随机,可能会让玩家感到困惑。因此,是否允许斜对角移动还需要进一步考虑。如果决定允许斜对角移动,那么可能会在代码中做出调整,确保在特定的区域内(例如中间区域)触发斜对角的移动,而不是选择直线移动。这样可以避免在斜对角方向时,角色的运动看起来不自然或不一致。实际上,通过一些简单的调整,应该可以轻松实现这一功能,确保玩家在斜对角移动时能够获得更流畅的体验。
修改 game_world_mode.cpp:引入 DesiredDirection,使身体更直接移动到头部
有两种方法可以实现这个目标。当前的方法是通过遍历所有点,选择距离头部位置最近的点。但也可以采取另一种方法:计算一个理想的方向向量,理想方向向量是从身体到头部的向量。通过这个向量,可以确定一个理想的运动方向。
然后,测试时不再是计算点之间的距离,而是测试移动到某个点与当前运动方向的对齐程度。具体来说,就是通过点和目标方向的内积来衡量它们之间的相似性,进而决定哪个点最符合理想方向。为了确保方向一致,可以使用单位向量(归一化后的向量)来进行测试。这样可以更准确地选择与目标方向最接近的点。
修改 game_math.h:引入 NOZ(Normalize 或 Zero)函数
首先,提到的 normalizer_zero
函数是一种自动的 epsilon 检查,它的作用是计算一个向量的长度,如果长度大于某个阈值(epsilon),则归一化该向量,否则返回零向量。这个操作避免了对非常小的向量进行归一化,防止在计算中出现不稳定性。
接下来,介绍了如何计算和使用方向向量。在这个过程中,首先计算的是"从头部到目标点"的向量和"从身体到目标点"的向量。然后,通过内积运算,可以测试"从身体到目标点"的向量与"理想移动方向"之间的对齐程度。内积的结果表示两个向量之间的夹角,取值范围从 -1 到 1,分别表示完全反向、正向和垂直。
然而,内积的值会随着目标距离的增加而被放大,这并不符合期望。为了修正这一问题,需要对这个向量进行归一化,使得结果仅反映角度的对齐程度,而不受距离的影响。
此外,为了提高效率,可以将这一计算放在内层循环中,只在当前点更优时才进行相关计算。最后,结合距离和方向对齐程度,可以优化目标选择,确保选择最近且与理想方向最一致的点。这种方法不仅更准确,还能避免因为距离过远而导致的误差。



运行游戏并测试新移动逻辑
如何筛选出那些大致与头部朝向一致的目标点。目的是通过内积运算来判断目标点的方向与头部朝向的对齐程度。如果目标点的方向与头部朝向的方向大致一致,则该目标点会被认为是一个有效的选择。
同时,提到了一些内存计算的细节,通过解释如何将这一筛选操作应用到整个过程,确保在选择目标点时,优先考虑那些方向对齐的点。而"在中间能成立"的问题似乎指的是某些逻辑或数学操作在特定条件下能正常运行,可能是在解释为什么某些筛选条件能够生效时,给出了对其背景和实现方式的理解。
修改 game_world_mode.cpp:有条件地设置最近点
如何调整选择目标点的策略。首先,提到为了避免总是选择距离最近的点,需要引入一个新的条件判断。这是为了确保选择的点不仅距离近,而且方向上也对齐。
为了解决这个问题,可以使用一个布尔变量来跟踪是否已经选择了一个目标点。若尚未选择,就会使用一个标志位(例如 pickany
),然后通过比较"测试方向"与当前选择的点来决定是否更新目标点。
此外,增加了一个判断条件:如果还没有选择目标点,或者当前测试的方向更合适,则更新选择。这样做是为了确保最终选择的目标点不仅仅是最近的那个,而是最符合预期方向的那个。
总体来说,这是在优化目标点选择的方式,确保选择的目标点符合方向上的要求,而不仅仅是距离上的要求。
运行游戏并看到身体更接近目标点停止
在处理代码时,遇到的问题是如何处理"wiggliness"(波动性),主要是对目标方向的选择和判断逻辑不够精确。当前的思路是,当目标位置发生变化时,系统需要根据当前位置来选择一个新的方向。这个选择需要基于一些测试逻辑,但目前这个测试还没有完全做好,导致有时目标方向无法准确判断。
理想的逻辑是,如果当前没有选定的目标方向,并且所有可能的方向都没有通过测试,那么可以选择一个最接近的方向继续前进。如果没有找到一个通过测试的方向,那么就需要根据具体情况选择一个方向,并调整策略。
随着系统逐步运动,关键点在于判断哪些方向是合适的,哪些方向是无效的,而在目前的实现中,判断逻辑仍然有些不足。下一步需要进一步改进对目标方向的判断,并确保选择的方向符合预期的运动轨迹,从而避免不必要的波动。
修改 game_world_mode.cpp:恢复原来的移动方式
在编写代码时,发现需要更多的时间来完成这一部分,因为有很多因素需要考虑。首先,需要清楚地知道起始点的位置,确保始终能够使用最初的起点。这一点非常重要,因为在进行动画处理时,我们总是需要依赖于最初的起始点。
目前的代码实现还不够完善,不能直接替代现有的逻辑,所以决定暂时保持原有的实现方式。虽然当前的代码还不够好,不能直接替换,但未来有可能进行替换。实现这一步并不困难,关键是确保拥有正确的变量来跟踪运动的状态,以便准确地知道原来的运动轨迹。
此外,还需要处理头部的"回弹"效果,这要求我们在代码中加入相应的处理逻辑。总体来说,接下来的工作会包括调整变量和代码逻辑,以确保动画的正确性和流畅度,最终实现所期望的效果。
这可能是错觉,但看起来身体对齐在瓦片的角落。通常在基于瓦片的游戏中,实体位于瓦片中心。这是有意为之吗?
看起来像是身体在朝着瓦片的角落移动,这可能只是视觉上的错觉。在大多数基于瓦片的游戏中,实体通常占据瓦片的中心位置。实际上,当前显示的是瓦片的可行走点,而不是瓦片的角落,因此瓦片的碰撞体积目前并未包括地面瓦片。
需要注意的是,当前的碰撞检测仅限于"可行走点",而不涉及瓦片的角落部分。要解决这个问题,可能需要在地面瓦片的碰撞体积上进行调整,确保它能够正确处理实体与地面之间的互动。目前,地面瓦片的碰撞检测尚未完全实现,需要进一步完善。
修改 game_world_mode.cpp:让 MakeSimpleFloorCollision 绘制瓦片
如果将碰撞体积部分加入进去,假设设置了一个体积计数为1,并且为其定义了相关参数,那么就可以实现碰撞体积的正确检测。通过添加这些内容并绘制出来,效果将更加明显。接下来,代码会在启动时自动处理这些设置,这样就能在实际运行中看到碰撞体积的效果。
通过这种方式,可以更清晰地观察到碰撞体积对游戏世界中的实体运动和交互的影响。

运行游戏,看到瓦片并在其上移动
瓦片本身可以清楚地看到,实体确实是站在瓦片的中心位置上的。这也验证了之前关于实体在瓦片中心而非角落的判断是正确的。通过可视化的方式进一步确认了瓦片绘制的位置和实体的站位逻辑是一致的。
此外,在循环过程中还有其他视觉线索可以辅助判断,例如实体与瓦片之间的对齐方式、移动路径的过渡效果,以及在帧更新时位置的稳定性等。这些细节共同说明当前的实现是按照瓦片中心来作为实体的定位基准,从而确保了空间逻辑的一致性和动画表现的自然性。