游戏引擎学习第274天:基于弹簧的动态动画

回顾前一天内容,并为今天的工作设定目标

我们昨天展示了一些内容,现在先回顾一下昨天的进展。我们目前正在处理的是角色跳跃的动画------特别是身体部分的跳跃。

现在角色的动画状态如下:

  • 正在实现角色的移动和跳跃。
  • 跳跃中已经加入了一些预备动作(anticipation),例如起跳前的下蹲准备。
  • 跳跃轨迹呈抛物线形状。
  • 落地时也加入了相应的动作处理。
  • 动画整体开始有了一些像样的效果,看起来比较自然。

不过,这些还只是初步阶段,我们还有许多工作要做:

  • 比如让头部和身体连接,现在头部是分离状态,但最终的设定是要连在一起的,他并不是"断头"角色。
  • 我们还需要处理角色在动作过程中的倾斜(leaning),这部分目前还没完成。
  • 整体动画系统的各个方面我们还没有完全讲完,现在是一个一个地逐步讲解和实现。

我们还打算在调整动画的同时,用时间步进(timestep)的方式慢动作查看动画效果,从而更清楚地看到细节、便于调试和调整。

总之,现在我们正在逐步推进角色动画系统的开发,并将在过程中逐一讲解实现细节。

修改 game.cppgame_config.h,通过调试系统使 Global_Timestep_Percentage 可编辑

我们现在要做的是减慢整个程序的时间步长,以便更好地观察动画效果。

为了实现这一点,我们计划将时间步长变成一个可调控的变量。具体来说,就是在模拟系统中加入一个可调的参数,比如叫做 Global_Timestep_Percentage,让我们可以手动控制时间流逝的速度。

这个变量初始值为100%,代表正常时间流速。我们之后可以根据需要调小这个值,比如设置为50%来减慢一半时间。

在游戏更新和渲染函数中,会传入一个 dt 值(每帧时间差),这是我们用来计算当前帧经过了多少时间的关键。虽然目前在代码中这个 dt 似乎还没有被使用,我们计划从这里开始处理。

我们会用 Global_Timestep_Percentage 来乘上原始的 dt 值,然后再除以100,这样就能根据百分比来控制时间快慢。例如:

  • 设置为100,相当于时间按正常速度推进;
  • 设置为50,时间减慢一半;
  • 设置为0,就相当于暂停时间推进。

这个变量会影响整个模拟逻辑,确保所有相关系统的时间推进都能按照统一的步长执行。

此外,还有一个小的技术细节说明:对于这种值类型的变量(比如浮点数),我们不需要特别使用 b32 这种类型,只有布尔值时才需要那种声明方式。

总之,通过加入一个全局可调的时间步长百分比变量,我们实现了在运行时灵活控制模拟时间速度的功能,这对调试动画细节和物理系统行为非常有帮助。

改回正常速度

运行游戏并降低 Global_Timestep_Percentage 的值

现在我们已经完成了角色移动的基本逻辑。如果我们想更清楚地观察动画效果,只需要调低我们之前设置的全局时间步长百分比变量,比如设置为50%就是一半速度,25%就是四分之一速度。这样就能以慢动作的方式观察角色的跳跃动画。

此时我们可以看到角色在跳跃的过程中的动作效果,包括向前跳跃、落地之后的反弹等。整体看起来还可以,动画表现力也不错。但我们今天想要重点解决的,是在角色连续跳跃多个格子时出现的一些不自然的弹跳行为。

具体来说,当角色连续跨越两个格子时,动作会出现额外的"弹跳"感:先下沉一次、上升、落地时再下沉、然后又上升跳起。这就导致画面上出现了两次下沉和上升的动作。而如果现实中一个人快速连续跳跃,他不会每次落地后都做完整的下沉和起跳动作,而是直接落地、蹬地,再起跳,这个过程会更加连贯。

问题的根源在于我们当前的跳跃逻辑是"预设动画"的方式------也就是所谓的"canned animation"。跳跃就是一个完整的固定动作序列,包括下沉、上升、跳起、落地再下沉、再回到站立状态。就像是让一个美术人员在动画软件里制作了一整套跳跃动画一样,这套动作不会根据上下文动态调整。

然而我们现在想实现更自然的运动效果,就需要考虑"上下文",比如判断当前跳跃是否是连续跳跃的中间部分,如果是,就不需要在落地时再重复下沉和起跳的完整动作。

我们不打算继续沿用这种"死动画"式的方式来处理所有的跳跃情况。相反,我们今天的目标是演示如何把这些原本是固定控制的动画逻辑,转化成部分受物理或"伪物理"控制的系统。通过对其中某个动作阶段的动力学进行模拟,使跳跃过程更自然、更连贯。

当然我们也可以通过扩展固定动画的方式来解决这个问题,比如增加专门用于连续跳跃的动画片段,在识别出连续跳跃的情况时播放。但这样做的复杂度会不断上升,需要制作更多动画,逻辑也会变复杂。

而如果适当地引入一些物理模拟的方式,不仅能节省动画制作的时间和成本,还能让动画系统更具有可拓展性和表现力。所以今天我们就来讲解和实现这种基于模拟的方法来改进动画连贯性。

查看 game_world_mode.cpp 中当前的跳跃代码,考虑为弹跳引入物理模拟(弹簧行为)

我们目前正在处理主角的跳跃动作逻辑,在跳跃移动模式下,跳跃被划分为三个阶段:起跳阶段(角色下压再起跳)、飞行阶段(在空中运动的一段时间,动作会与起跳阶段部分重叠)、落地阶段(角色着地并出现二次压缩)。当前的问题在于这些动作是以一种固定的"手动方式"编码进去的,比如 T_Bob 是通过正弦波函数模拟角色的上下弹动,但这种方式无法应对连续跳跃场景中的自然过渡。

为了解决这个问题,我们决定将这些上下弹动的动作完全从跳跃模式中解耦出来,让它们始终处于一种物理模拟状态。也就是说,主角身体(特别是躯干)与披风之间的连接关系将不再由预设动画或函数驱动,而是改用弹簧模拟系统。这样,当角色起跳时弹簧就会自然向上拉动角色,而当角色落地时弹簧则会向下压缩并随之回弹,从而实现更自然的运动效果,同时也具有阻尼行为以避免过度摆动。

为了实现这一点,我们将原本受跳跃模式控制的 T_Bob 计算部分移除,并提取出来放入一个始终进行的物理模拟系统中。这意味着 T_Bob 不再受具体跳跃阶段的影响,而是始终根据物理规则进行更新。虽然以后依然可以根据需要在某些特殊模式下覆盖其行为,但默认行为将是基于模拟的。

接下来我们会为 T_Bob 设置一个目标值(target value),也就是当前时刻它应该朝向的位置。为了能够根据不同的动作状态(比如起跳、落地、空中)动态调整 T_Bob 的行为,我们需要引入更多的信息来描述这种弹簧系统的状态,比如当前的速度、加速度、目标位置等,并建立一套机制用来持续更新这些值,以便系统始终知道 T_Bob 应该如何运动。

这是我们接下来实现自然连续动作系统的核心步骤,也为后续身体各个部分(如头部、披风等)进行弹簧模拟提供了框架基础。

在黑板上跟踪披风的速度与目标位置

我们现在的目标是改进主角身体部分(特别是底部和披风之间连接处)的动画模拟方式。我们把这两个部分之间的连接看作一个可以上下移动的结构,为了让动画表现得更自然,我们决定将这种连接关系物理化,即通过模拟物理行为实现自然的动作过渡。

我们需要做的第一件事是追踪这段连接的速度(或说动量)。虽然在真实物理中动量包含质量因素,但在我们的动画系统里并不涉及实际质量计算,因此这里我们仅仅把动量视为"速度"。这项速度追踪的重要性体现在如下场景:比如角色处于空中运动阶段即将落地时,底部着地后,身体上部(如披风)仍应保有此前下落时的速度并继续运动一段时间,这样才能体现出物理的惯性感和弹性效果。

第二件要做的事是设置一个"目标位置"或"目标速度"。因为角色的部分动作(例如下蹲蓄力后起跳)是由动画逻辑强制驱动的,它并不是物理反应的结果。在这种情况下,我们需要提供一个明确的指令来让身体下压或上弹,这些动作不能依赖惯性或速度保留,因为它们根本不是惯性导致的,而是角色动作行为的一部分。

所以我们将原本通过正弦函数来设定 T_Bob(上下弹动变量)值的方式,替换为"建议性的速度"或"目标位置"的形式。也就是说,系统不再直接指定一个位置,而是告诉系统:"我希望你朝这个方向以某种速度移动。"这样一来,我们既可以保留原有的动画效果,又能将它与物理模拟融合,使得角色动作之间的过渡更加自然。

这种做法其实是动画系统中一个非常常见的技术,即将原本强制设定的位置,改为可由物理系统演化出来的位置,通过引入目标速度或目标加速度来实现更加真实的动态表现。这种方式能让不同动画阶段自然衔接,同时还保留对动作细节的控制能力,是非常实用的策略。

总结来说,我们的目标是:

  1. 为角色身体与披风之间的连接添加速度状态,保留惯性运动;
  2. 引入目标位置或速度作为动画指令;
  3. 将这些信息结合进一个简单的物理模拟(如弹簧阻尼)系统;
  4. 使动画表现更加连贯、自然,同时保留可控性。

接下来我们将正式实现这个结构。

game_world_mode.cpp 中使用标准运动方程模拟 tBob

我们现在开始用一种非常基础且直观的方式来模拟角色身体上下弹动(T_Bob)的行为。这个弹动效果原本是通过一个简单的正弦函数手动设置的,现在我们将其转化为一个更具物理感的过程,具体来说就是用一个"弹簧系统"来进行模拟。

首先,我们定义两个变量:

  • T_Bob:表示弹动的当前位置(就像弹簧的位置)
  • dT_Bob:表示弹动的速度(即 T_Bob 的一阶导数)

接下来,我们不再在动画代码中直接赋值为零,而是把它纳入角色整体运动流程中的一个"子系统",在角色更新逻辑执行完毕后,进行 T_Bob 的模拟更新。

我们采用的是经典的运动方程来实现:

  1. 基于当前加速度,对速度进行积分(即速度变化);
  2. 基于当前速度,对位置进行积分(即位置变化);
  3. 所有的更新都乘以时间步长 dtdt²,以获得正确的物理尺度。

这种方法虽然模拟的是一个并非真实空间中的位置(T_Bob 是人为设定的一种"弹性位移"概念),但我们希望它的行为看起来像是物理驱动的,因此我们使用真实的物理运动方程来驱动它。

加速度可以理解为"我们想让它加速到哪里去",我们可能会设置某个目标位置,然后让它像弹簧一样向那个目标位置弹去。同时,速度的保留(惯性)允许动画在角色落地后仍保留上身(如披风)的一些上下运动,制造出真实感。

目前我们并不确定是继续使用加速度来驱动,还是直接设置目标速度(即以速度为控制变量)。但我们从最基础的方式入手------通过加速度来计算速度,再计算位置。

为了确保数值合理性,我们把运动系统整合到角色更新周期的最后阶段,也就是每帧角色完成自己的状态逻辑之后,T_Bob 也会根据这个系统进行一次物理模拟。

总结我们做了以下几个步骤:

  1. 定义 T_Bob 位置值和速度值(dT_Bob);
  2. 在每一帧末尾,用标准物理公式更新它们;
  3. 引入时间步长 dt,确保更新结果随着帧率自适应;
  4. 保留未来扩展空间(如可能用目标速度代替加速度);
  5. 逐步从原来硬编码的正弦函数弹动,转变为更加物理自然的弹簧动画系统。

这样可以让角色的身体弹动表现出更加真实的物理惯性和缓冲效果,同时仍然保有可控性和可扩展性。接下来我们将继续优化这个系统的响应特性与控制方式。

设置加速度为 0,观察关节没有任何移动

当我们将加速度设置为零时,预期的行为是该连接点(代表角色身体某部分,例如上半身与下半身之间的弹动部分)不再发生任何移动。这也确实是我们所观察到的现象。

具体来说:

  • 我们当前实现的模拟机制是基于经典的物理运动方程,即:通过加速度计算速度,通过速度计算位置变化;
  • 如果我们把加速度设为零,意味着系统中不再有任何"外力"驱动这个连接部位;
  • 此时,根据运动方程,速度不会再发生变化,而如果速度本身也为零,则位置也不会变化;
  • 也就是说,整个弹动系统完全静止,不会有任何上下移动或振荡的表现;
  • 在实际运行中,我们将时间步调慢下来(通过调低 time scale),以便更清晰地观察模拟过程;
  • 观察结果与预期一致:连接点(也可以理解为角色身体的弹性部分)完全静止,没有发生任何动作;
  • 这进一步验证了我们的模拟机制是准确的,能够如实地根据当前的加速度输入做出响应。

这个测试起到了非常重要的验证作用,确保弹动系统在无加速条件下能够保持静止状态,这也是物理系统中一个最基本的行为预期。接下来,我们可以在此基础上逐步引入合适的加速度值或者目标位置/速度,从而创建出我们想要的弹性运动效果。

设置循环并调整跳跃相关的变量

我们现在开始引入一个非常直接的机制:通过构建一个弹簧系统来模拟 t_bob(一个代表角色身体弹动的值)。为此,我们增加了 dt_bob 变量,表示 t_bob 的一阶导数,即"速度"。t_bob 表示位置,dt_bob 表示速度,这构成了最基本的物理运动建模框架。

我们将这一机制整合到了循环代码编辑系统中,这意味着我们可以在观察角色动画实时运行的同时,直接修改代码,立刻看到变更结果。这种做法极大地提升了调试效率。

具体步骤如下:

  1. 建立实时动画观察系统:我们记录角色动作循环,确保能在修改代码时同步观察动画效果。窗口大小被适当压缩,使得动画与代码能同时展现。

  2. 模拟基础物理行为

    • 首先测试将加速度设为零,这样系统就不应产生任何弹动或移动;
    • 验证后果确实如此,说明模拟系统基准逻辑正确;
    • 接着尝试手动输入一个非零加速度值,例如 1.025.0,观察角色弹动部件向上不断加速移动,验证了加速度对位置变化的物理性作用。
  3. 引入二阶导数变量 ddt_bob

    • 它代表"加速度",我们为它设定名称使其更易读;

    • 将该变量的位置初始化放到合适区域,使其作用清晰;

    • 在逻辑流程中,针对不同时期(例如跳跃开始、空中阶段、落地)设定不同的 ddt_bob 值;

      • 起跳阶段设置为正值,向上推;
      • 空中阶段不做加速度;
      • 落地阶段也无加速度,但可能直接修改 dt_bob,即给一个负速度,以制造"压缩"感觉。
  4. 关于数据更新的问题

    • 发现有些变量在每帧会被静态重置,因此任何累积效果都被抹除;
    • 虽然暂时不打算重构这部分逻辑,但注意到这个潜在问题是有价值的,后续可能需要将这些变量转移到持久位置,以便保留物理状态。
  5. 物理方程的应用

    • 使用标准运动方程:

      • 位置 += 速度 × dt;
      • 速度 += 加速度 × dt;
    • 即使 t_bob 不是真实空间位置,我们仍希望其具备"物理感",因此沿用真实世界的动力学方程建模;

  6. 动态调整与观察效果

    • 设置短循环以避免动画过长造成干扰;
    • 调试加速度、速度值,逐步构建出符合预期的弹动效果;
    • 过程中还会精细调整跳跃时间点、状态判断条件等,以确保在不同时机作用的加速度或速度合理自然。

总之,我们建立了一个具备基础物理特性(位置、速度、加速度)驱动的弹动模拟系统。借助实时循环编辑机制,我们可以高效试验不同参数,调整运动逻辑,并最终实现角色弹性身体部位的自然动态响应。这种方法同时保证了控制的灵活性与视觉表现的真实感。

将位置重置为 0

我们接下来需要对当前的物理模拟值进行更深入的调整,目标是让弹动系统(t_bob)在多次跳跃过程中具备"回归到原位"的机制,也就是实现一个弹簧般的复原力,始终将 t_bob 拉回到零点(默认位置)附近。

核心思路如下:

  1. 添加复原加速度(Restoring Force)

    • 当前我们已经在使用加速度(ddt_bob)推动系统;
    • 现在我们引入一个新的逻辑:根据当前位置 t_bob 与零点的偏移,施加一个反向加速度;
    • 这个加速度的方向始终指向零点(即当前值越远,恢复力越大,方向指向原点);
    • 类似于弹簧力(胡克定律),也就是 ddt_bob += -t_bob * K,其中 K 是恢复系数(弹性系数)。
  2. 实现逻辑结构

    • 在每一帧或者物理更新中,我们不再直接用一个静态初始值赋值 ddt_bob

    • 而是在原有基础上累加一个"恢复加速度":

      c 复制代码
      ddt_bob += -t_bob * stiffness; // stiffness 是我们设定的常数
    • 这个恢复加速度和当前偏离量成正比,使得系统具有自我调节的回弹性。

  3. 调试和观察

    • 初始设置的 stiffness 值可能太小,导致恢复力太弱;
    • 观察动画发现回弹不明显,于是逐步加大这个系数;
    • 当恢复系数达到一定强度时,可以看到系统尝试将偏移拉回中心位置;
    • 同时也发现如果跳跃落地时给的负速度过大,恢复力不足以拉回,需要进行参数平衡;
    • 所以一方面需要适当调小落地时设定的负速度,一方面可以调大恢复加速度。
  4. 跳跃运动渐趋自然

    • 在不断微调参数后,现在系统已经开始表现出一种合理的"跳跃感觉";
    • 起跳时 t_bob 向上偏移,落地时产生回落,再配合恢复力向中间位置收敛;
    • 整体呈现出一种弹簧振动后趋于稳定的状态;
    • 初步验证了整个弹动模拟逻辑是有效的,已经具备较好的跳跃物理基础。

总结而言,我们为 t_bob 系统添加了一种基于当前偏移量的复原加速度,使其在每次被推动后能够自然回到默认位置。这个机制类似于弹簧的拉力,确保跳跃过程中弹动曲线具有自然的物理回弹感,并通过参数微调来实现期望的运动表现,逐步建立出一个合理稳定的弹性运动系统。

把速度调小

初始加速度为0

控制主角移动,观察跳跃出现振荡

现在我们可以更清楚地看到系统正在发生的行为了。画面中实体的动作呈现出一种来回振荡的状态,这种不断反复地左右摆动就是我们目前需要解决或进一步理解的现象。

这是一个很好的机会,可以深入探讨这种振荡行为的本质。在系统中,实体的某个部分(例如头部或躯干)受到了某种力的作用,比如重力、恢复力或者由位置偏移引发的加速度,这些因素共同导致了当前这种"摆动"效果。

目前的行为表现为:实体并没有稳定地停在目标位置,而是来回穿越目标点,超出后又被反向加速拉回,再次穿越,然后再被拉回,持续往复。这样的现象说明系统内部存在一个没有充分阻尼(damping)的振荡系统。

这是一个典型的物理模拟问题,类似于弹簧-质量系统:

  • 如果只施加恢复力(例如根据与目标位置的偏差产生的加速度),但没有施加速度方向上的阻尼力(例如根据当前速度反向作用的摩擦力或空气阻力),就会造成系统持续地来回震荡,永远不会收敛到稳定状态。
  • 这就意味着我们需要引入阻尼机制,来吸收这种多余的动能,使得实体逐渐回归静止状态。

阻尼可以通过在系统中加入一个与当前速度方向相反、大小与速度成比例的加速度来实现。例如:

cpp 复制代码
ddtBob += -kDamping * dtBob;

其中 kDamping 是一个阻尼系数,dtBob 是当前速度,这项加速度会持续削弱速度的大小,直到其趋近于零。

总之,这种"来回震荡"的现象提示我们当前系统缺乏有效的能量衰减机制,而这是实现稳定自然运动状态的关键。我们正好借此机会来引入和调整阻尼,使系统具备更真实、更可控的物理表现。

黑板讲解:弹簧系统

我们正在尝试实现的效果,本质上就是一个"弹簧"的行为,我们希望系统具有一种弹性反馈的感觉,带有自然的"回弹"和"摆动"效果。

具体来说,我们引入了一个变量 tBob,它表示从"零点"偏移的位置,也就是当前偏离静止状态的程度。如果 tBob 为正,代表系统处于上方偏移;如果为负,代表在下方偏移。我们的目标是:无论偏移朝哪个方向,系统都应该产生一个"加速度"来把它拉回到中心(也就是零点)。

因此,我们的加速度设置为:

复制代码
加速度 = -K * tBob

其中 K 是一个待调整的系数,用于控制回弹的强度。这个加速度的方向始终与偏移的方向相反,也就是说系统总是在"试图恢复"到初始状态。这种机制就是一个典型的"无阻尼弹簧系统(undamped spring)"。

在传统物理学中,弹簧作用的不是加速度,而是"力"。力等于质量乘以加速度(F = ma)。如果我们模拟的是更精确的物理系统,那么我们需要明确地区分"加速度"和"力"的概念。比如两个物体质量不同,它们在同样的力作用下加速度会不同。

但是在我们当前的模拟中,我们并不关心质量的问题。我们直接将弹簧的力作为加速度使用,即默认质量为1,因此:

复制代码
力 = 加速度

这种简化让我们能够更方便地处理模拟逻辑,而不必引入复杂的质量或动量计算。

总之,通过给偏离中心的位置施加一个与偏移方向相反的加速度,我们构建了一个简化版的弹簧反馈系统。这使得对象具有回弹性和弹性摆动的自然感觉,从而营造出富有动态感的表现形式。这个机制就是整个"弹簧效应"的基础构建块。

黑板讲解:无阻尼弹簧

我们现在的目标是计算一个弹簧的"力",这个力是要施加在物体上的,用来模拟一个弹性恢复的效果。这个过程一般包括两个部分:位置相关的恢复力速度相关的阻尼力

首先是 位置部分的恢复力。弹簧总是希望回到目标位置(理想位置),所以我们要计算当前位置相对于目标位置的偏移,并根据这个偏移值施加一个相反方向的力。计算方式是这样的:

复制代码
F_position = Cp * (P_target - P_current)

其中:

  • P_target 是目标位置(我们希望回去的那个点),在这里就是 0;
  • P_current 是当前位置,这里是 tBob
  • Cp 是一个可调节的系数,决定了弹簧的"刚度"或者说恢复力的强度。

所以最终这部分可以简化为:

复制代码
F_position = -Cp * tBob

接着我们来处理第二部分,即 速度相关的阻尼力。如果只施加位置的恢复力,那么系统就会表现为一个"无阻尼弹簧",这会导致持续来回震荡(类似钟摆),因为当系统返回目标位置时速度不为零,会继续越过目标点。为了避免这种震荡,我们需要添加一个阻尼项,让系统在接近目标位置时速度减小,最终静止下来。

阻尼项的计算方法如下:

复制代码
F_velocity = Cv * (V_target - V_current)

其中:

  • V_target 是我们期望的速度,这里是 0,因为我们希望在目标点时静止;
  • V_current 是当前速度,这里是 dtBob
  • Cv 是一个阻尼系数,可调节。

因此这部分力简化为:

复制代码
F_velocity = -Cv * dtBob

把两个力加在一起就是总的弹簧力:

复制代码
F_total = -Cp * tBob - Cv * dtBob

这个力会作为加速度被施加到系统中,从而控制位置和速度的恢复,使对象既能有自然弹性的回弹,也能在适当时候稳定下来,防止永远震荡。这就是一个典型的"有阻尼弹簧系统"的实现方式。

通过调整 CpCv 的数值,我们可以控制系统的弹性强度和阻尼快慢,从而达到更自然、符合物理直觉的运动效果。

game_world_mode.cpp 中添加无阻尼弹簧方程

我们观察到系统正在来回振荡,因此需要引入一个阻尼项来抑制这种振荡。这就涉及到我们前面讲到的弹簧方程,它分为两个主要部分:位置修正项速度阻尼项

首先是位置修正项,我们通过一个系数 Cp 来控制当前的位置 tBob 与目标位置 BobTarget(通常是0)之间的差值对系统的影响:

复制代码
SpringForce = Cp * (BobTarget - tBob)

这个公式的作用是让系统不断地朝着目标位置靠近,当 tBob 偏离目标时,它就产生一个恢复力将其推回去。我们可以将这个公式直接应用在代码中,并加上注释明确 Cp 是控制弹簧位置部分恢复力的系数。

接下来是关键的阻尼项,我们想让振荡逐渐减弱、系统最终稳定在目标点,因此引入速度阻尼项。我们希望当前速度(dtBob)逐渐变为0,即:

复制代码
DampingForce = Cv * (0 - dtBob) = -Cv * dtBob

这里的 Cv 是速度阻尼系数,用于控制系统减速的程度。将这个项加入到总加速度中后,再次编译运行,可以看到系统现在有了理想的阻尼效果------振荡被逐渐抑制,物体逐渐停在目标位置。

这个过程直观上很好理解:系统在被恢复力拉向目标位置的同时,也受到阻尼力的抑制,防止它过度冲出目标点。这个弹簧公式非常简洁,但作用非常有效,是一种实现自然"回弹-停稳"运动的经典方法。只需调节 CpCv,就可以控制回弹的速度和稳定程度,形成我们想要的运动节奏和物理感觉。

cpp 复制代码
// 定义弹簧位置恢复力的系数(弹簧刚度),值越大,弹簧越"硬",恢复越快
real32 Cp = 100.0f;

// 定义速度阻尼系数,值越大,阻尼越强,回弹振荡越快被抑制
real32 Cv = 10.0f;

// 计算总加速度(恢复力 + 阻尼力)
// 恢复力部分:Cp * (目标位置 - 当前tBob),目标为0,因此是 -tBob
// 阻尼力部分:Cv * (目标速度 - 当前dtBob),目标速度为0,因此是 -dtBob
// 最终:ddtBob 是要叠加上的总"力",模拟弹簧回弹 + 阻尼的合力
ddtBob += Cp * (0.0f - Entity->tBob) + Cv * (0.0f - Entity->dtBob);

// 用"加速度公式"更新 tBob 的位置值
// tBob 表示当前的弹簧位移(偏离零点),加入 dt 和 dt² 控制帧率影响
Entity->tBob += ddtBob * dt * dt + Entity->dtBob * dt;

// 根据当前加速度更新速度 dtBob
// 相当于 dtBob 是当前帧的速度,持续积累加速度的影响
Entity->dtBob += ddtBob * dt;

模拟落地阶段的行为

我们现在需要开始考虑落地阶段的行为,特别是在实体处于地面(Planted)状态时的处理。当头部开始向身体侧面移动时,我们希望能启动一个跳跃动作,并在跳跃前模拟一个"下蹲"动作------即向下压缩,为弹跳做准备。

我们已经有了 BodyDistance,表示身体和头部之间的距离。我们可以利用这个信息,在实体准备起跳之前,基于这个距离来施加一个向下的加速度,模拟下蹲。

为了实现这个,我们执行了以下几个步骤:


1. 计算头部距离(Head Distance)

我们在所有模式下都增加了一个计算:从实体身体的位置到头部位置的向量长度,即"头部距离"(Head Distance)。这是一个动态值,表示当前头部相对于身体的偏移程度。

cpp 复制代码
v3 HeadDelta = Head->P - Entity->P;
real32 HeadDistance = Length(HeadDelta);

2. 设置最大头部偏移值(Max Head Distance)

我们设定了一个最大头部距离的参考值,用于做归一化映射(ClampMapToRange),例如:

cpp 复制代码
real32 MaxHeadDistance = 0.5f; // 具体数值待调整

3. 映射成标准范围值(0~1)

我们将头部实际距离按最大距离映射成 [0.0, 1.0] 区间:

cpp 复制代码
real32 CrouchAmount = ClampMapToRange(0.0f, HeadDistance, MaxHeadDistance);

这个值代表我们应该施加多大的下压"蹲伏"力,值越大,代表头部越远,下压越明显。


4. 在 Planted 模式中施加蹲伏加速度

当实体处于 MovementMode_Planted 状态时,也就是站在地面还没跳跃的阶段,我们根据 CrouchAmount 施加一个向下的额外加速度到 ddtBob,例如:

cpp 复制代码
ddtBob += -50.0f * CrouchAmount;

这样当头部向外移动,身体就会像蹲伏一样被压低,为后续的起跳提供一个自然的过渡。


5. 调试和修正

在初期实现时发现这个蹲伏力没有正常作用。经检查后,发现是因为在 MovementMode_Planted 中没有正确应用 ddtBob 的值。问题出在编译时缺少操作符(可能是加法符被遗漏),修正之后系统恢复正常。

修正后发现效果太强,我们就把这个值调小,比如从 -50.0f 改为 -20.0f,使得下蹲动作看起来更自然不过度。


6. 优化跳跃的节奏和时间点

原本的起跳阶段、落地阶段使用了 tJumptMidtLand 三个阶段。现在为了简化逻辑和更精确控制动作,我们移除了一些中间的过渡阶段,把 tLand 的处理合并到整体的运动结束点上,只在真正落地的那一刻设置最终状态。


7. 清理冗余代码

清理了一些不再需要的跳跃过渡判断和位置更新逻辑,使得跳跃逻辑更加简洁,后续更容易维护和调整。


最终效果

现在实体在地面状态下,当头部向侧面移动时,会自然地产生一个下蹲准备起跳的动作,这个动作通过头部距离动态控制蹲伏深度,带来了更自然、更物理化的视觉反馈。跳跃起跳和落地的动作也变得更加连贯,整体手感更接近理想的弹跳行为。

调整跳跃参数,使跳跃感觉更自然

我们现在开始对系统中的弹跳行为进行进一步调整和调试,希望使整体动作表现得更加自然和有力度感。

我们希望实现一个"t跳跃"动作,也可以理解为"t推力",这个力应该持续一段时间,而不是瞬间完成。我们尝试增加一个"起跳缓冲",让角色在起跳完成之前保留一部分向上的推力,这样角色会被继续推向空中,形成一种更流畅的弹跳感。

经过初步测试发现,这个调整会导致角色跳得有点太高,因此我们需要对参数进行微调,找到一个平衡点。

我们查看当前的 tJump 参数设定,并发现当前 dtBob 设置为 -20(代表一个初始向上的速度),而 ddtBob 此时是 0,也就是说没有额外的加速度被叠加上去。但看起来跳跃动画中仍存在一种"不连贯"的突变,我们暂时无法确定具体是哪部分引起的。

为了更好地观察和分析问题,我们关闭了自动循环执行的代码流程(loop loop-like coding),转而采用手动控制角色运动的方法,这样我们可以更直观地感受到动画和物理效果之间的配合问题。

接下来的工作将集中在进一步调试参数,比如 tJumpdtBobddtBob 等变量,以及这些变量如何协同作用产生符合预期的弹跳表现。目标是确保角色的起跳、上升、下落过程连贯自然,不突兀。

黑板讲解:该弹簧方程的工作原理

我们现在深入理解弹簧系统中两个关键组成部分------位置修正项和速度阻尼项------在运动过程中的动态关系。

首先,位置修正项(Position Term)是根据当前的位置与目标位置之间的差距计算出来的。当我们距离目标位置很远时,这个位置差值很大,因此该项产生的加速度非常高,系统会强烈地推动物体朝目标靠近。这个加速度是线性的,随着位置差距的减小而线性下降。当我们接近目标位置时,这个差值逐渐变小,加速度随之减小,最终在位置完全对齐的那一刻为零。用图像来表示,时间为横轴,加速度为纵轴,这条曲线从高点线性地滑落到零。

而速度阻尼项(Velocity Term)起作用的方式正好相反。在初始时刻,速度为零,这个项不施加任何影响。但随着我们因位置修正而开始加速前进,速度逐渐增大。速度越大,速度阻尼项所施加的反作用力就越大。这个反向的加速度会抑制继续加速的趋势,从而逐步减缓速度,直到最终在目标点停止。

从整体来看,系统的行为类似于这样一种过程:

  • 开始时,由于距离目标较远,位置项起主导作用,大幅推动物体向目标运动。
  • 随着物体逐渐接近目标,位置项贡献逐渐减弱。
  • 与此同时,由于速度逐渐变大,速度项的抑制作用逐渐增强。
  • 当速度达到峰值时,速度项成为主导,开始抵消之前的加速度。
  • 结果是物体加速、减速、最终平稳地停在目标位置。

这种机制正是弹簧阻尼系统自然减震的关键:通过这两个相反方向但彼此协调的力项,系统能够实现稳定收敛,既快速又不发生剧烈震荡地趋向目标位置。我们可以通过调整位置项和速度项的系数(Cp 和 Cv)来精确控制系统响应的速度和稳定性。

黑板讲解:欠阻尼、过阻尼和临界阻尼弹簧的区别

我们在构建弹簧系统时,需要理解三种主要的阻尼状态:欠阻尼(Underdamped)、过阻尼(Overdamped)和临界阻尼(Critically Damped)。它们的区别体现在系统到达目标点时的动态表现上,决定因素是系统中位置修正项(Cp)和速度阻尼项(Cv)之间的相对关系。


欠阻尼(Underdamped)

这是一种阻尼不足的弹簧系统,是我们当前所采用的状态。这种类型的弹簧具有一定的"弹性"或"晃动",会在目标点附近来回振荡几次后才逐渐停下。这种振荡带来一种动感,能增加动画的表现力和活力,适合用于视觉反馈丰富的动画场景。我们之所以选择它,是因为我们希望保留一点"回弹"的感觉,让动画显得更自然、更富表现力。


过阻尼(Overdamped)

过阻尼的弹簧系统具有过强的阻尼力,会大大抑制速度。虽然它能保证最终稳定地停在目标位置,而且不会出现震荡,但到达目标点的过程非常缓慢。就像一个物体在强烈的逆风中前进一样,虽然目标明确,但每前进一步都受到强力的反作用。这种类型的弹簧适合用于需要绝对平稳、无振荡过渡的场景,比如一些精密控制或慢动作过渡动画。


临界阻尼(Critically Damped)

临界阻尼是一种理想状态的弹簧配置,它的阻尼刚好足够,不多也不少,使得系统能够以最快速度到达目标位置并在到达瞬间刚好停下,不产生任何振荡。这种状态可以视为完美的物理平衡点,它是效率和稳定性的极致结合。与欠阻尼和过阻尼不同,临界阻尼的参数并不能仅凭感觉或调试得出,需要通过数学方式精确求解 Cp 和 Cv 的匹配关系,才能真正达到"临界"状态。


参数调节的影响

这三种状态完全是由位置项和速度项之间的关系决定的:

  • 如果我们降低速度项(Cv)相对位置项(Cp)的影响,系统就会变得更加欠阻尼,振荡更多。
  • 如果我们提高速度项(Cv)的权重,就会变得更加过阻尼,收敛速度变慢但稳定。
  • 要达到临界阻尼,则必须求解一个特定的数学公式,不能单纯通过不断调整参数来得到,它有一组精确的理想系数。

总的来说,这三种弹簧模型提供了丰富的动态调节方式,可以根据不同的使用场景和表现需求自由切换和调节,以实现最合适的动画行为或者物理响应。

game_config.hgame_world_mode.cpp 中设置为 100% 全局速度并放慢头部和跳跃动作

我们目前的进展已经比较理想了,接下来继续对一些细节进行微调,以使整体表现更贴合预期。

首先,将时间步长的参数恢复到正常状态,这是为了观察当前系统的整体运行状态,评估当前动画动态是否达到目标表现。观察下来,发现角色的头部移动速度略快,特别是在游戏开始时这种速度显得不太自然。当前的头部运动节奏更像是在一种特殊情境下,比如在空中或某种特殊状态下的快速反馈,而不是普通状态下应有的缓慢摆动。

因此决定调整头部的动态响应,让它的运动变得更加迟缓、柔和,从而看起来更自然。通过调节响应参数,让头部动作的惯性更明显,减少过快的反馈,使其看起来不那么灵敏、更加贴合动画节奏。

随后,同样地也对角色的跳跃速度进行了放慢处理。此前跳跃动作使用了一个时间步长的倍数作为跳跃节奏的控制参数,设置为大约 0.2 秒(1/5 秒) 来完成一次跳跃动作。现在将这个数值调整为大约 0.25 秒(1/4 秒),也就是让跳跃动作在时间上延长了一点,使整个跳跃过程看起来更从容、自然,减少过快起跳带来的违和感。

总结来说:

  • 头部运动调整:通过降低速度响应系数,使其运动更加缓慢、具有延迟感;
  • 跳跃速度调整:增加跳跃动作所需的时间,使其更加柔和自然;
  • 整体目的 :优化角色动画表现,避免早期出现过快或突兀的动作,让动作表现更具重量感与自然过渡性。

调整下落动作(预备阶段)中的运动效果

我们发现角色的头部在运动过程中缺少预期中的下沉感,也就是说,在角色起跳或者运动时,头部并没有如我们希望的那样产生明显的向下拉动。这导致动画的表现缺少重力反馈感,显得不够真实。

经过检查,我们意识到这个问题出现在弹簧系统的设置上,弹簧并没有提供足够的向下反馈力。这可能是因为弹簧力的强度不够,也可能是相关的系数设置不合理,导致我们在偏移过程中无法获得理想的反向回拉。

为了解决这个问题,我们回到之前的弹簧模型中进行排查。确认后发现确实是弹簧力不够强的问题,于是我们尝试加大弹簧系数,增强其将物体拉回目标点的能力。

另外,我们注意到控制头部下沉程度的变量 tHeadDistance 没有产生预期效果。理论上这个值应当可以调节到接近零,使头部动作更贴合目标位置。于是我们临时将某些值夸张放大,以便观察其最大幅度下的响应,借此更准确地估算合理的参数范围。

在这个过程中,我们尝试逐步调小这个夸张的值,以达到比较自然的表现效果。观察后发现当前的头部回拉动作基本贴合我们的目标,所以我们将系数逐步调回更合适的值,避免因过大而引发动作过度、抖动或不连贯。

在调整中还注意到一点小问题,不过初步看来可能是预期内的效果,因此暂时不做深入处理。

整体来看,这些细节上的工作开始体现成效,角色跳跃的动作渐渐呈现出我们想要的动感,头部动作也有了真实的惯性反应。虽然还未完全完成,例如头部的绑定可能还存在些许问题需要后续处理,但目前整体动作已经有了明显改善。

总结如下:

  • 问题识别:角色运动中缺少头部的向下反馈,动作显得轻浮;

  • 原因定位:弹簧系统反馈力不足,tHeadDistance 无法有效控制回拉;

  • 解决方法:

    • 增加弹簧系数以增强向目标位置的拉力;
    • 夸张设置观察效果,再逐步调小系数以寻找最自然值;
  • 当前效果:

    • 动作更贴合实际物理反应;
    • 起跳与落地过程中头部表现更有重量感;
  • 后续待处理:头部与身体连接关系的视觉与动力一致性问题。

考虑在静止时让头部"弹回"身体上

现在我们希望在角色不再控制头部的时候,让头部自动"吸附"回身体的标准位置,也就是说,当玩家拖动头部时它可以自由运动,但一旦放手,它就会自动弹回默认的中立状态。这种表现能让角色动作看起来更自然,同时带有一定的物理感知。

为实现这个效果,我们计划将之前用于弹簧的力反馈机制同样应用到头部,使其在被释放后产生弹性回拉的动画。这个逻辑与之前给身体的 tBob 添加弹簧效果的方法类似,因此思路上可以直接借鉴。

目前我们注意到,现有的结构中,身体可以定位到头部,但头部并没有主动回馈或靠近身体的逻辑,这显然会造成不连贯。我们决定修改这个关系,让头部也能感知身体的位置,并受到来自身体方向的"回拉力"。

在这一过程中,我们还在思考是否应将头部和身体建模为完全独立的两个实体,或者将它们统一为一个整体。我们暂时保留它们为分离的结构,因为游戏设计中可能会有让头部与身体分离(例如法术或特效)等特殊需求,这种架构有助于我们在未来灵活拓展功能,而不是被静态绑定所限制。

所以,接下来的做法是:

  • 继续维持头部与身体两个独立的实体;
  • 在物理模拟流程中加入"回拉力",通过弹簧模型将头部吸附回身体位置;
  • 考虑在物理模拟阶段(physics pass)中,允许身体和头部之间互相施加力;
  • 使用之前身体弹簧那套逻辑,即根据两者位置差(偏移量)计算回拉力,再施加给头部;
  • 调整头部的 distance 或偏移变量,作为计算弹簧回拉的基础;
  • 最终目标是:在玩家释放头部控制后,头部能自然地带有惯性地弹回中立位置,表现出柔和且真实的物理反馈。

通过这种方式,我们可以在保持功能扩展性的同时,赋予角色头部更生动、物理感更强的表现,从而提升整体动画和游戏手感的质量。

黑板讲解:如何在不同方向上应用弹簧技术

我们之前在处理弹簧力(spring force)时相对轻松,是因为处理的是标量(scalar)形式的变量,比如 tBobdtBob。它们分别代表目标位置与速度的偏移值,以及它们的变化率,也就是速度,所以很容易套入一个基本的弹簧公式进行物理反馈。

现在我们希望将这一方法应用到更复杂的头部与身体之间的弹簧运动上,但这里的变量是向量(vector)而非单一标量,因此我们需要将之前的标量弹簧力计算方法扩展成向量版本。

基本的弹簧力计算可以表示为:

  • 位置校正力(F_position) = 位置弹性系数(Cp) ×(目标位置 - 当前实际位置)
  • 速度校正力(F_velocity) = 速度阻尼系数(Cv) ×(目标速度 - 当前速度)

这两个量原本在标量的世界里都很直观,但在向量世界中我们要考虑的是向量之间的距离和方向。

我们通过以下方式来拓展到向量形式:

  1. 向量差值的方向性:

    向量差 delta_p = p_target - p_current 表示当前与目标之间的差异,是一个向量,指向我们希望对象前进的方向。

  2. 弹簧力方向:

    弹簧的校正力总是沿着 delta_p 的方向反向作用。为了获得一个单位方向向量,我们将 delta_p 除以其模长(length(delta_p)),这可以获得方向信息。

  3. 优化简化:

    实际上,在乘法展开中我们会发现,单位化再乘回来是冗余操作。即:
    (delta_p / |delta_p|) * Cp * delta_p 最终等价于 Cp * delta_p,因为向量本身就同时包含了方向与幅度。因此完全可以跳过单位化步骤,直接使用 Cp * delta_p

  4. 速度弹簧力也是类似处理:

    同样我们有 delta_v = v_target - v_current,然后直接 Cv * delta_v 即可,不必做任何分量拆解或单位化。

因此,最终的弹簧力向量计算公式为:

复制代码
F_spring = Cp * (p_target - p_current) + Cv * (v_target - v_current)

这两个向量相加后直接作为物理引擎中的力输入,用于驱动物体朝目标位置和速度移动,实现弹簧吸附的视觉和物理效果。

这个方法既简洁又高效,同时避免了不必要的分量处理或归一化运算,可以直接用于角色头部与身体之间的动态关联上,让头部在放手后自然弹回身体对齐的中立状态。

总结起来,核心结论是:在向量空间中使用弹簧力时,完全可以直接用差值向量乘以弹性/阻尼系数来得到所需的修正力,而无需显式计算方向和长度,公式简化且效果一致。

game_world_mode.cpp 中为头部实现该弹簧方程

我们现在想要实现一个作用在角色头部的弹簧力,让头部在偏离身体时能够自动被拉回到身体所在的位置。具体来说,就是模拟一个头部恢复力,使其在不受控制输入时自然回弹。

整个过程可以分为以下几个步骤和关键思路:


一、头部弹簧力的核心逻辑

我们当前已经有了头部的位置差值 dp,这个是头部与身体之间的位置差向量。

然后我们还需要一个加速度项,通过乘以时间步长 dt,将其加入到当前的 dp 中,作为一个近似的速度更新。也就是说,我们设置:

复制代码
dp += dt * acceleration

这里的 acceleration 就是弹簧产生的加速度。虽然这种做法理论上不够精确(最好应该重构整个能量系统),但为了快速实现弹簧回弹效果,目前先用这种方式暂时实现。


二、弹簧力的构建方式

头部弹簧力分为两部分:

  1. 位置校正力

    将头部朝向身体位置拉回,公式为:

    复制代码
    spring_force_position = stiffness_coefficient * (body_position - head_position)

    这里的 stiffness_coefficient 是弹簧强度因子,用来控制弹力的强弱。

  2. 速度阻尼力

    为了避免头部持续震荡,加入一个基于当前速度的阻尼力:

    复制代码
    spring_force_velocity = damping_coefficient * (-head_velocity)

    合力就是两者之和,作为最终作用在头部的恢复加速度。

最终的更新逻辑就是:

复制代码
dp += dt * (spring_force_position + spring_force_velocity)

三、调试中发现的问题与处理

在实际调试过程中发现:

  • 如果弹簧强度系数太小,头部回弹很慢,看不出效果。
  • 如果弹簧强度系数太大,头部会被强行拉回原位,导致无法自然移动,看起来非常僵硬。
  • 而且系统中本身已经存在其他阻力项(drag),可能和弹簧力产生干扰,导致头部反应不如预期。

为了进一步调试效果,我们注释掉部分控制逻辑,测试头部在没有额外力影响下的自然运动,确认问题确实出在弹簧实现而不是其他机制。


四、改进策略:按需施加弹簧力

为了避免弹簧力始终作用于头部,导致其无法正常响应玩家输入,我们引入判断机制,只在特定情况下才应用恢复力:

  1. 判断当前控制输入的强度 dtp

    • 如果几乎没有控制输入(即玩家未移动头部),才施加恢复力。
    • 例如:if |dtp| < 某个阈值,才激活弹簧拉回。
  2. 这样做可以确保:

    • 玩家控制头部时,不会被弹簧阻碍;
    • 玩家放开控制后,头部能自然回归身体位置,实现"释放自动回正"的动态效果。

五、角色之间的引用关系

为了让头部能知道身体的位置,系统中必须建立两者之间的互相引用关系:

  • 头部对象需要能引用到对应的身体对象;
  • 身体对象也可以引用回对应的头部;
  • 在初始化角色时设置这种双向引用关系,确保在逻辑更新时能拿到对方的位置或速度。

这一步非常关键,否则在施加弹簧力时头部根本不知道该朝哪个方向恢复。


六、总结

我们通过以下方式实现了头部的弹簧回弹机制:

  • 构建了基于位置差和速度差的恢复力;
  • 将其应用于头部的速度更新;
  • 加入了"是否正在被控制"的判定机制,避免弹簧干扰正常控制;
  • 建立了头部与身体之间的引用关系,便于获取目标位置;
  • 不断调节弹簧系数,以找到合适的回弹强度;
  • 留下了后续优化空间(例如重构能量系统、完善状态管理等)。

这样就实现了一个具备自然回弹的角色头部控制系统。

重新运行游戏并观察恢复力效果,但注意到朝向受到影响

在实现头部恢复力的过程中,我们现在已经加入了恢复力的方向性,使得弹簧力可以朝着正确的方向作用。然而,目前实现中仍然有一些细节需要调整和优化,尤其是速度阻尼和方向性的问题。

一、恢复力和速度阻尼

首先,恢复力的实现没有加入速度阻尼(damping force)。如果没有阻尼力,头部的恢复力可能会导致振荡,头部来回弹跳,无法平稳地回归目标位置。为了防止这种情况,需要加入速度阻尼来减少振荡。

具体来说,阻尼力会根据头部的当前速度反向作用,强度与速度成正比。这样,速度越快,阻力越大,从而帮助平稳地拉回头部。


二、添加拖拽力

为了进一步控制头部的运动,我们可以通过增加拖拽力来限制头部的过快运动,避免头部在恢复力作用下过于剧烈地回弹。拖拽力的大小可以根据需要调整:

  • 如果当前的拖拽力不足以阻止过大的运动,可以适当增加拖拽的强度。
  • 这可以通过对当前头部的速度 dp 应用一个负向力来实现,这样可以减少头部的移动速度。

三、方向性调整

除了速度和拖拽力之外,另一个重要的调整是让头部的运动沿着"卡迪尔"方向(即标准轴向)进行。当前的实现并没有充分考虑方向性的影响,导致头部的恢复力可能并不完全沿着正确的方向作用。

为了解决这个问题,可以引入一个方向性偏移的机制,使得力的应用会沿着卡迪尔坐标轴(x、y、z)进行校正。这意味着,恢复力的计算不仅要考虑头部与目标位置之间的距离差,还需要根据卡迪尔方向来确保恢复力仅作用于正确的方向。


四、按元素逐步应用力

在实际计算中,为了使力的作用更加精确,应该对每个坐标轴方向(x、y、z)单独计算恢复力。这可以通过在每个坐标轴上逐一检查是否需要施加力来实现。具体地,可以在每个方向上逐个检查是否需要应用恢复力,避免不必要的力作用。

例如,代码中可以采用类似于以下的结构:

cpp 复制代码
for (int e = 0; e < 3; e++) {
    // 检查是否在特定方向上施加力
    if (should_apply_force_in_direction(e)) {
        // 应用恢复力
    }
}

这样做可以使得每个方向的恢复力更具方向性,且每个坐标轴的力可以独立控制。


五、问题总结与改进方向

通过这些改进措施,当前的头部恢复力系统可以得到更精确的控制。具体而言,添加速度阻尼和方向性偏移后,系统的稳定性和头部运动的自然性将得到增强。此外,按元素逐一应用力能够确保每个方向的力都能精确地作用在头部上。

未来可以继续改进的方向包括:

  1. 完善能量系统:重构力学系统,使得恢复力和阻尼力更加自然地与头部运动的其他部分相互作用。
  2. 更细致的调节:根据需要进一步调整弹簧强度、拖拽力和速度阻尼的值,以适应不同的运动场景。
  3. 方向性优化:进一步优化力的方向性计算,确保其更加精确。

最终,整个恢复力系统将能够平稳地控制头部的运动,并让它在不受控制输入时自动回归目标位置,避免过度震荡或不自然的运动。

黑板讲解:将头部重新对准身体运动轴线

我们现在想要进一步改进头部恢复力系统,使其更加智能和自然。我们的目标是让头部在不受控制输入的某个方向上时,自动回归到身体的中心位置,但不会在当前受到输入的方向上进行回正。这个机制可以避免在用户正在主动控制某个方向时产生冲突性的自动回正力。

一、恢复机制的方向性判断

具体来说,我们希望实现如下逻辑:

  • 当头部与身体在某个轴向(如X或Y轴)发生偏移,但当前没有控制输入作用在该方向时,就自动施加一个"弹簧恢复力",将头部拉回到身体的中心线上。
  • 如果在该方向上存在控制输入(例如,玩家正在沿着X轴移动头部),则不进行该轴向上的恢复,让用户的输入优先。
  • 这样可以实现一个"条件恢复力",只在用户不主动控制的方向上施加,从而避免不自然的干扰或僵硬的强制回正。

二、向量恢复力改为轴向分离恢复力

我们原本的恢复机制是基于一个整体的向量方向,即从头部指向身体的差值向量,乘以某个恢复系数形成弹簧力。这种做法存在问题:即使用户正在输入某个方向,也会在该方向上施加恢复力,导致操作感受不顺畅。

为此,我们需要将恢复力的实现从"向量式"改为"轴向分离式"。新的方案如下:

  • 分别对 X、Y、Z(或2D中的X、Y)三个方向进行判断。

  • 在每个方向上单独判断是否存在控制输入:

    • 如果该轴上没有控制输入:计算当前头部与身体在该轴上的位置差,乘以恢复系数,施加该方向上的恢复力。
    • 如果该轴上有控制输入:不施加恢复力。

伪代码大致结构如下:

cpp 复制代码
for (int axis = 0; axis < dimension; axis++) {
    if (abs(user_input[axis]) < threshold) {
        // 没有输入,施加恢复力
        float offset = head_pos[axis] - body_pos[axis];
        float restoring_force = -offset * spring_strength;
        head_dp[axis] += restoring_force * dt;
    }
}

这种做法更具鲁棒性,能够确保恢复力只在"被放任"的方向上自动修正,而不会干扰玩家的主动控制行为。


三、行为示意

  • 如果头部偏离了身体的左侧,而用户正在向上移动头部,那么X轴方向没有输入,会有一个向右的恢复力将其拉回身体中心。
  • 如果头部偏离了身体的下方,而用户正在向下推动头部,那么在Y轴方向上不会有恢复力,避免与用户操作产生矛盾。

四、预期效果

引入这种分离轴向的恢复机制后,头部将更加智能地保持对身体的对齐,只在需要时进行纠正,而不会造成突兀的反向弹跳。整体运动感受将更加自然、流畅且响应性更好。

最终我们实现了一个动态、自适应的头部回正系统,可以显著提升控制体验和物理反馈的合理性。

game_world_mode.cpp 中实现头部重新居中

我们在实现头部的轴向恢复力时遇到了一些逻辑上的问题,并花了一些时间排查问题所在。在当前的实现中,我们尝试按分量逐个判断每个方向(X、Y、Z轴)上控制输入的强度,以便决定是否施加恢复力。


一、恢复力的分量判断逻辑

我们当前的设定逻辑如下:

  • 遍历每个分量(例如0、1、2对应X、Y、Z);

  • 如果某个轴向的控制输入(gdp$$i])低于某个很小的阈值(如0.1f),说明该方向当前没有输入;

  • 在这个方向上就施加恢复力:

    • 方向为(body i ] − h e a d i] - head i]−headi]);
    • 同时将当前速度分量(gdp$$i])加上一个恢复系数乘以方向差值;
    • 并附带一个速度衰减,模拟弹簧减速效果。

但是问题在于------这个逻辑并没有像预期那样生效


二、调试思路与排查过程

我们尝试逐步验证代码,发现:

  • 在做统一向量判断时(整体gdp的长度平方小于0.1f),恢复逻辑是正常工作的;
  • 但当改为分量判断方式时,恢复力却无法生效;
  • 最初认为逻辑没有问题,可能是小地方写错了,例如绝对值判断还是平方比较;
  • 最后排查到一处关键逻辑:恢复力在施加后会被后续的移动逻辑中的 drag(拖拽)力抵消或削弱

这意味着,即便恢复力被正确计算并施加,后续的逻辑可能因为全局的速度阻尼机制对其进行了反向削弱,从而让我们误以为恢复力没有生效。


三、典型表现和疑点

在具体调试过程中,还出现了如下现象:

  • 使用整体向量计算时,恢复效果明显;
  • 使用逐分量逻辑时,不管怎么设置阈值、恢复系数等,效果都不明显;
  • 一度尝试将阈值调得极小甚至荒唐的程度,恢复力依然未体现;
  • 后来突然意识到是后续的 drag 阶段抵消了恢复力;
  • 这导致我们即使在每个方向正确施加了力,也被系统中的其他运动机制给"中和"了。

四、下一步调整方向

我们意识到,为了避免恢复力被系统中其他力抵消,必须进行以下优化:

  1. 调整施加恢复力的位置

    应该确保恢复力在 drag 之前或者明确加入恢复力后不被清除。

  2. 设置区分恢复模式的 drag 系数

    在施加恢复力时,考虑临时降低 drag,或设定不同逻辑下不同的阻尼系数。

  3. 调试每一分量的施加和结果

    加入调试输出或可视化,查看每一帧头部与身体之间的相对位移和速度变化。

  4. 必要时进行物理系统解耦重构

    长远来看,可能需要重构整个运动/控制逻辑的组织方式,让自动力(如恢复)和玩家控制力之间的优先级和干预更清晰地隔离。


五、当前总结

  • 我们已经基本确认逐分量恢复逻辑是合理的;
  • 问题出在恢复力被后续物理处理"抵消"了;
  • 暂时搁置这个问题,留待第二天在头脑清醒时再继续查验;
  • 当前系统中存在若干交叉影响力源,后续需要结构上进一步理清。

最终我们意识到:不是"数学不存在",而是系统中各种作用力之间的逻辑流存在不透明和交叉影响,下一步应当系统性梳理各类力的施加与叠加顺序。

game_world_mode.cpp 中引入未归一化的 ddP2 用于弹簧计算

我们终于发现了之前恢复力不起作用的根本原因:因为我们将恢复用的速度(gdp)传入了后续的移动处理逻辑中,而那部分代码会对向量进行归一化。也就是说:

  • 恢复速度方向虽然是正确的,但在被归一化之后,大小信息就丢失了
  • 恢复力的"力度"完全依赖于向量长度,但归一化操作会让其长度恒为1,导致原有设计的效果彻底被破坏。

一、问题本质

在整个处理链中,存在如下处理顺序:

  1. 我们构造了一个恢复向量 gdp,其大小根据头部偏移量和一些弹簧系数计算;
  2. 然后这个向量被传入移动处理例程,在这个函数内部,会自动对输入向量进行归一化;
  3. 归一化会清除掉方向向量的大小信息,恢复力等同于仅剩一个单位方向,大小始终一致;
  4. 这就导致弹簧恢复机制形同虚设------方向有了,但力度丢了

二、解决方案

为了解决这个问题,我们决定不再使用 gdp 向量来传递恢复力,而是:

  1. 单独创建一个新的恢复向量变量 ,不依赖 gdp
  2. 将这个恢复向量在逻辑处理完成之后直接加进头部的位置增量或速度向量中
  3. 绕开后续会对 gdp 做归一化处理的部分,从而保留恢复力本身应有的方向和大小。

也就是说,我们将 gdp 专门保留用于玩家输入控制,而恢复力不通过它传递,而是另建通道直接注入。


三、实现方式思路

我们采用如下方式:

cpp 复制代码
Vec3 recovery_force;

// 逐分量判断是否缺少输入,如果是,则添加恢复力分量
for (int i = 0; i < 3; ++i) {
    if (abs(gdp[i]) < some_threshold) {
        recovery_force[i] = (body[i] - head[i]) * spring_constant - velocity[i] * damping_factor;
    }
}

// 计算最终增量速度或位置
dp += recovery_force * dt;
  • gdp 用于判断是否存在输入;
  • 只有在某个方向无输入时,才施加恢复力;
  • 恢复力由位移差乘弹性系数减去当前速度乘阻尼系数组成;
  • 最后将 recovery_force 直接加到最终位置/速度中。

四、好处总结

  • 避免恢复力被归一化处理破坏;
  • 控制逻辑清晰分离,玩家输入和自动恢复各行其道;
  • 弹簧恢复力度和方向都可以通过参数自由调控;
  • 代码更健壮,逻辑更透明。

五、下一步方向

  1. 彻底检查整个移动逻辑流程,确保恢复力不再被其他地方覆盖或抵消;
  2. 将这个新逻辑系统化地整理到物理模块或行为控制模块中,减少"临时拼接"的现象;
  3. 后续可以根据使用反馈对恢复力的弹性系数、阻尼系数做动态调整,适配不同角色或场景需要。

这一修复思路标志着我们成功绕开了归一化对恢复机制的破坏,使得弹簧恢复逻辑真正具备了应有的效用。

运行游戏并确认问题已经修复

我们终于确认之前的问题完全就是出在那个归一化操作上------这点现在完全说得通了。虽然接下来还有很多工作要做,但整体系统已经开始逐渐成形,并且效果也越来越令人满意。

目前的状态让我们感到非常振奋,系统的响应和动态反馈已经具备良好的基础。可以明确地预感到,随着后续调整的深入,这个机制会变得真正强大且实用。

最重要的是,我们成功避免了一种可能会让整体体验变差的技术陷阱。如果没有找到这个归一化的问题,那么最终出来的效果会显得僵硬或失衡,而我们会完全不知道原因。现在既然问题找到了,路径也明确了,就可以安心推进下去了。

总的来说,这一阶段是个很关键的转折点,让我们从"感觉不对但说不上来"中走出来,逐步踏上"真实可控、具有表现力"的道路。尽管还有不少事情要处理,但从当前趋势来看,这个系统的最终效果会非常不错。

有人将新的运动风格类比为《节奏地牢》,我们觉得手感其实更棒

有人把这个新的移动系统比作《Griptape the Necromancer》的感觉。那款游戏本身就很有趣,我们能理解这种比较的出发点,但目前这个系统的表现已经明显更加流畅,操控感也更具吸引力,玩起来手感会更好。

从整体体验来看,这个新设计在风格上非常独特,和市面上的常规方案差异明显。在逐步完善的过程中,操控角色移动的反馈已经非常自然,并具备一种令人愉悦的物理感和动能感。

随着系统逐渐成熟,我们越来越能感受到,这种移动方式将带来非常优秀的可玩性。不仅在移动中具备更强的表现力,还让角色的操作更加丰富和富有层次。角色的动作反应不再是线性死板的,而是带有弹性和恢复力,使整体体验更具沉浸感。

未来随着更多细节的打磨,我们预期最终的玩法不仅手感上佳,而且会具备高度的操作自由度和策略性,非常值得期待。

可能我错过了,是什么促使我们将角色移动从模拟滚动改为格子跳跃风格的?

我们之所以从类比式的"滚动"移动切换到数字化的"跳跃式"点对点移动,是因为游戏的设计需求决定了这一点。

游戏的整体玩法基于时间线系统,角色的移动方式也必须配合这个核心机制。为此,我们现在采用了基于固定点的跳跃机制,即角色只能在预先定义的一组位置点之间进行移动,而不是在一个连续的空间中自由滑动。

虽然目前我们使用了类似网格的布局来测试和实现这些点位,是因为很多场景确实会呈现出规则的网格形态,但本质上这些点并不是严格的"格子"或"瓦片"。实际上,它们只是我们手动放置的、位置固定的点位。它们可以以任何形式排列,不局限于网格布局,这就赋予了我们极大的灵活性,可以根据不同关卡的美术和玩法需求自由设计点位的排列方式。

这种跳跃式的移动机制符合我们游戏的节奏感和操作策略性,角色在这些固定点之间跳跃,增强了动作的明确性和反馈感,同时也让控制方式更具有策略性和表现力。这个机制也为之后的玩法系统(如攻击、闪避、互动等)奠定了清晰可控的基础。

角色身体是否始终会沿着固定网格移动?

角色的主体并不总是在严格的网格上移动,大多数情况下并不是网格结构。但主体始终只能从一个固定点跳跃到另一个固定点进行移动。虽然这些点有时候可能会呈现出网格的排列方式,但核心机制并不依赖于网格,而是基于一组手动或程序生成的预设位置点。

与之相对的是角色的"头部",它拥有相对自由的移动空间,可以在身体的基础上略微偏离,进行更动态的表现。头部的移动不受严格的点限制,可以在一定范围内"漂移"或自然摆动,从而营造出更具表现力和生命力的动画效果。

而身体部分始终需要回到或对齐到最近的一个有效点位上,不允许在两个点之间的中间状态悬停。这一机制确保了核心交互和路径判断的明确性,有利于控制、动画和关卡逻辑的设计。

关卡生成系统会在场景中预设好这些点位,根据特定的玩法意图和挑战方式排布它们,玩家需要在这些点之间进行跳跃移动,以实现探索、战斗和解谜等行为。这种设计在保持操作明确性的同时,也为动作和节奏创造了丰富的策略空间和表现可能。

玩家是否可以看到这些设计点?

玩家在游戏中通常是可以清楚看到角色可站立的点位的。整体美术设计会明确地表现出这些点位的位置,让玩家在视觉上能够自然地识别出哪些地方是可以站上去的、哪些是路径节点。除非在某些特定区域中,出于设计目的,故意隐藏了这些信息,以营造悬念、增加挑战或引导探索行为。

这种方式既保证了大部分时间里玩家的移动目标和路径是明确的,也允许在需要时引入一些迷惑性或隐藏机制,以提升关卡的多样性和趣味性。美术与设计将协同工作,让玩家无需依赖显式的格子提示就能感知游戏的移动结构,从而维持游戏的美观性和沉浸感。

角色身体是否会朝向头部倾斜?还是保持分离?

身体会朝向头部的方向倾斜,在角色移动时,身体会有一种拉伸的效果,会像被牵引一样往头部的方向延展。这种拉伸并不是完全硬性的连接,而是更偏向一种弹性的视觉表现。

这种表现本身有一定的挑战性,因为在游戏动画中很常见的一个问题是,动画效果无法准确预测玩家的行为。例如,无法预判玩家是否会突然松开方向键或改变方向,这使得动画很难始终与玩家的操作保持一致。因此,身体的倾斜和拉伸程度会在"连接头部的需要"与"动画自然性"之间进行权衡。

虽然这种方式可能会在视觉效果上稍逊于那种完全不考虑操控感的做法,但核心目标是让头部的操控感足够流畅和舒适。头部的控制手感始终是优先考虑的要素,为了达到这一点,身体部分的动画会适度牺牲一点外观上的精细度,以换取整体的响应性和操作反馈的清晰度。

头部是否应该始终绘制在披风之上?

当前头部应该始终绘制在披风之上,但现在的问题出在Z轴排序上。目前头部、身体和披风这三个部分实际上都在相同的Z值上,这显然是不对的,导致在渲染时它们之间的绘制顺序是错误的。

现在我们已经在渲染系统中加入了排序功能,这是之前还没有的功能,因此我们需要尽快对现有内容进行调整,确保渲染顺序能够反映出正确的图层逻辑。当前资源中头部被披风遮挡的问题,正是因为它们的Z轴没有区分导致的。

除了Z值问题之外,图像资源的放置也存在问题。现在的精灵图是直接将角色部位在位图内做了位移处理,但实际上这种处理方式并不符合我们真正的需求。我们希望的是,每个角色部位的实际位置由游戏逻辑来控制,而不是通过图像内部的偏移来实现。

在理想情况下,每个部位应该按照其逻辑位置来放置,Z轴也应该根据实际深度进行正确排序。我们也希望利用Z轴偏移实现角色在空间中"高低"感的表现,例如头部略高于身体,身体略高于披风。但由于当前所有部件在数据中都处于同一Z层,因此游戏无法正确判断绘制顺序,这就造成了现在这种看起来完全错误的视觉效果。

因此,接下来的工作重点之一就是完善Z轴排序逻辑,确保头部永远在披风之上,整个角色部件的绘制顺序符合物理直觉和美术设计要求。

其他实体是否也只能在这些点之间移动?还是可以自由移动?

实体的移动方式既有在固定点上移动的,也有可能是自由移动的。大多数实体会在固定的点之间移动,这些点是通过网格布局实现的。然而,也有一些特殊的情况,某些实体的移动不完全依赖于这些点。具体情况会根据游戏中实际情况调整,可能会根据加入的内容而发生变化。

对于需要在网格上移动的实体,需要编写相应的AI代码来确保它们能够沿着网格进行移动。此外,也有一些实体不一定需要遵循网格布局,它们可以通过路径规划(Pathfinding)自由移动,即不必局限于网格的结构。这些自由移动的实体需要一种算法来帮助它们在环境中找到合适的路径。

这种将预设动画与物理模拟动画混合的技术有名称吗?在动画/3D 软件中常见吗?

这种将物理模拟与动画融合的技术,是否有一个明确的名字,并不是很确定。一般来说,动画师在3D动画软件中会使用表达式控制骨骼系统的各个元素,将其转化为一种符合需求的动画效果。骨骼系统本身就是一个层级化的系统,能够通过这种方式工作。

然而,虽然可以在动画包中进行一定程度的物理模拟,但其实现存在局限性,因为这些动画包最初并不是为物理模拟设计的。物理模拟与动画包的工作方式往往是冲突的,因此它们的结合往往显得有些勉强。在很多情况下,动画师需要将物理模拟的结果"烘焙"成一个数据通道,然后再在动画中使用这些数据。

因此,虽然在动画软件中可以实现这种物理与动画结合的技术,且它是常见的,但实际上要比直接实现物理模拟要复杂得多,也更加困难。不同的软件包在这方面的支持程度也有所不同,一些软件在处理物理与动画融合时会更方便,而另一些则相对更难实现。

为什么我应该使用动画软件,而不是像这样直接在引擎中做动画?

首先,对于一些独立开发者或小团队来说,手动编写动画和物理效果可能是更合适的选择,尤其是当需要更高的灵活性和控制时。在这种情况下,可以尽量减少对手工动画的需求,因为手工编写代码能够实现更多自定义的效果。例如,像这种项目,虽然需要外部艺术创作,但大部分的工作都是由开发者自己完成的。

然而,如果有一个专业的动画团队支持,情况就不同了。专业的动画师可以通过使用动画包中的时间线、关键帧等工具,制作更精细和复杂的动画。这种方法更适用于那些需要精细表现和动作捕捉的动画场景,比如游戏中的人物动作。比如像《使命召唤》中的凯文·史派西角色,无法通过手动编写代码来实现这么复杂的动画,而是通过动作捕捉技术和动画师的调整来完成。

当动画涉及到更为细腻的情感表达、肌肉运动等人类动作时,手工编写代码来模拟这些动作变得不切实际。此时,使用动画包和工具,结合动画师的专业知识,可以更好地处理这些复杂的任务。通过工具,动画师可以输入预定的、非物理的动作序列,并将其整合到游戏中,从而避免了手动编码的巨大工作量。

总之,物理模拟动画对于一些简单的场景非常合适,能够带来动态和有趣的效果,但当涉及到大量的复杂数据和人物表演时,使用动画包和专业工具就显得更为必要。

将来有添加音乐的计划吗?

音乐部分基本上已经完成,只剩下一些小的调整。当前的版本可能已经能够支持播放音乐文件。接下来的工作主要是实现不同区域之间的音乐过渡效果,具体来说,就是能够在玩家从一个区域移动到另一个区域时,平滑地切换不同的音乐。这样,随着场景的变化,音乐也会随之变化,带来更加沉浸的游戏体验。

一个不太严肃的问题,是否可以使用神经网络做出类似波士顿动力 BigDog 的角色?

关于是否可以使用神经网络来制作类似于波士顿动力的大狗机器人,实际上并不清楚波士顿动力在大狗机器人中是否使用了神经网络。最初,波士顿动力的大多数机器人算法是基于橡胶跳跃者算法,而并非神经网络,因此并不依赖神经网络来实现机器人运动和稳定控制。大狗的控制系统主要是通过标准的平衡算法来完成的,而不是通过神经网络。

不过,目前波士顿动力是否已经转向使用神经网络或其他新技术并不清楚,因为没有最新的信息。总体来说,波士顿动力的机器人早期并不需要神经网络,仅依赖经典的控制算法来保持平衡。所以,是否能用神经网络来实现类似的大狗机器人,无法准确回答,因为并不了解波士顿动力现在的做法。

相关推荐
我想吃余6 分钟前
【Linux修炼手册】Linux开发工具的使用(一):yum与vim
linux·运维·学习·vim
Chef_Chen1 小时前
从0开始学习大模型--Day06--大模型的相关网络架构
运维·服务器·学习
byte轻骑兵1 小时前
【Bluedroid】蓝牙HID DEVICE断开连接流程源码分析
android·c++·蓝牙·hid·bluedroid
strongwyy2 小时前
DA14585墨水屏学习(2)
前端·javascript·学习
renhl2522 小时前
英语句型结构
学习
修修修也2 小时前
【C++】特殊类设计
开发语言·c++·特殊类·类与对象
海尔辛3 小时前
学习黑客了解Python3的“HTTPServer“
学习
byte轻骑兵3 小时前
【C++重载操作符与转换】转换与继承
开发语言·c++
白天学嵌入式3 小时前
STM32f103 标准库 零基础学习之按键点灯(不涉及中断)
stm32·单片机·学习