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

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

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

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

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

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

4

5 回答 5

3

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

|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)
于 2010-04-11T16:30:41.920 回答
1

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

对于“P”分量,您应用的扭矩与转塔角度和目标角度之间的差值成正比,即

P = P0 * differenceAngle

如果这仍然振荡太多(它会有点)然后添加一个“I”组件,

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

如果这个过冲太多,那么添加一个“D”项

derivAngle = (prevDifferenceAngle - differenceAngle) / dt
prevDifferenceAngle = differenceAngle
D = D0 * derivAngle

P0I0并且D0是常数,您可以调整以获得您想要的行为(即炮塔响应速度等)

只是作为提示,通常P0> I0>D0

使用这些术语来确定施加了多少扭矩,即

magnitudeAngMomentum = P + I + D

编辑:

这是一个使用使用 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;
}
于 2010-04-11T15:59:46.330 回答
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。由于条件非常重复,我使用了一些布尔逻辑来简化算法。不利的一面是,即使它工作正常并且并不复杂,它也不会揭示它为什么工作。

一旦代码更加纯净,我将在此处发布演示链接。

非常感谢。

编辑:工作 LÖVE 样本现在可在此处获得。重要的东西在actors/AI.lua里面(.love文件可以用zip解压器打开)

于 2010-04-14T00:19:53.670 回答
0

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

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

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

于 2010-04-11T15:34:58.660 回答
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 方程。

于 2010-04-12T14:57:40.920 回答