随时间加速

Acceleration over time

场景

我正在开发一个俯视图游戏,在该游戏中敌人会向特定位置移动。移动的目的地通常会发生巨大变化 - 有时敌人仍在朝着先前的目的地移动......

我想实现比线性运动更真实的运动,所以当敌人在目的地之间切换时应该有一些加速和减速。

转向(方向)不是一个因素。您可能会假设精灵会像气垫船一样移动,在目的地之间尽可能快地在加速和减速方面进行漂移。

为简单起见 - 假设只有一维 (Y) 而不是 X 和 Y...运动应模拟只能向北或向南移动的汽车。

由于我们考虑的是这种情况下的真实运动,因此随着时间推移的最大速度也在考虑范围之内,您可能不会感到惊讶。敌人永远不能超过自己的最大速度;敌人将自己的最大速度存储在一个变量中。

最后要考虑的是,敌人不仅会有 'maximum speed' 值,而且还会有 'maximum acceleration' 值 - 这将决定每个敌人的反应速度朝相反的方向移动。

为简单起见,假设敌人没有任何运动摩擦...当它停止加速时,它只是一直保持当前速度巡航。

例子

对于上下文,让我们详细说明汽车示例。特定汽车具有:

就像我开车时,我想象所有这些值都存在,但我无法改变它们。我真正能改变的是我在 acceleration/brake 上的投入程度。我们称其为 'throttle'。就像汽车中的加速踏板一样,我可以随时将此值更改为任意值,根本没有时间。

我可以踩下油门(油门=1),立即松开踏板(油门=0),甚至换成倒档再次踩下脚(油门=-1)。让我们假设油门的这些变化是即时的(不像速度或加速度,grow/shrink 随着时间的推移)

综上所述,我想我真正需要计算的唯一值是油门应该是多少,因为这是我在车辆中唯一可以控制的东西。

那么,我怎么知道在任何给定时刻使用多少油门,以尽快到达目的地(如一些红绿灯)而不会超过我的目的地?我需要知道将油门踩下多少,短暂地完全不加速,然后在接近目的地时减速的力度有多大。

抢占移动

这个游戏可能会有一个在线组件。也就是说,玩家将通过套接字连接传输他们的位置......然而,即使是最好的连接也永远无法足够频繁地发送位置以实现平滑移动 - 你需要插值。您需要发送 'velocity'(速度)以及坐标,以便我可以在接收数据包之间的时间里假设未来的位置。

出于这个原因,Tweens 是不行的。没有办法发送一个补间,然后准确地通知其他方每个实体当前在每个补间的哪个点(我的意思是,我想这是可能的,但非常复杂并且可能涉及相当大的数据包发送,并且可能是就在线组件而言也很容易被利用),那么你需要考虑在目的地改变时中止 Tweens,等等。

不要误会我的意思,我可能已经可以使用 Tweens 的 ease-in/ease-out 功能模拟一些非常逼真的动作,它看起来很棒,但在在线设置中会.. 非常混乱.

目前看起来怎么样?

所以,基本上我已经确定任何时候需要计算的主要方面是使用多少油门。让我们来看看我的逻辑...

想象一个非常基本的线性随时间变化的公式...它看起来像这样:

示例 1 - 随时间变化的位置

currentDY = 5;               // Current 'velocity' (also called Delta Y or 'DY')
currentY += currentDY * time // Current Y pos is increased by movement speed over time.

如您所见,在任何给定时刻,由于 'velocity' 或 DY(随时间),Y 位置随时间增加。时间只是一个因素,所以一旦我们到达目的地,我们只需将 DY 设置为零。非常尖锐的不切实际的运动。为了平滑运动,速度还需要随时间变化...

示例 2 - 随时间变化的速度

throttle = -1
currentDY += throttle * time;
currentY += (currentDY * time);
//Throttle being -1 eventually decelerates the DY over time...

在这里,油门是“-1”(最大反向!),所以随着时间的推移,这会降低速度。这对于逼真的加速非常有用,但不提供逼真的预期或减速。

如果我们这次到达目的地,我们可以将油门设置为'0'...但是已经来不及刹车了,所以结果敌人只会继续移动永远超过目标。我们可以 throttle = '1' 返回,但我们最终会永远来回摆动。

(另请注意,最大加速度和速度甚至还不是一个因素 - 它肯定需要成为一个因素!敌人不能永远提高他们的速度;速度增量和加速度增量都需要有限制)。

综上所述,仅仅随时间改变速度是不够的,我们还需要能够在它发生之前预测减速多少(即 'backwards throttle')。这是我到目前为止所得到的,我几乎可以肯定这是错误的方法...

示例 3 - 油门超时?? (我卡住了)

guideY = currentY + (currentDY * (timeScale * 3000));
dist = endY - guideY;
throttle = Math.max(-1, Math.min(1, dist / 200));
currentDY += throttle * time;
currentY += (currentDY * time);

如你所见,这次我们试图通过 猜测 敌人的位置来预测使用多少 throttle未来的任意时间(即 3 秒)。如果 guideY 超过了目的地,我们知道我们必须开始刹车(即减速以停在目的地之上)。多少-取决于敌人未来位置的距离(即throttle = dist / 200;

这是我放弃的地方。测试此代码并更改值以查看它们是否正确缩放,敌人总是在目的地上方摆动,或者花费太长时间到达目的地 'close in'。

我觉得这是错误的方法,我需要更准确的方法。感觉我需要一个交叉点来正确预测未来的位置。

我只是为 3 secondsdist / 200 使用了错误的值,还是我没有在此处实施完全有效的解决方案?

目前,与直线运动相比,到达目标位置似乎总是需要8倍的时间。我什至还没有达到为 DeltaVelocity 或 DeltaAcceleration 实现最大值的地步 - 接受的解决方案必须考虑这些值,尽管在我的 JSFiddle 下面...

测试我的逻辑

我已将我所有的示例放在一个可用的 JSFiddle 中。

JSFiddle working testbed (单击 canvas 下方的 'ms' 按钮可模拟时间流逝。单击一个按钮然后按住 Return 可快速重复)

精灵最初在 'wrong' 方向移动 - 这是为了测试稳健性 - 它假设了一个想象的场景,我们刚刚尽可能快地向屏幕下方的旧目的地移动,现在我们需要突然开始向上移动...

如你所见,我的第三个例子(见update函数),精灵'settle'到达所需位置的时间比它应该花费的时间要长得多。我的数学错了。我无法理解这里需要什么。

在任何给定时间 throttle 应该是什么?使用 throttle 甚至是正确的方法吗?非常感谢您的帮助。

决胜局

好吧,我已经坚持了好几天了。这个问题正在上升以获得一些 phat 赏金。

如果需要决胜局,获胜者将需要证明数学足够合法以进行反向测试。原因如下:

由于游戏还包含多人游戏组件,因此敌人将传输其位置和速度。

作为黑客保护,如果任意两个采样时间之间的速度和位置是可能的

,我最终将需要一种远程方法'check'

如果根据最大速度和加速度移动太快,将调查帐户等。您可以假设游戏会提前知道敌人的真实最大加速度和速度值。

因此,除了赏金之外,您还可以满意地知道您的答案将有助于破坏肮脏的视频游戏作弊者的生活!

编辑 2:Fiddle 由答案作者添加;添加到他的答案中以防其他人发现这个问题:http://jsfiddle.net/Inexorably/cstxLjqf/。 Usage/math 下文有进一步解释。

编辑 1:重写以便在评论中收到澄清。

你真的应该改变你的实施风格。

假设我们有以下变量:currentX、currentY、currentVX、currentVY、currentAX、currentAY、jerk。 currentVX 就是你的 currentDX。同样,currentAX 是速度增量值的 x 分量(加速度是速度的导数)。

现在,按照您的风格,我们将有一个 guideX 和一个 guideY。但是,您这样做还有另一个问题:您通过在三秒内预测目标的位置来找到 guideY。虽然这是个好主意,但无论您离目标有多近(无论 dist 有多小),您都在使用三秒。所以当精灵距离目标 0.5 秒时,它仍然会朝着目标的估计位置移动(未来三秒)。这意味着它实际上无法击中目标,这似乎是您暗示的问题。

继续,回忆一下我之前提到的变量。这些是当前变量——即,它们将在几秒钟后的每次调用中更新(就像您之前所做的那样)。您还提到希望拥有 maxV 和 maxA。

请注意,如果您的 currentVX 为 5,currentVY 为 7,则速度为 (5^2+7^2)^0.5。因此,每次更新 'current' 变量原型时,您要做的是在更新值之前,查看这些值的大小(所以 sqrt(x^2+y^2)像我用速度显示的变量)将超过您设置为常量的相应 maxV、maxA 或 jmax 值。

我还想改进您生成指导值的方式。我假设指南可以移动。在这种情况下,目标将具有上面列出的值:x、y、vx、vy、ax、ay、jx、jy。你可以随心所欲地命名这些,我将使用 targetX、targetY...等来更好地说明我的观点。

从这里您应该可以找到您的指导值。当精灵距离目标超过三秒时,您可以使用目标在三秒内的位置(注意:我建议将其设置为变量,以便于修改)。对于这种情况:

 predictionTime = 3000*timescale; //You can set this to however many seconds you want to be predicting the position from.

如果您真的想要,您可以使用积分函数或循环来平滑曲线以获得指导值,以便从目标值中获得更准确的指导值。但是,这不是一个好主意,因为如果您曾经实现多个目标等,它可能会对性能产生负面影响。因此,我们将使用一个非常简单的估计,对于如此低的成本来说非常准确。

 if (sprite is more than predictionTime seconds away from the target){
      guideX = targetX + predictionTime * targetVX;
      guideY = targetY + predictionTime * targetVY;
 }

请注意,我们在此并未考虑目标的加速度和加加速度,这种简单的近似不需要它。

但是,如果精灵距离目标的距离小于 predictionTime 秒怎么办?在这种情况下,我们希望开始逐渐减少我们的 predictionTime 值。

 else{
      guideX = targetX + remainingTime * targetVX;
      guideY = targetY + remainingTime * targetVY;.
 }

在这里您可以选择三个选项来计算剩余时间的价值。您可以将 remainingTime 设置为零,使向导坐标与目标坐标相同。您可以将 remainingTime 设置为 sqrt((targetX-currentX)^2+(targetY-currentY))/(sqrt(currentVX)^2+(currentVY)^2),这基本上是 2d 中的距离/时间,便宜且体面的近似值。或者您可以使用前面提到的 for 循环来模拟积分以说明变化的速度值(如果它们偏离 maxV)。但是,通常您会保持在或接近 maxV,因此这真的不值得付出代价。编辑:此外,我还建议将 remainingTime 设置为 0,如果它小于某个值(可能大约 0.5 左右)。这是因为你不希望 hitbox 问题,因为你的 sprite 遵循具有轻微偏移的引导坐标(以及绕圈移动的目标会在改变方向时为其提供更大的速度值/本质上是一种强大的规避策略。也许你应该专门为此添加一些东西。

我们现在有了 guideX 和 guideY 值,并且考虑到非常接近移动目标会影响引导坐标应放置在距目标多远的位置。我们现在将做 'current' 值原型。

我们将首先更新最低的导数值,检查它们是否在我们的最大值范围内,然后更新下一个最低的等等。注意 JX 和 JY 如前所述,以允许非常数加速度。

 //You will have to choose the jerk factor -- the rate at which acceleration changes with respect to time.    

 //We need to figure out what direction we're going in first.  Note that the arc tangent function could be atan or whatever your library uses.
 dir = arctan((guideY-currentY)/(guideX-currentX));

这将 return 方向作为一个角度,以弧度或度数为单位,具体取决于您的三角函数库。这是您的 sprite 需要沿着引导方向移动的角度。

 t = time; //For ease of writing.
 newAX = currentAX + jerk*t*cos(dir);
 newAY = currentAY + jerk*t*sin(dir);

您可能想知道 newAx 值将如何减少。如果是这样,请注意,如果向导位于目标左侧,cos(dir) 将 return 为负,同样,如果精灵需要向下移动,sin(dir) 将 return 为负。因此,还要注意,如果向导直接位于精灵下方,则 newAx 将为 0,newAY 将为负值,因为它正在下降,但加速度的大小,换句话说,与 maxA 相比,将为正 - - 即使精灵向下移动,它也不会以负速度移动。

请注意,因为 cos 和 sin 可能与 atan 属于同一个库,所以单位将相同(所有度数或所有弧度)。我们有一个最大加速度值。所以,我们将检查以确保我们没有超过那个。

 magnitudeA = sqrt(newAX^2+newAY^2);
 if (magnitudeA > maxA){
      currentAX = maxA * cos(dir);
      currentAY = maxA * sin(dir);
 }

所以在这一点上,我们要么已经限制了我们的加速度,要么具有令人满意的加速度分量,其幅度小于 maxA。让我们对速度做同样的事情。

 newVX = currentVX + currentAX*t;
 newVY = currentVY + magnitudeA*t*sin(dir);

请注意,我在这里提供了两种查找速度分量的方法。任何一个都有效,为了简单起见,我建议选择一个并将其用于 x 和 y 速度值。我只是想强调加速度大小的概念。

 magnitudeV = sqrt(newVX^2+newVY^2);
 if (magnitudeV > maxV){
      currentVX = maxV * cos(dir);
      currentVY = maxV * sin(dir);
 }

我们也想停止围绕我们的目标回旋。但是,我们不想像您在 JSFiddle 中那样只是说放慢速度,因为那样的话,如果目标在移动,它就会逃脱(笑)。因此,我建议检查你有多近,如果你在一定的距离内,则根据距离 和目标速度的偏移 线性降低你的速度。因此,将 closeTime 设置为较小的值,例如 0.3 或游戏中感觉良好的值。

 if (remainingTime < closeTime){
      //We are very close to the target and will stop the boomerang effect.  Note we add the target velocity so we don't stall while it's moving.
      //Edit: We should have a min speed for the enemy so that it doesn't slow down too much as it gets close.  Lets call this min speed, relative to the target.
      currentVX = targetVX + currentVX * (closeTime - remainingTime);
      currentVY = targetVY + currentVY * (closeTime - remainingTime);
      if (minSpeed > sqrt(currentVX^2+currentVY^2) - sqqrt(targetVX^2-targetVY^2)){
           currentVX = minSpeed * cos(dir);
           currentVY = minSpeed * sin(dir);
      }

 }
 magnitudeV = sqrt(currentVX^2+currentVY^2);

此时我们也有很好的速度值。如果您要安装速度计或检查速度,您对 magnitudeV 感兴趣。

现在我们对位置做同样的事情。请注意,您应该检查位置是否良好。

 newX = currentX + currentVX*t;
 newY = currentY + currentVY*t;

 //Check if x, y values are good.
 current X = newX; currentY = newY;

现在所有内容都已更新为正确的值,您可以在屏幕上写字了。