Pull to refresh

Box2D — Физика движения авто своими руками

Reading time14 min
Views28K
Приветствую всех читателей хабра. В этом топике я постараюсь показать вам как просто можно создать простую физику движения передне— и полноприводного автомобиля. Итак, поехали!

Вступление


Скажу сразу, что я еще не достаточно знаком со всеми понятиями физики, но понимаю что они из себя представляют, поэтому умных слов не ждите, все буду стараться описывать наиболее понятным и человеческим языком, извиняйте заранее.
Толчком для написания этой статьи является отсутствие рабочего аналога. На тот момент когда мне нужно было сделать подобное, я нагуглил лишь один более—менее нормальный пример, который, к сожалению, был мне не интересен по некоторым причинам:
  1. Полное отсутствие дрифта/скольжения
  2. Различная скорость при движении по осям X и Y (неизвестно, чем это вызвано, но мне такое не надо)


Подготовка


Итак, что мы знаем. А знаем мы то, что при движении на авто (а в нашем случае — на колёса) действуют некоторые силы. Для нашего простого проекта естесвенно, некоторыми из этих сил можно пренебречь (мы же не полноценный симулятор делаем).
Все параметры, которые мы будем использовать:
  1. Сила тяги (оно же у нас будет являться скоростью и ускорением)
  2. Продольное трение колесa
  3. Поперечное трение колеса

Все эти силы мы будем прикладывать непосредственно к объектам колёс.
При движении вперёд графически эти силы можно изобразить так (за переднюю часть авто на картинках принимать верх):
image
По оси X — поперечное трение колеса
По оси Y вниз — продольное трение колеса
По оси Y вверх — скорость и ускорение
Все вышеописанные оси всегда относительны центра каждого колеса и его угла поворота.

Пишем код


В качестве физического движка я выбрал Box2D. Для облегчения написания кода я все—таки решил использовать обертку QuickBox2D, потому как она существенно облегчает создание примитивов в Box2D.

Подключаем необходимые классы:
  1. import Box2D.Collision.Shapes.*;
  2. import Box2D.Common.Math.*;
  3. import Box2D.Dynamics.*;
  4. import Box2D.Dynamics.Joints.*;
  5. import Box2D.Collision.*;
  6. import Box2D.Common.*;
  7. import com.actionsnippet.qbox.*;

… и создаем наш мир:
  1. var world:QuickBox2D = new QuickBox2D(this,{debug:false, frim:true});
  2. world.gravity = new b2Vec2(0,0)//вектор гравитации
  3. world.start()//старт отрисовки и просчета физики
  4. world.createStageWalls()//фича от QuickBox'a - создает 4 стены по размерам stage
  5. world.mouseDrag()//и разрешаем перемещать все тела мышью


Прописываем необходимые переменные или константы:
  1. //константы и переменные:
  2. private const FWD=1//передний 
  3. private const AWD=2//полный
  4. private const RWD=3//задний
  5. private var _privod:Number//текущий привод авто, выбирается при создании
  6. private var _maxTurnAngle:Number// максимальный угол поворота колес
  7. private var _width:Number//длина авто
  8. private var _height:Number//ширина
  9. private var _axisOffset:Number//отступ колес по длине, относительно центра авто
  10. private const _angularDamping = 7//замедление кручения колеса
  11. private const _angularDampingReverse = 3//то же, но при заднем ходе
  12. private const _linearDamping = 0.25//замедление скольжения колеса, что-то вроде трения
  13. private const _linearDampingReverse = 0.5// и для заднего хода
  14.  
  15. public var _carBody:QuickObject;//объект автомобиля
  16.  
  17. private var _frontAxisBodyLeft:QuickObject;//и его колес
  18. private var _frontAxisBodyRight:QuickObject;
  19. private var _rearAxisBodyLeft:QuickObject;
  20. private var _rearAxisBodyRight:QuickObject;
  21.  
  22. private var _frontLeftJoint:QuickObject;//шарнирные соединения для передних колёс
  23. private var _frontRightJoint:QuickObject;
  24. private var _rearLeftJoint:QuickObject;//шарнирные соединения для задних колёс
  25. private var _rearRightJoint:QuickObject;
  26. protected var axisHeight:Number = 4;//ширина колеса
  27. protected var axisWidth:Number = 7;//длина
  28.  
  29. public var drive:int = 0;//переменные для управлления автомобилем
  30.         public var steer:int = 0;
  31. //drive = 1 - едем вперед
  32. //drive = 0 - отпускаем газ и катимся по инерции, либо просто стоим
  33. //drive = -1 - едем назад
  34. //
  35. //steer = -1 - рулим влево
  36. //steer = 0 - никуда не рулим
  37. //steer = 1 - рулим вправо
  38.  


Создаем авто с колёсами:
  1. //создаем авто
  2. _width = 76
  3. _height = 40
  4. _maxTurnAngle = 15 * Globals.DEG_TO_RAD//максимальный угол поворота передних колес
  5. _axisOffset:Number = (_width/4 - axisWidth)/ Globals.RATIO //отступ колес по длине
  6. _privod = FWD; // ну и наш привод, пока что функционируют передний и полный
  7.  
  8. _carBody = world.addBox({
  9.  width :this._width / 2 / Globals.RATIO,
  10.  height :this._height / 2 / Globals.RATIO,
  11.  x :(this.x + this._width / 2) / Globals.RATIO,
  12.  y :(this.y + this._height/ 2) /Globals.RATIO,
  13.  density: 0.48,
  14.  driction:0.3,
  15.  restitution:0.4,
  16.  linearDamping:this._linearDamping,
  17.  angularDamping:this._angularDamping,
  18.  groupIndex:-1,
  19.  isSleeping:true
  20. skin:null,
  21.  scaleSkin:true});
  22.  
  23. this._frontAxisBodyLeft = this.world.addBox({
  24. width :this.axisWidth / Globals.RATIO,
  25. height :(this.axisHeight) / Globals.RATIO,
  26. x :_carBody.body.GetWorldCenter().x + this._axisOffset,
  27. y :_carBody.body.GetWorldCenter().y - this._height/4/Globals.RATIO+1/Globals.RATIO,// + (this.axisHeight/2) / Globals.RATIO,
  28. density:0.48,
  29. friction:0.3,
  30. restitution:0.5,
  31. linearDamping:this._linearDamping,
  32. angularDamping:this._angularDamping,
  33. groupIndex:-1});
  34.  
  35. this._frontAxisBodyRight = this.world.addBox({
  36. width :this.axisWidth / Globals.RATIO,
  37. height :(this.axisHeight) / Globals.RATIO,
  38. x :_carBody.body.GetWorldCenter().x + this._axisOffset,
  39. y :_carBody.body.GetWorldCenter().y + this._height/4/Globals.RATIO-1/Globals.RATIO,// - (this.axisHeight) / Globals.RATIO,
  40. density:0.48,
  41. friction:0.3,
  42. restitution:0.5,
  43. linearDamping:this._linearDamping,
  44. angularDamping:this._angularDamping,
  45. groupIndex:-1});
  46.  
  47. this._frontLeftJoint = this.world.addJoint({
  48. type:QuickBox2D.REVOLUTE,
  49.     a:this._carBody.body,
  50. b:this._frontAxisBodyLeft.body,
  51. x1:this._frontAxisBodyLeft.body.GetWorldCenter().x,
  52. y1:this._frontAxisBodyLeft.body.GetWorldCenter().y,
  53. x2:this._frontAxisBodyLeft.body.GetWorldCenter().x,
  54. y2:this._frontAxisBodyLeft.body.GetWorldCenter().y,
  55. enableLimit:true,
  56. enableMotor:true,
  57. collideConnected:true,
  58. lowerAngle:-this._maxTurnAngle,
  59. upperAngle:this._maxTurnAngle
  60. });
  61.  
  62. this._frontRightJoint = this.world.addJoint({
  63. type:QuickBox2D.REVOLUTE,
  64.     a:this._carBody.body,
  65. b:this._frontAxisBodyRight.body,
  66. x1:this._frontAxisBodyRight.body.GetWorldCenter().x,
  67. y1:this._frontAxisBodyRight.body.GetWorldCenter().y,
  68. x2:this._frontAxisBodyRight.body.GetWorldCenter().x,
  69. y2:this._frontAxisBodyRight.body.GetWorldCenter().y,
  70. enableLimit:true,
  71. enableMotor:true,
  72. collideConnected:true,
  73. lowerAngle:-this._maxTurnAngle,
  74. upperAngle:this._maxTurnAngle
  75. });
  76.  
  77. this._rearAxisBodyLeft = this.world.addBox({
  78. width :this.axisWidth / Globals.RATIO,
  79. height :(this.axisHeight) / Globals.RATIO,
  80. x :_carBody.x - this._axisOffset,//-this.frontPivotOffset,
  81. y :_carBody.y - this._height/4/Globals.RATIO+1/Globals.RATIO,// + (this.axisHeight/2) / Globals.RATIO,
  82. density:0.48,
  83. friction:0.3,
  84. restitution:0.5,
  85. linearDamping:this._linearDamping,
  86. angularDamping:this._angularDamping,
  87. groupIndex:-1,
  88. isSleeping:true});
  89.  
  90. this._rearAxisBodyRight = this.world.addBox({
  91. width :this.axisWidth / Globals.RATIO,
  92. height :(this.axisHeight) / Globals.RATIO,
  93. x :_carBody.x - this._axisOffset,//-this.frontPivotOffset,
  94. y :_carBody.y + this._height/4/Globals.RATIO-1/Globals.RATIO,// - (this.axisHeight) / Globals.RATIO,
  95. density:0.48,
  96. friction:0.3,
  97. restitution:0.5,
  98. linearDamping:this._linearDamping,
  99. angularDamping:this._angularDamping,
  100. groupIndex:-1,
  101. isSleeping:true});
  102.  
  103.  
  104. this._rearLeftJoint = this.world.addJoint({
  105. type:QuickBox2D.REVOLUTE,
  106.     a:this._carBody.body,
  107. b:this._rearAxisBodyLeft.body,
  108. x1:this._rearAxisBodyLeft.x,
  109. y1:this._rearAxisBodyLeft.y,
  110. x2:this._rearAxisBodyLeft.x,
  111. y2:this._rearAxisBodyLeft.y,
  112. enableLimit:true,
  113. enableMotor:false,
  114. collideConnected:false,
  115. lowerAngle:0,
  116. upperAngle:0
  117. });
  118.  
  119. this._rearRightJoint = this.world.addJoint({
  120. type:QuickBox2D.REVOLUTE,
  121.     a:this._carBody.body,
  122. b:this._rearAxisBodyRight.body,
  123. x1:this._rearAxisBodyRight.x,
  124. y1:this._rearAxisBodyRight.y,
  125. x2:this._rearAxisBodyRight.x,
  126. y2:this._rearAxisBodyRight.y,
  127. enableLimit:true,
  128. enableMotor:false,
  129. lowerAngle:0,
  130. upperAngle:0
  131. });


Ну вот, мы создали наше авто. Вид его примерно как на картинке выше, но только оно будет повернуто на 90% по часовой.
Теперь перед нами стоит главная задача: научить наше авто ездить. Для достижения эффекта езды мы будем прикладывать ApplyForce(..) к нужным колесам (для переднего привода — передние колеса. для полного — все колеса)
Для просчета физики авто и управления им используем accelerate() и вызываем её при каждом обновлении мира:
  1. stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownList);
  2. stage.addEventListener(KeyboardEvent.KEY_UP, keyUpList);
  3. world.addEventListener(QuickBox2D.STEP,onStep);
  4. ...
  5. var bup:Boolean;
  6. var bdown:Boolean;
  7. var bleft:Boolean;
  8. var bright:Boolean;
  9.  
  10. function keyDownList(e:KeyboardEvent):void
  11. {
  12.   if (e.keyCode == Keyboard.UP){
  13.    bup = true;
  14.   }
  15.   if (e.keyCode == Keyboard.LEFT){
  16.    bleft = true;
  17.   }
  18.   if (e.keyCode == Keyboard.DOWN){
  19.    bdown = true;
  20.   }
  21.   if (e.keyCode == Keyboard.RIGHT){
  22.    bright = true;
  23.   }
  24. };
  25. function keyUpList(e:KeyboardEvent):void
  26. {
  27.   if (e.keyCode == Keyboard.UP){
  28.    bup = false;
  29.   };
  30.   if (e.keyCode == Keyboard.LEFT){
  31.    bleft = false;
  32.   };
  33.   if (e.keyCode == Keyboard.DOWN){
  34.    bdown = false;
  35.   };
  36.   if (e.keyCode == Keyboard.RIGHT){
  37.    bright = false;
  38.   };
  39. };
  40.  
  41. function onStep(evt:Event):void
  42. {
  43. car.accelerate();
  44. if (bup) {car.drive =1};
  45. if (bdown) {car.drive=-1};
  46. if (!bup && !bdown) (car.drive=0);
  47. if (bleft) {car.steer=-1};
  48. if (bright) {car.steer=1};
  49. if (!bleft && !bright) {car.steer=0};
  50. };


Ну и собственно сама процедура accelerate() (разбираем построчно, полный код процедуры будет позже):
1) Для начала мы разбудим наши тела
  1. this._carBody.body.WakeUp();
  2. this._frontAxisBodyLeft.body.WakeUp();
  3. this._frontAxisBodyRight.body.WakeUp();


2) Затем добавим необходимые силы трения(общую, по X, и по Y) чтобы авто ехало вперед, а не скользило при резком повороте, и при движении по инерции — замедлялось:
  1. if (this.drive > 0){ //едем вперёд
  2.  this._acceleration = this._accelerationForward;
  3.  this.setLinearDamping(this._linearDamping);
  4.  this.setAngularDamping(this._angularDamping);
  5. }
  6. else { //едем назад или стоим
  7.  this._acceleration = this._accelerationBackwards;
  8.  this.setLinearDamping(this._linearDamping);
  9.  this.setAngularDamping(this._angularDamping);
  10. }
  11. this.addFriction(this._rearAxisBodyLeft.body,0);
  12. this.addFriction(this._rearAxisBodyRight.body,0);
  13. this.addFriction(this._frontAxisBodyLeft.body,1);
  14. this.addFriction(this._frontAxisBodyRight.body,1);


3) setLinearDamping() и setAngularDamping()
  1. private function setLinearDamping(linDamp:Number) : void
  2. {
  3.  this._frontAxisBodyLeft.body.m_linearDamping = linDamp;
  4.  this._frontAxisBodyRight.body.m_linearDamping = linDamp;
  5.  this._rearAxisBodyLeft.body.m_linearDamping = linDamp;
  6.  this._rearAxisBodyRight.body.m_linearDamping = linDamp;
  7.  this._carBody.body.m_linearDamping = linDamp;
  8.  return;
  9. }
  10.  
  11. private function setAngularDamping(angDamp:Number) : void
  12. {
  13.  this._frontAxisBodyLeft.body.m_angularDamping = angDamp;
  14.  this._frontAxisBodyRight.body.m_angularDamping = angDamp;
  15.  this._rearAxisBodyLeft.body.m_angularDamping = angDamp;
  16.  this._rearAxisBodyRight.body.m_angularDamping = angDamp;
  17.  this._carBody.body.m_angularDamping = angDamp;
  18.  return;
  19. }


4) Ну и самая интересная на мой взгляд процедура addFriction(), реализацию которой я так долго искал и не нашел, и вот реализовал сам:
  1. function addFriction(targetBody:b2Body, isFront:Number)
  2. {
  3.   var spd,spd2:b2Vec2;
  4.   spd = targetBody.GetLinearVelocity();
  5.   spd2 = targetBody.GetLocalVector(spd);
  6.  
  7.   if (this._privod == FWD){//передний привод
  8.    if (isFront == 1){
  9.     spd2.y = spd2.y * 0.5;
  10.    }
  11.    else {
  12.     spd2.y = spd2.y>2.5? spd2.y * 0.45 : 0;
  13.     }
  14.    spd = targetBody.GetWorldVector(spd2);
  15.   targetBody.SetLinearVelocity(spd);
  16.   }
  17.  
  18.   if (this._privod == RWD){
  19.    if (isFront == 1){
  20.     spd2.y = 0;
  21.     spd2.x = spd2.x * .9;
  22.     spd = targetBody.GetWorldVector(spd2);
  23.     targetBody.SetLinearVelocity(spd);
  24.    }
  25.    else{
  26.     spd2.y = spd2.y>2.5? spd2.y * 0.25 : 0;
  27.     spd = targetBody.GetWorldVector(spd2);
  28.     targetBody.SetLinearVelocity(spd);
  29.    }
  30.   }
  31. }

Данная процедура раскладывает глобальный вектор скорости spd у каждого колеса на локальный spd2 и прикладывает трения по X и по Y. Единственные момент, который не укладывается в моей голове так это то, почему же все-таки оси повернуты на 90% по часовой? То есть чтобы отучить авто скользить и добавить трения по оси X которую я зарисовывал выше сейчас нам приходится добавлять трение на ось Y (оно же — поперечное трение колеса).

5) Продолжаем дополнять accelerate(), просчитываем углы по которым потом будем прикладывать силы скорости и ускорения (для передних колес):
  1. var carAngle:Number = this._carBody.body.GetAngle();
  2. if (this.steer !0){ // при езде вперед
  3.  this._steerAngle = this._steerAngle + this.steer * this._steeringAcceleration;
  4. }
  5.  
  6. if (this._steerAngle > carAngle + this._steeringRange){
  7.  this._steerAngle = carAngle + this._steeringRange;
  8. }
  9. else if (this._steerAngle < carAngle - this._steeringRange){
  10.  this._steerAngle = carAngle - this._steeringRange;
  11. }
  12.  
  13. if (this.drive < 0){//при езде назад
  14.  if (this.steer == 0){
  15.   this._steerAngle = carAngle;
  16.  }
  17.  else if (this.steer == -1){
  18.    this._steerAngle = carAngle - this._steeringRange;
  19.  }
  20.  else if (this.steer == 1){
  21.    this._steerAngle = carAngle + this._steeringRange;
  22.  }
  23. }
  24. if (this.steer == 0){
  25.  this._steerAngle = carAngle;
  26. }


6)Поворачиваем передние колёса:
  1. this._frontAxisBodyLeft.body.SetXForm(_frontAxisBodyLeft.body.GetXForm().positionthis._steerAngle);
  2. this._frontAxisBodyRight.body.SetXForm(_frontAxisBodyRight.body.GetXForm().positionthis._steerAngle);


7)Ну и в зависимости от привода прикладываем необходимые силы к нужным колёсам:
  1. var carspeedX, carspeedY, carRearX,carRearY:Number;
  2.  
  3. if (this._privod == FWD){ //передний привод
  4.   carspeedX = this.drive * Math.cos(this._steerAngle) * (this._acceleration);
  5.   carspeedY = this.drive * Math.sin(this._steerAngle) * (this._acceleration);
  6. }
  7. if (this._privod == AWD){
  8.   carspeedX = this.drive * Math.cos(this._steerAngle) * (this._acceleration);
  9.   carspeedY = this.drive * Math.sin(this._steerAngle) * (this._acceleration);
  10.   carRearX = this.drive * Math.cos(this._rearAxisBodyRight.angle) * (this._acceleration);
  11.   carRearY = this.drive * Math.sin(this._rearAxisBodyRight.angle) * (this._acceleration);      
  12. }
  13.  
  14. if (this.drive !0){
  15.   if (this._privod == FWD){//передний привод, едем (прикладываем скорости)
  16.     this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  17.     this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  18.   }
  19.  
  20.   if (this._privod == AWD)//задний привод, едем (прикладываем скорости)
  21.   {
  22.     this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  23.     this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  24.  
  25.     this._rearAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carRearX, carRearY));
  26.     this._rearAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carRearX, carRearY));       
  27.   }       
  28.  
  29. }
  30. else{ //замедляемся
  31.   spd = this.speed;
  32.  
  33.   if (spd > this._friction)
  34.   {
  35.     spd = spd - this._friction/1.3;
  36.   }
  37.   else
  38.   {
  39.     spd = 0;
  40.   }
  41.   if (this._privod == FWD)//передний привод, замедляемся (прикладываем скорости)
  42.   {
  43.     carspeedX = Math.cos(this._steerAngle) * spd;
  44.       carspeedY = Math.sin(this._steerAngle) * spd;
  45.     this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  46.     this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));         
  47.   }
  48.   if (this._privod == AWD){ //полный привод, замедляемся (прикладываем скорости)
  49.     carspeedX = Math.cos(this._steerAngle) * spd;
  50.       carspeedY = Math.sin(this._steerAngle) * spd;
  51.     this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  52.     this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY))
  53.  
  54.     carspeedX = Math.cos(this._carBody.angle) * spd;
  55.       carspeedY = Math.sin(this._carBody.angle) * spd;
  56.     this._rearAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  57.     this._rearAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
  58.   }
  59. }


8) Последний штрих:
  1. public function get speed() : Number{
  2.  return this._carBody.body.GetLinearVelocity().Length();
  3. }


Итого


В результате мы получаем примерно такое:
Демо-swf на народ.ру
Демка на megaswf.com(желательно в фуллскрине)

p.s. Полный код по некоторым причинам выложу немного позже.
Tags:
Hubs:
+89
Comments64

Articles