物理游戏编程Box2D - 使用扭矩定向炮塔般的对象。

这是我在使用 LÖVE 引擎实现一个游戏时遇到的问题,它使用了使用 Lua 脚本给 box2d 提供了覆盖。

目标很简单:一个类似炮塔的物体(在2D环境中从顶部看)需要将自己定位,使其指向目标。

炮塔位于 x、y 坐标上,目标位于 tx、ty 上。我们可以认为 x、y 是固定的,但 tx、ty 往往会在一个瞬间变化(例如,它们可能是鼠标指针)。

炮塔有一个可以在任何时刻施加旋转力矩(扭矩)的转子,可顺时针或逆时针旋转。该力矩的大小有一个上限,称为 maxTorque。

炮塔还具有 certain rotational inertia,对于角度移动与质量对线性移动的作用是相同的。没有任何形式的摩擦力,因此如果它有角速度,它将一直旋转。

炮塔有一个小的 AI 函数,重新评估其方向以验证它是否指向正确的方向,并激活旋转器。这每 dt(每秒约60次)发生一次。现在它看起来是这样的:

  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

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

  • 炮塔环绕目标角度“振荡”。
  • 如果目标“就在炮塔的正右方,稍微顺时针”,则炮塔将开始施加顺时针扭矩,并继续施加直到它超过目标角度的瞬间。此时,它将开始朝相反方向施加扭矩。但是它已经获得了显着的角速度,所以它将持续顺时针一段时间……直到目标“就在后面,但是稍微逆时针”。并且它会重新开始。因此,炮塔会振荡甚至绕圆圈。

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

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

如何计算炮塔何时“开始刹车”?

原文链接 https://stackoverflow.com/questions/2617476

点赞
stackoverflow用户220299
stackoverflow用户220299

你可以找到当施加加速扭矩时,转子的角速度与角位移之间的方程,然后找到当施加制动扭矩时同样的方程。

然后修改制动方程使其在所需角度处与角位移轴相交。有了这两个方程,你可以计算它们相交的角位移,这将为你提供制动点。

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

2010-04-11 15:34:58
stackoverflow用户199
stackoverflow用户199

这似乎是可以用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](http://processing.org/)编写的应用程序,它使用PID。它实际上可以在没有I或D的情况下正常工作。在这里看它运行[here](http://www.phy.bris.ac.uk/people/arnold_bj/sketch_apr12a/applet/)


//演示了使用PID算法模拟炮塔找到目标的过程。 鼠标指针是目标

float dt = 1e-2;
float turretAngle = 0.0;
float turretMass = 1;
//调整它们以获得不同的炮塔行为
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;

  //找到目标
  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;

  //这是“最大扭矩”等效值
  angMomentum = constrain(angMomentum, -maxAngMomentum, maxAngMomentum);

  // Ang. Momentum = mass * ang.速度
  // ang.速度 = ang.动量/ 质量
  angVel = angMomentum / turretMass;

  turretAngle = turretAngle + angVel * dt;

  //绘制“炮塔”
  rotate(turretAngle);
  triangle(-20, 10, -20, -10, 20, 0);

  prevDiffAngle = diffAngle;
}
2010-04-11 15:59:46
stackoverflow用户128940
stackoverflow用户128940

倒着思考。炮台必须在刚好有足够的空间来减速到静止时开始“刹车”,这与从静止加速到当前角速度所需的空间相同,即

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

如果您的步长时间太大,您可能还会遇到围绕目标进行小振荡的问题;这需要更多的技巧,您必须提前一点点轻轻地刹车。直到您看到问题时才担心它。

现在应该足够了,但还有一个问题可能会在以后让您失误:决定应该去哪。有时,绕远路会更快,如果您已经走了那条路。在这种情况下,您必须决定哪种方式需要更少的时间,这并不困难,但再次,问题到了再解决。

编辑:

我的方程式有误,应该是惯量/2 * maxTorque,而不是2 * maxTorque/惯量(这就是我在键盘上尝试代数的结果)。我已经修正了它。

尝试这个:

local torque = maxTorque;
if(differenceAngle > math.pi) then -- 顺时针是最短的路径
    torque = -torque;
end
if(differenceAngle < w*w*Inertia/(2*MaxTorque)) then -- 刹车
    torque = -torque;
end
self:applyTorque(torque)
2010-04-11 16:30:41
stackoverflow用户28817
stackoverflow用户28817

这个问题的简化版很容易解决。假设电机有无限的扭矩,即可以瞬间改变速度。这显然不符合物理规律,但可以使问题简单解决,最终也不是问题。

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

current_angle = “炮塔当前角度”;
target_angle = “炮塔应该指向的角度”;
dt = “Box2D使用的时间步长,通常为1/60”;
max_omega = “炮塔可以旋转的最大速度”;

theta_delta = target_angle - current_angle;
normalized_delta = 将theta_delta正规化为-π到π之间;
delta_omega = normalized_deta / dt;
normalized_delta_omega = min(delta_omega,max_omega);

turret.SetAngularVelocity( normalized_delta_omega );

这个方法有效的原因是当炮塔接近目标角度时自动尝试减缓移动速度。

无限扭矩被掩盖在炮塔不尝试瞬间关闭距离的事实中。而是尝试在一次时间步长内完成距离的缩短。此外,因为-π到π范围相当小,所以可能的疯狂加速度从未显现出来。最大角速度可以保持炮塔旋转时看起来逼真。

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

2010-04-12 14:57:40
stackoverflow用户312586
stackoverflow用户312586

我相信我有解决方案。

这是基于Beta的想法,但需要进行一些必要的调整。这就是它的工作方式:

local twoPi=2.0* math.pi --小优化

- 根据x>0,x<0或x= 0,返回-110的函数
function _sign(x)
  返回x>01或x<0-10
结束

- 将任何角度转换为0-2Pi范围
local _normalizeAngle=function(angle)
  Angle=角度% twoPi
  返回(角度< 0 and(角度+ twoPi)或角度)
结束

function Turret:update(dt)

  TX,TY=self:getTargetPosition()
  X,Y=self:getPosition()
  Angle=self:getAngle()
  MaxTorque=self:getMaxTorque()
  Inertia=self:getInertia()
  W=self:getAngularVelocity()

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

  - 我必须覆盖的距离
  DifferenceAngle=_normalizeAngle(targetAngle-angle)

  - 停止所需的距离
  BrakingAngle=_normalizeAngle(_sign(w)* 2.0*w*w* inertia/MaxTorque)

  扭矩=MaxTorque

  - 这3个条件中有两个必须为真
  A、B、C=differenceAngle > math.pi,brakingAngle > differenceAngle,w > 0
  如果( (a and b)或(a和c)或(b和c) )那么torque=-扭矩
  结束

  self:applyTorque(torque)
结束

这背后的概念很简单:我需要计算塔需要多少“空间”(角度)才能完全停止。这取决于炮塔移动的速度和它能施加的扭矩。简而言之,这就是我用“BrakingAngle”计算的东西。

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

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

最初这是一个纠缠不清的if-else-if-else。由于条件非常重复,我使用了一些布尔逻辑来简化算法。缺点是,即使它可以正常工作且不复杂,但它并不是为什么工作的理由。

一旦代码变得更清晰,我会在这里发布一个演示的链接。

非常感谢。

编辑:现在可以在此处获取工作的LÃ ‌ VE示例。重要的材料在演员/人工智能。Lua( .love文件可以使用zip解压缩器打开)

2010-04-14 00:19:53