这是我在尝试使用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
......它失败了。让我用两个说明性的情况来解释:
我认为我的炮塔应该在达到目标角度之前开始在“最短路径的相反方向”上施加扭矩(就像停车前的汽车制动一样)。
直观地说,我认为炮塔应该“在最短路径的相反方向上开始施加扭矩,当它大约是目标物体的一半”时。我的直觉告诉我它与角速度有关。然后事实是目标是移动的 - 我不知道我是否应该以某种方式考虑到这一点或者忽略它。
如何计算炮塔何时必须“开始制动”?
答案 0 :(得分:3)
向后看。当炮塔有足够的空间从当前的角速度减速到死点时,它必须“开始制动”,这与从死点加速到当前角速度所需的空间相同,这是< / p>
|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)
答案 1 :(得分:1)
这似乎是一个可以用PID controller解决的问题。我在工作中使用它们来控制加热器输出以设定温度。
对于'P'组件,应用与转塔角度和目标角度之差成比例的扭矩,即
P = P0 * differenceAngle
如果这仍然振荡太多(会有点),那么添加一个'I'组件,
integAngle = integAngle + differenceAngle * dt
I = I0 * integAngle
如果这过多,则添加'D'一词
derivAngle = (prevDifferenceAngle - differenceAngle) / dt
prevDifferenceAngle = differenceAngle
D = D0 * derivAngle
P0
,I0
和D0
是您可以调整以获得所需行为的常数(即炮塔响应的速度等)。
就像提示一样,通常是P0
&gt; I0
&gt; D0
使用这些术语来确定应用了多少扭矩,即
magnitudeAngMomentum = P + I + D
修改强>
这是使用Processing编写的使用PID的应用程序。没有我或者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;
}
答案 2 :(得分: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工作样本here。重要的东西是在actors / AI.lua中(.love文件可以用zip压缩器打开)
答案 3 :(得分:0)
当施加加速扭矩时,您可以找到转子的角速度与角距离的等式,并找到制动扭矩施加时的相同等式。
然后修改破坏方程,使其将角距离轴与所需角度相交。使用这两个方程式,您可以计算出它们相交的角度距离,这将为您提供断点。
虽然可能是完全错误的,但很长一段时间没有这样做。可能是一个更简单的解决方案。我假设加速度不是线性的。
答案 4 :(得分: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方程。