【问题标题】:physics game programming box2d - orientating a turret-like object using torques物理游戏编程 box2d - 使用扭矩定位类似炮塔的物体
【发布时间】:2010-04-16 15:31:52
【问题描述】:

这是我在尝试使用 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

...它失败了。让我用两个说明性的情况来解释:

  • 炮塔围绕目标角度“摆动”。
  • 如果目标“正好在炮塔后面,顺时针方向稍微”,炮塔将开始施加顺时针扭矩,并一直施加到超过目标角度的瞬间。那时它将开始在相反方向上施加扭矩。但它会获得很大的角速度,所以它会继续顺时针移动一段时间……直到目标“刚好在后面,但有点逆时针”。它会重新开始。所以炮塔会摆动,甚至转圈。

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

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

如何计算炮塔必须“开始制动”的时间?

【问题讨论】:

    标签: lua physics box2d


    【解决方案1】:

    向后思考。当炮塔有足够的空间从当前角速度减速到死停时,炮塔必须“开始制动”,这与从死停加速到当前角速度所需的空间相同,即

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

    如果您的步长过大,您可能还会遇到围绕目标的小幅度振荡的问题;这将需要更多技巧,您必须更快,更轻柔地刹车。在你看到之前不要担心。

    现在这应该已经足够了,但还有一个问题可能会在以后让你绊倒:决定走哪条路。有时,如果您已经这样做了,那么走很长的路会更快。在这种情况下,你必须决定哪条路花费的时间更少,这并不难,但同样,当你到达它时,越过那座桥。

    编辑:
    我的方程式是错误的,它应该是 Inertia/2*maxTorque,而不是 2*maxTorque/Inertia(这就是我尝试在键盘上做代数的结果)。我已经修好了。

    试试这个:

    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,感谢您的回答。我应该提到我的数学不是很强。我应该用这个方程做什么?有些东西对你来说似乎是隐含的,但我就是看不到。
    • mm 我已经考虑了更多。根据我卑微的尝试,角度 = MaxTorque * t * t / 惯性,对于给定的时间段 t。你如何从那个等式到你在你的例子中展示的那个让我不解。
    • 你几乎已经知道了:加速度 alpha=maxTorque/inertia, w=alphat,但是要计算角度,你必须使用该时间段内的平均速度,所以 angle=(maxTorque t/惯性)*t/2。现在取 w=alphat 并将其平方:ww=alphaalphatt,并用它来摆脱 tt:angle = alphatt/2 = alpha*(ww/alphaalpha)/2 = ww/2*alpha = ww*惯量/2*maxTorque.
    • 我相信公式可能是错误的。然而,它让我走上了正轨。为此+1!您错过了“制动”仅在某些情况下发生的事实,具体取决于 w 是正数还是负数。请参阅我的答案以获得完整的解释。
    【解决方案2】:

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

    对于“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 的情况下它实际上工作正常。看到它工作 here

    
    // 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,我对振荡没有帮助,D 可能会起作用,但它会使过程变慢。
    • 你是对的,我给出的例子并没有直接处理扭矩。然而,maxAngleMomentummaxTorque 成正比,当人们考虑炮塔旋转“机制”中的摩擦时——可以认为在使用任意单位时它们可以互换。
    • 实现看起来不错。 “保持角动量”的想法很有趣。然而,这不是我所要求的——我想施加扭矩,而最后你自己设置角度。但是 +1 用于制作演示和代码优雅。
    【解决方案3】:

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

    这是基于 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 示例现在可用here。重要的东西在actors/AI.lua里面(.love文件可以用zip解压器打开)

    【讨论】:

      【解决方案4】:

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

      然后修改断裂方程,使其以所需角度与角距离轴相交。通过这两个方程,您可以计算出它们相交的角距离,这将为您提供断点。

      虽然可能完全错了,但很长一段时间都没有这样做过。可能是一个更简单的解决方案。我假设加速度不是线性的。

      【讨论】:

        【解决方案5】:

        这个问题的简化版本很容易解决。假设电机具有无限转矩,即它可以瞬间改变速度。这显然在物理上不准确,但使问题更容易解决,最终不是问题。

        关注目标角速度而不是目标角度。

        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 方程。

        【讨论】:

        • 我喜欢你的想法,我会记住它以备将来使用。谢谢分享。如果您有兴趣,我设法找到了方程式并编写了一个工作示例。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多