物理游戏编程Box2D - 使用扭矩定位类似炮塔的对象

9
这是我在使用LÖVE引擎实现游戏时遇到的问题,该引擎使用Lua脚本覆盖了box2d
目标很简单:一个炮塔状物体(从上面看,处于2D环境中)需要定位自己,使其指向一个目标。
炮塔位于x、y坐标上,目标位于tx、ty上。我们可以认为x、y是固定的,但tx、ty往往会因时间而变化(例如鼠标光标)。
炮塔有一个转子,可以在任何时刻施加旋转力矩,顺时针或逆时针。该力矩的大小有一个上限,称为maxTorque。
炮塔还有一定的旋转惯量,对于角动量的运动起到与质量对于线性运动的作用相同的作用。没有任何形式的摩擦力,因此如果具有角速度,则炮塔将继续旋转。
炮塔有一个小型AI函数,重新评估其方向以验证其是否指向正确的方向,并激活旋转器。这发生在每个dt(约每秒60次)。它目前看起来像这样:
function Turret:update(dt)
  local x,y = self:getPositon()
  local tx,ty = self:getTarget()
  local maxTorque = self:getMaxTorque() -- max force of the turret rotor
  local inertia = self:getInertia() -- the rotational inertia
  local w = self:getAngularVelocity() -- current angular velocity of the turret
  local angle = self:getAngle() -- the angle the turret is facing currently

  -- the angle of the like that links the turret center with the target
  local targetAngle = math.atan2(oy-y,ox-x)

  local differenceAngle = _normalizeAngle(targetAngle - angle)

  if(differenceAngle <= math.pi) then -- counter-clockwise is the shortest path
    self:applyTorque(maxTorque)
  else -- clockwise is the shortest path
    self:applyTorque(-maxTorque)
  end
end

......它失败了。让我用两个例子来解释一下:

  • 炮塔在目标角度周围“振荡”。
  • 如果目标“就在炮塔的右后方,稍微顺时针”,则炮塔将开始施加顺时针扭矩,并继续施加,直到超过目标角度的瞬间。此时它将开始朝相反方向施加扭矩。但是它已经获得了很大的角速度,因此它会继续顺时针旋转一段时间……直到目标变成“稍微逆时针”的位置。然后它又会重新开始。因此,炮塔将振荡甚至绕圈。

我认为我的炮塔应该在达到目标角度之前开始施加“最短路径的相反方向”的扭矩(就像汽车在停止之前刹车)。

直觉告诉我,当炮塔到达目标的一半时,它应该“开始施加最短路径相反方向的扭矩”。我的直觉告诉我这与角速度有关。然后还有一个事实,即目标是移动的-我不知道是否应该以某种方式考虑它,还是只需忽略它。

我如何计算炮塔必须“开始刹车”的时间?

5个回答

3

倒推思考。当炮塔有足够的空间从当前角速度减速到静止时,必须“开始刹车”,这与需要从静止加速到当前角速度所需的空间相同。

|differenceAngle| = w^2*Inertia/2*MaxTorque.

如果您的步骤时间太长,围绕目标发生小振荡可能会让您遇到一些麻烦;这将需要更多的技巧,您需要尽早并更加温和地刹车。在看到之前不要担心它。
现在应该足够好了,但还有一个问题可能会让您后来遇到麻烦:决定要走哪条路。有时候绕路反而更快,如果你已经朝着那个方向走了。在这种情况下,您必须决定哪种方式需要较少的时间,这并不困难,但是再次提醒,到时再处理吧。 编辑:
我的公式错了,应该是惯性/(2*最大扭矩),而不是2*最大扭矩/惯性(这就是我试图在键盘上做代数运算的结果)。我已经修复了它。
试试这个:
local torque = maxTorque;
if(differenceAngle > math.pi) then -- clockwise is the shortest path
    torque = -torque;
end
if(differenceAngle < w*w*Inertia/(2*MaxTorque)) then -- brake
    torque = -torque;
end
self:applyTorque(torque)

嗨Beta,谢谢你的回答。我应该提到我的数学不是很强。我应该怎么处理这个方程?对你来说似乎有一些隐含的东西,但我就是看不出来。 - kikito
我认真思考了一下。根据我的尝试,对于给定的时间t,角度=最大扭矩* t * t /惯性。你是如何从那个方程式推导出你在例子中展示的方程式的,这让我感到困惑。 - kikito
你差不多就快做到了:加速度 alpha = 最大扭矩 / 惯性,w = alpha * t,但是要计算角度,您必须使用时间段内的平均速度,因此角度=(最大扭矩t /惯性) t / 2。现在取w = alpha * t并将其平方:w * w = alpha * alpha * t * t,并使用它来消除t * t:角度= alpha * t * t / 2 = alpha *(w * w / alpha * alpha)/ 2 = w * w / 2 * alpha = w * w *惯性 / 2 *最大扭矩。 - Beta
我认为公式可能是错误的。但是,它让我走上了正确的轨道。因此加1分!你忽略了“制动”仅在某些情况下发生的事实,这取决于w是正数还是负数。请查看我的答案以获得完整的解释。 - kikito

1

这似乎是一个可以使用PID控制器解决的问题。在我的工作中,我使用它们来控制加热器输出以设定温度。

对于“P”分量,您应用与炮塔角度和目标角度之间差异成比例的扭矩,即:

P = P0 * differenceAngle

如果仍然振荡过大(会有一点),那么添加一个“I”分量,

integAngle = integAngle + differenceAngle * dt
I = I0 * integAngle

如果这个超调太多了,那就加一个“D”项。
derivAngle = (prevDifferenceAngle - differenceAngle) / dt
prevDifferenceAngle = differenceAngle
D = D0 * derivAngle

P0I0D0 是常数,您可以调整它们以获得所需的行为(例如炮塔响应速度等)。

作为提示,通常情况下,P0 > I0 > D0

使用这些术语确定施加的扭矩量。

magnitudeAngMomentum = P + I + D

编辑:

这里有一个使用Processing编写的应用程序,它使用PID。实际上,即使没有I或D,它也可以正常工作。在这里查看它的运行情况


// Demonstration of the use of PID algorithm to 
// simulate a turret finding a target. The mouse pointer is the target

float dt = 1e-2;
float turretAngle = 0.0;
float turretMass = 1;
// Tune these to get different turret behaviour
float P0 = 5.0;
float I0 = 0.0;
float D0 = 0.0;
float maxAngMomentum = 1.0;

void setup() {
  size(500, 500);  
  frameRate(1/dt);
}

void draw() {
  background(0);
  translate(width/2, height/2);

  float angVel, angMomentum, P, I, D, diffAngle, derivDiffAngle;
  float prevDiffAngle = 0.0;
  float integDiffAngle = 0.0;

  // Find the target
  float targetX = mouseX;
  float targetY = mouseY;  
  float targetAngle = atan2(targetY - 250, targetX - 250);

  diffAngle = targetAngle - turretAngle;
  integDiffAngle = integDiffAngle + diffAngle * dt;
  derivDiffAngle = (prevDiffAngle - diffAngle) / dt;

  P = P0 * diffAngle;
  I = I0 * integDiffAngle;
  D = D0 * derivDiffAngle;

  angMomentum = P + I + D;

  // This is the 'maxTorque' equivelant
  angMomentum = constrain(angMomentum, -maxAngMomentum, maxAngMomentum);

  // Ang. Momentum = mass * ang. velocity
  // ang. velocity = ang. momentum / mass
  angVel = angMomentum / turretMass;

  turretAngle = turretAngle + angVel * dt;

  // Draw the 'turret'
  rotate(turretAngle);
  triangle(-20, 10, -20, -10, 20, 0);

  prevDiffAngle = diffAngle;
}

这种方法的问题在于它是为像加热系统这样的东西设计的,你所控制的是功率,这是温度的一阶导数;而egarcia正在控制扭矩,这是第二个。P会因为它的目标是a=0而不是w=0而大幅超调,I对振荡没有帮助,D可能有效但会使过程变慢。 - Beta
你说得对,我给出的例子并没有直接涉及到扭矩。然而,当考虑到炮塔旋转的摩擦机制时,maxAngleMomentummaxTorque成正比 - 在使用任意单位时可以将它们视为可互换的。 - Brendan
实现看起来还不错。"保持角动量"的想法很有趣。然而,这不是我要求的-我想施加扭矩,而你最终是自己设置角度。但是,对于制作演示和代码优雅性,我给你点赞。 - kikito

1

好的,我相信我找到了解决方案。

这是基于Beta的想法,但需要进行一些必要的调整。下面是具体步骤:

local twoPi = 2.0 * math.pi -- small optimisation 

-- returns -1, 1 or 0 depending on whether x>0, x<0 or x=0
function _sign(x)
  return x>0 and 1 or x<0 and -1 or 0
end

-- transforms any angle so it is on the 0-2Pi range
local _normalizeAngle = function(angle)
  angle = angle % twoPi
  return (angle < 0 and (angle + twoPi) or angle)
end

function Turret:update(dt)

  local tx, ty = self:getTargetPosition()
  local x, y = self:getPosition()
  local angle = self:getAngle()
  local maxTorque = self:getMaxTorque()
  local inertia = self:getInertia()
  local w = self:getAngularVelocity()

  local targetAngle = math.atan2(ty-y,tx-x)

  -- distance I have to cover
  local differenceAngle = _normalizeAngle(targetAngle - angle)

  -- distance it will take me to stop
  local brakingAngle = _normalizeAngle(_sign(w)*2.0*w*w*inertia/maxTorque)

  local torque = maxTorque

  -- two of these 3 conditions must be true
  local a,b,c = differenceAngle > math.pi, brakingAngle > differenceAngle, w > 0
  if( (a and b) or (a and c) or (b and c) ) then
    torque = -torque
  end

  self:applyTorque(torque)
end

这背后的概念很简单:我需要计算炮塔需要多少“空间”(角度)才能完全停下来。这取决于炮塔移动的速度以及它可以施加多少扭矩。简而言之,这就是我用 brakingAngle 计算的内容。

我的计算角度公式与 Beta 的略有不同。我的一个朋友帮我解决了物理问题,好像它们正在起作用。添加 w 的符号是我的想法。

我不得不实现一个“归一化”函数,将任何角度放回到 0-2Pi 区域。

最初这是一个纠缠的 if-else-if-else。由于条件非常重复,我使用了一些 boolean logic 来简化算法。缺点是,即使它工作正常并且不复杂,但它为什么工作并不明显。

一旦代码稍微更加精简,我会在这里发布演示链接。

非常感谢。

编辑:现在可以在这里找到可工作的LÖVE示例。重要的内容位于actors/AI.lua文件中(.love文件可以使用zip解压缩器打开)


0
这个问题的简化版本非常容易解决。 假设电动机具有无限的扭矩,即它可以瞬间改变速度。这显然不是物理上准确的,但使问题更容易解决,最终并不是问题。
专注于目标角速度而不是目标角度。
current_angle = "the turrets current angle";
target_angle = "the angle the turret should be pointing";
dt = "the timestep used for Box2D, usually 1/60";
max_omega = "the maximum speed a turret can rotate";

theta_delta = target_angle - current_angle;
normalized_delta = normalize theta_delta between -pi and pi;
delta_omega = normalized_deta / dt;
normalized_delta_omega = min( delta_omega, max_omega );

turret.SetAngularVelocity( normalized_delta_omega );

这个方法的原理是炮塔在接近目标角度时会自动尝试减速。

无限扭矩被掩盖在这样一个事实中,即炮塔不会立即尝试缩小距离。相反,它会在一个时间步长内尝试缩小距离。此外,由于-pi到pi的范围非常小,可能疯狂的加速度从未显现。最大角速度使炮塔的旋转看起来更加逼真。

我从未解出使用扭矩而不是角速度求解的真正方程式,但我想它看起来很像PID方程式。


我喜欢你的想法,我会记住它以备将来之需。谢谢你分享。如果你有兴趣,我已经找到了方程并编写了一个可工作的示例程序。 - kikito

0

当施加加速扭矩时,您可以找到转子的角速度与角距离的方程,并找到当施加制动扭矩时相同的方程。

然后修改制动方程以便在所需角度处与角距离轴相交。有了这两个方程,您可以计算它们相交的角距离,这将给出制动点。

可能完全错误,很长时间没有做过这样的事情了。可能有更简单的解决方案。我假设加速度不是线性的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接