Physics Game Programming Box2D - توجيه كائن يشبه البرج باستخدام Torques

StackOverflow https://stackoverflow.com/questions/2617476

  •  26-09-2019
  •  | 
  •  

سؤال

هذه مشكلة ضربتها عند محاولة تنفيذ لعبة باستخدام حب المحرك الذي يغطي Box2d مع لوا البرمجة النصية.

الهدف بسيط: يحتاج كائن يشبه البرج (الذي يظهر من الأعلى ، في بيئة ثنائية الأبعاد) إلى توجيه نفسه بحيث يشير إلى هدف.

البرج على إحداثيات X ، Y ، والهدف على TX ، TY. يمكننا التفكير في أن x ، y ثابت ، لكن tx ، تميل إلى تختلف من لحظة إلى أخرى (أي أنها ستكون مؤشر الماوس).

يحتوي البرج على دوار يمكنه تطبيق قوة الدوران (عزم الدوران) في أي لحظة معينة ، في اتجاه عقارب الساعة أو عكس اتجاه عقارب الساعة. حجم تلك القوة لها حد أعلى يسمى Maxtorque.

يحتوي البرج أيضًا على بعض الجمود الدوراني ، والذي يعمل على الحركة الزاوية بنفس الطريقة التي تعمل بها الكتلة للحركة الخطية. لا يوجد احتكاك من أي نوع ، وبالتالي فإن البرج سيستمر في الغزل إذا كان له سرعة زاوية.

يحتوي البرج على وظيفة منظمة العفو الدولية الصغيرة التي تعيد تقييم اتجاهها للتحقق من أنه يشير إلى الاتجاه الصحيح ، ويقوم بتنشيط الدوار. يحدث هذا كل 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

... يفشل. اسمحوا لي أن أشرح مع حالتين توضيحيين:

  • برج "يتذبذب" حول الهدف.
  • إذا كان الهدف "خلف البرج مباشرة ، فقط على مدار الساعة" ، سيبدأ البرج في تطبيق عزم الدوران في اتجاه عقارب الساعة ، ويواصل تطبيقها حتى تتجاوز الزاوية المستهدفة. في تلك اللحظة ، سيبدأ تطبيق عزم الدوران على الاتجاه المعاكس. لكنه سيكون قد اكتسب سرعة زاوية كبيرة ، لذلك سوف تستمر في اتجاه عقارب الساعة لبعض الوقت ... حتى يكون الهدف "خلفًا ، ولكن عكس اتجاه عقارب الساعة". وسيبدأ من جديد. لذلك سوف يتأرجح البرج أو حتى في دوائر مستديرة.

أعتقد أن البرج يجب أن يبدأ في تطبيق عزم الدوران في "الاتجاه المعاكس لأقصر مسار" قبل أن يصل إلى الزاوية المستهدفة (مثل فرملة السيارة قبل التوقف).

بشكل حدسي ، أعتقد أنه يجب على البرج "البدء في تطبيق عزم الدوران على الاتجاه المعاكس لأقصر مسار عندما يكون في منتصف الطريق إلى الهدف المستهدف". يخبرني حدسي أن الأمر يتعلق بالسرعة الزاوية. ثم هناك حقيقة أن الهدف متنقل - لا أعرف ما إذا كان ينبغي علي أخذ ذلك في الاعتبار بطريقة أو بأخرى أو مجرد تجاهله.

كيف يمكنني حساب عندما يجب أن "يبدأ البرج"؟

هل كانت مفيدة؟

المحلول 3

حسنًا ، أعتقد أنني حصلت على الحل.

يعتمد هذا على فكرة بيتا ، ولكن مع بعض التعديلات اللازمة. من هنا تبدأ:

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.

تختلف صيغتي لحساب هذه الزاوية قليلاً عن بيتا. لقد ساعدني صديق لي في الخروج من الفيزياء ، وبصحة جيدة ، يبدو أنه يعمل. كانت إضافة علامة W كانت فكرتي.

اضطررت إلى تنفيذ وظيفة "تطبيع" ، والتي تعيد أي زاوية إلى منطقة 0-2PI.

في البداية ، كان هذا متشابكًا إذا كان إيلي إيليس. منذ الظروف التي تكررها للغاية ، استخدمت البعض المنطق البوليني من أجل تبسيط الخوارزمية. الجانب السلبي هو أنه ، حتى لو كان يعمل بشكل جيد ولم يكن معقدًا ، فإنه لا ينقل سبب عمله.

بمجرد أن يتم ترقيب الكود قليلاً ، سأقوم بنشر رابط إلى عرض تجريبي هنا.

شكرًا جزيلاً.

تحرير: عينة العمل Löve متوفرة الآن هنا. الأشياء المهمة هي داخل الجهات الفاعلة/ai.lua (يمكن فتح ملف .love باستخدام مضغوط مضغوط)

نصائح أخرى

فكر للخلف. يجب على البرج "البدء في الكبح" عندما يكون لديه مساحة كافية للتباين من سرعته الزاوية الحالية إلى توقف مسدود ، وهو نفس الغرفة التي ستحتاج إلى تسريعها من توقف مسدود إلى سرعتها الزاوية الحالية ، وهي

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

قد تواجه أيضًا بعض المشاكل مع التذبذبات الصغيرة حول الهدف إذا كان وقت خطوةك كبيرًا جدًا ؛ سيتطلب ذلك المزيد من البراعة ، عليك أن تضطر إلى الفرامل في وقت أقرب قليلاً ، بلطف. لا تقلق بشأن ذلك حتى تراه.

يجب أن يكون ذلك جيدًا بما يكفي في الوقت الحالي ، ولكن هناك صيد آخر قد يتجه إليك لاحقًا: تحديد أي طريقة للذهاب. في بعض الأحيان يكون الطريق الطويل أسرع ، إذا كنت تسير على هذا النحو بالفعل. في هذه الحالة ، عليك أن تقرر الطريقة التي تستغرق وقتًا أقل ، وهذا ليس صعبًا ، ولكن مرة أخرى ، عبور هذا الجسر عندما تأتي إليه.

تعديل:
كانت معادلاتي خاطئة ، يجب أن تكون الجمود/2*Maxtorque ، وليس 2*Maxtorque/القصور الذاتي (هذا ما أحاول القيام به لجبر في لوحة المفاتيح). لقد أصلحته.

جرب هذا:

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)

هذا يبدو وكأنه مشكلة يمكن حلها مع أ وحدة تحكم PID. أستخدمها في عملي للتحكم في إخراج المدفأة لتعيين درجة حرارة.

بالنسبة للمكون "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 > I0 > D0

استخدم هذه المصطلحات لتحديد مقدار عزم الدوران ، أي

magnitudeAngMomentum = P + I + D

تعديل:

هنا تطبيق مكتوب باستخدام يعالج الذي يستخدم PID. إنه يعمل بشكل جيد في الواقع دون أن أراه يعمل هنا


// 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;
}

يمكنك العثور على معادلة للسرعة الزاوية مقابل المسافة الزاوية للدوار عند تطبيق عزم الدوران المتسارع ، وتجد نفس المعادلة عند تطبيق عزم الدوران.

ثم قم بتعديل معادلة كسر بحيث تتفوق على محور المسافة الزاوي بالزاوية المطلوبة. مع هاتين المعادلتين ، يمكنك حساب المسافة الزاوية التي تتقاطع فيها والتي من شأنها أن تمنحك نقطة الانهيار.

يمكن أن يكون مخطئا تماما ، لم تفعل أي مثل هذا لفترة طويلة. ربما حل أبسط. أفترض أن التسارع ليس خطيًا.

نسخة مبسطة من هذه المشكلة بسيطة للغاية. افترض أن المحرك له عزم دوران لا حصر له ، أي أنه يمكن أن يغير السرعة على الفور. من الواضح أن هذا ليس دقيقًا جسديًا ولكنه يجعل المشكلة أكثر بساطة لحلها وفي النهاية ليست مشكلة.

ركز على السرعة الزاوية المستهدفة وليس زاوية مستهدفة.

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 );

والسبب في أن هذا يعمل هو أن البرج يحاول تلقائيًا تحريك أبطأ مع وصوله إلى زاوية المستهدف.

يتم ملء عزم الدوران اللانهائي بحقيقة أن البرج لا يحاول إغلاق المسافة على الفور. بدلاً من ذلك ، يحاول إغلاق المسافة في Timestep واحدة. أيضًا نظرًا لأن نطاق -PI إلى PI صغير جدًا ، فإن التسارع المجنون ربما لا تظهر أبدًا. الحد الأقصى للسرعة الزاوية تبقي دورات البرج تبدو واقعية.

لم أحمل مطلقًا المعادلة الحقيقية لحل عزم الدوران بدلاً من السرعة الزاوية ، لكنني أتخيل أنها ستبدو مثل معادلات PID.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top