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

По оси X — поперечное трение колеса
По оси Y вниз — продольное трение колеса
По оси Y вверх — скорость и ускорение
Все вышеописанные оси всегда относительны центра каждого колеса и его угла поворота.
В качестве физического движка я выбрал Box2D. Для облегчения написания кода я все—таки решил использовать обертку QuickBox2D, потому как она существенно облегчает создание примитивов в Box2D.
Подключаем необходимые классы:
… и создаем наш мир:
Прописываем необходимые переменные или константы:
Создаем авто с колёсами:
Ну вот, мы создали наше авто. Вид его примерно как на картинке выше, но только оно будет повернуто на 90% по часовой.
Теперь перед нами стоит главная задача: научить наше авто ездить. Для достижения эффекта езды мы будем прикладывать ApplyForce(..) к нужным колесам (для переднего привода — передние колеса. для полного — все колеса)
Для просчета физики авто и управления им используем accelerate() и вызываем её при каждом обновлении мира:
Ну и собственно сама процедура accelerate() (разбираем построчно, полный код процедуры будет позже):
1) Для начала мы разбудим наши тела
2) Затем добавим необходимые силы трения(общую, по X, и по Y) чтобы авто ехало вперед, а не скользило при резком повороте, и при движении по инерции — замедлялось:
3) setLinearDamping() и setAngularDamping()
4) Ну и самая интересная на мой взгляд процедура addFriction(), реализацию которой я так долго искал и не нашел, и вот реализовал сам:
Данная процедура раскладывает глобальный вектор скорости spd у каждого колеса на локальный spd2 и прикладывает трения по X и по Y. Единственные момент, который не укладывается в моей голове так это то, почему же все-таки оси повернуты на 90% по часовой? То есть чтобы отучить авто скользить и добавить трения по оси X которую я зарисовывал выше сейчас нам приходится добавлять трение на ось Y (оно же — поперечное трение колеса).
5) Продолжаем дополнять accelerate(), просчитываем углы по которым потом будем прикладывать силы скорости и ускорения (для передних колес):
6)Поворачиваем передние колёса:
7)Ну и в зависимости от привода прикладываем необходимые силы к нужным колёсам:
8) Последний штрих:
В результате мы получаем примерно такое:
Демо-swf на народ.ру
Демка на megaswf.com(желательно в фуллскрине)
p.s. Полный код по некоторым причинам выложу немного позже.
Вступление
Скажу сразу, что я еще не достаточно знаком со всеми понятиями физики, но понимаю что они из себя представляют, поэтому умных слов не ждите, все буду стараться описывать наиболее понятным и человеческим языком, извиняйте заранее.
Толчком для написания этой статьи является отсутствие рабочего аналога. На тот момент когда мне нужно было сделать подобное, я нагуглил лишь один более—менее нормальный пример, который, к сожалению, был мне не интересен по некоторым причинам:
- Полное отсутствие дрифта/скольжения
- Различная скорость при движении по осям X и Y (неизвестно, чем это вызвано, но мне такое не надо)
Подготовка
Итак, что мы знаем. А знаем мы то, что при движении на авто (а в нашем случае — на колёса) действуют некоторые силы. Для нашего простого проекта естесвенно, некоторыми из этих сил можно пренебречь (мы же не полноценный симулятор делаем).
Все параметры, которые мы будем использовать:
- Сила тяги (оно же у нас будет являться скоростью и ускорением)
- Продольное трение колесa
- Поперечное трение колеса
Все эти силы мы будем прикладывать непосредственно к объектам колёс.
При движении вперёд графически эти силы можно изобразить так (за переднюю часть авто на картинках принимать верх):

По оси X — поперечное трение колеса
По оси Y вниз — продольное трение колеса
По оси Y вверх — скорость и ускорение
Все вышеописанные оси всегда относительны центра каждого колеса и его угла поворота.
Пишем код
В качестве физического движка я выбрал Box2D. Для облегчения написания кода я все—таки решил использовать обертку QuickBox2D, потому как она существенно облегчает создание примитивов в Box2D.
Подключаем необходимые классы:
- import Box2D.Collision.Shapes.*;
- import Box2D.Common.Math.*;
- import Box2D.Dynamics.*;
- import Box2D.Dynamics.Joints.*;
- import Box2D.Collision.*;
- import Box2D.Common.*;
- import com.actionsnippet.qbox.*;
… и создаем наш мир:
- var world:QuickBox2D = new QuickBox2D(this,{debug:false, frim:true});
- world.gravity = new b2Vec2(0,0); //вектор гравитации
- world.start(); //старт отрисовки и просчета физики
- world.createStageWalls(); //фича от QuickBox'a - создает 4 стены по размерам stage
- world.mouseDrag(); //и разрешаем перемещать все тела мышью
Прописываем необходимые переменные или константы:
- //константы и переменные:
- private const FWD=1; //передний
- private const AWD=2; //полный
- private const RWD=3; //задний
- private var _privod:Number; //текущий привод авто, выбирается при создании
- private var _maxTurnAngle:Number; // максимальный угол поворота колес
- private var _width:Number; //длина авто
- private var _height:Number; //ширина
- private var _axisOffset:Number; //отступ колес по длине, относительно центра авто
- private const _angularDamping = 7; //замедление кручения колеса
- private const _angularDampingReverse = 3; //то же, но при заднем ходе
- private const _linearDamping = 0.25; //замедление скольжения колеса, что-то вроде трения
- private const _linearDampingReverse = 0.5; // и для заднего хода
-
- public var _carBody:QuickObject;//объект автомобиля
-
- private var _frontAxisBodyLeft:QuickObject;//и его колес
- private var _frontAxisBodyRight:QuickObject;
- private var _rearAxisBodyLeft:QuickObject;
- private var _rearAxisBodyRight:QuickObject;
-
- private var _frontLeftJoint:QuickObject;//шарнирные соединения для передних колёс
- private var _frontRightJoint:QuickObject;
- private var _rearLeftJoint:QuickObject;//шарнирные соединения для задних колёс
- private var _rearRightJoint:QuickObject;
- protected var axisHeight:Number = 4;//ширина колеса
- protected var axisWidth:Number = 7;//длина
-
- public var drive:int = 0;//переменные для управлления автомобилем
- public var steer:int = 0;
- //drive = 1 - едем вперед
- //drive = 0 - отпускаем газ и катимся по инерции, либо просто стоим
- //drive = -1 - едем назад
- //
- //steer = -1 - рулим влево
- //steer = 0 - никуда не рулим
- //steer = 1 - рулим вправо
-
Создаем авто с колёсами:
- //создаем авто
- _width = 76;
- _height = 40;
- _maxTurnAngle = 15 * Globals.DEG_TO_RAD; //максимальный угол поворота передних колес
- _axisOffset:Number = (_width/4 - axisWidth)/ Globals.RATIO //отступ колес по длине
- _privod = FWD; // ну и наш привод, пока что функционируют передний и полный
-
- _carBody = world.addBox({
- width :this._width / 2 / Globals.RATIO,
- height :this._height / 2 / Globals.RATIO,
- x :(this.x + this._width / 2) / Globals.RATIO,
- y :(this.y + this._height/ 2) /Globals.RATIO,
- density: 0.48,
- driction:0.3,
- restitution:0.4,
- linearDamping:this._linearDamping,
- angularDamping:this._angularDamping,
- groupIndex:-1,
- isSleeping:true,
- skin:null,
- scaleSkin:true});
-
- this._frontAxisBodyLeft = this.world.addBox({
- width :this.axisWidth / Globals.RATIO,
- height :(this.axisHeight) / Globals.RATIO,
- x :_carBody.body.GetWorldCenter().x + this._axisOffset,
- y :_carBody.body.GetWorldCenter().y - this._height/4/Globals.RATIO+1/Globals.RATIO,// + (this.axisHeight/2) / Globals.RATIO,
- density:0.48,
- friction:0.3,
- restitution:0.5,
- linearDamping:this._linearDamping,
- angularDamping:this._angularDamping,
- groupIndex:-1});
-
- this._frontAxisBodyRight = this.world.addBox({
- width :this.axisWidth / Globals.RATIO,
- height :(this.axisHeight) / Globals.RATIO,
- x :_carBody.body.GetWorldCenter().x + this._axisOffset,
- y :_carBody.body.GetWorldCenter().y + this._height/4/Globals.RATIO-1/Globals.RATIO,// - (this.axisHeight) / Globals.RATIO,
- density:0.48,
- friction:0.3,
- restitution:0.5,
- linearDamping:this._linearDamping,
- angularDamping:this._angularDamping,
- groupIndex:-1});
-
- this._frontLeftJoint = this.world.addJoint({
- type:QuickBox2D.REVOLUTE,
- a:this._carBody.body,
- b:this._frontAxisBodyLeft.body,
- x1:this._frontAxisBodyLeft.body.GetWorldCenter().x,
- y1:this._frontAxisBodyLeft.body.GetWorldCenter().y,
- x2:this._frontAxisBodyLeft.body.GetWorldCenter().x,
- y2:this._frontAxisBodyLeft.body.GetWorldCenter().y,
- enableLimit:true,
- enableMotor:true,
- collideConnected:true,
- lowerAngle:-this._maxTurnAngle,
- upperAngle:this._maxTurnAngle
- });
-
- this._frontRightJoint = this.world.addJoint({
- type:QuickBox2D.REVOLUTE,
- a:this._carBody.body,
- b:this._frontAxisBodyRight.body,
- x1:this._frontAxisBodyRight.body.GetWorldCenter().x,
- y1:this._frontAxisBodyRight.body.GetWorldCenter().y,
- x2:this._frontAxisBodyRight.body.GetWorldCenter().x,
- y2:this._frontAxisBodyRight.body.GetWorldCenter().y,
- enableLimit:true,
- enableMotor:true,
- collideConnected:true,
- lowerAngle:-this._maxTurnAngle,
- upperAngle:this._maxTurnAngle
- });
-
- this._rearAxisBodyLeft = this.world.addBox({
- width :this.axisWidth / Globals.RATIO,
- height :(this.axisHeight) / Globals.RATIO,
- x :_carBody.x - this._axisOffset,//-this.frontPivotOffset,
- y :_carBody.y - this._height/4/Globals.RATIO+1/Globals.RATIO,// + (this.axisHeight/2) / Globals.RATIO,
- density:0.48,
- friction:0.3,
- restitution:0.5,
- linearDamping:this._linearDamping,
- angularDamping:this._angularDamping,
- groupIndex:-1,
- isSleeping:true});
-
- this._rearAxisBodyRight = this.world.addBox({
- width :this.axisWidth / Globals.RATIO,
- height :(this.axisHeight) / Globals.RATIO,
- x :_carBody.x - this._axisOffset,//-this.frontPivotOffset,
- y :_carBody.y + this._height/4/Globals.RATIO-1/Globals.RATIO,// - (this.axisHeight) / Globals.RATIO,
- density:0.48,
- friction:0.3,
- restitution:0.5,
- linearDamping:this._linearDamping,
- angularDamping:this._angularDamping,
- groupIndex:-1,
- isSleeping:true});
-
-
- this._rearLeftJoint = this.world.addJoint({
- type:QuickBox2D.REVOLUTE,
- a:this._carBody.body,
- b:this._rearAxisBodyLeft.body,
- x1:this._rearAxisBodyLeft.x,
- y1:this._rearAxisBodyLeft.y,
- x2:this._rearAxisBodyLeft.x,
- y2:this._rearAxisBodyLeft.y,
- enableLimit:true,
- enableMotor:false,
- collideConnected:false,
- lowerAngle:0,
- upperAngle:0
- });
-
- this._rearRightJoint = this.world.addJoint({
- type:QuickBox2D.REVOLUTE,
- a:this._carBody.body,
- b:this._rearAxisBodyRight.body,
- x1:this._rearAxisBodyRight.x,
- y1:this._rearAxisBodyRight.y,
- x2:this._rearAxisBodyRight.x,
- y2:this._rearAxisBodyRight.y,
- enableLimit:true,
- enableMotor:false,
- lowerAngle:0,
- upperAngle:0
- });
Ну вот, мы создали наше авто. Вид его примерно как на картинке выше, но только оно будет повернуто на 90% по часовой.
Теперь перед нами стоит главная задача: научить наше авто ездить. Для достижения эффекта езды мы будем прикладывать ApplyForce(..) к нужным колесам (для переднего привода — передние колеса. для полного — все колеса)
Для просчета физики авто и управления им используем accelerate() и вызываем её при каждом обновлении мира:
- stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownList);
- stage.addEventListener(KeyboardEvent.KEY_UP, keyUpList);
- world.addEventListener(QuickBox2D.STEP,onStep);
- ...
- var bup:Boolean;
- var bdown:Boolean;
- var bleft:Boolean;
- var bright:Boolean;
-
- function keyDownList(e:KeyboardEvent):void
- {
- if (e.keyCode == Keyboard.UP){
- bup = true;
- }
- if (e.keyCode == Keyboard.LEFT){
- bleft = true;
- }
- if (e.keyCode == Keyboard.DOWN){
- bdown = true;
- }
- if (e.keyCode == Keyboard.RIGHT){
- bright = true;
- }
- };
- function keyUpList(e:KeyboardEvent):void
- {
- if (e.keyCode == Keyboard.UP){
- bup = false;
- };
- if (e.keyCode == Keyboard.LEFT){
- bleft = false;
- };
- if (e.keyCode == Keyboard.DOWN){
- bdown = false;
- };
- if (e.keyCode == Keyboard.RIGHT){
- bright = false;
- };
- };
-
- function onStep(evt:Event):void
- {
- car.accelerate();
- if (bup) {car.drive =1};
- if (bdown) {car.drive=-1};
- if (!bup && !bdown) (car.drive=0);
- if (bleft) {car.steer=-1};
- if (bright) {car.steer=1};
- if (!bleft && !bright) {car.steer=0};
- };
Ну и собственно сама процедура accelerate() (разбираем построчно, полный код процедуры будет позже):
1) Для начала мы разбудим наши тела
- this._carBody.body.WakeUp();
- this._frontAxisBodyLeft.body.WakeUp();
- this._frontAxisBodyRight.body.WakeUp();
2) Затем добавим необходимые силы трения(общую, по X, и по Y) чтобы авто ехало вперед, а не скользило при резком повороте, и при движении по инерции — замедлялось:
- if (this.drive > 0){ //едем вперёд
- this._acceleration = this._accelerationForward;
- this.setLinearDamping(this._linearDamping);
- this.setAngularDamping(this._angularDamping);
- }
- else { //едем назад или стоим
- this._acceleration = this._accelerationBackwards;
- this.setLinearDamping(this._linearDamping);
- this.setAngularDamping(this._angularDamping);
- }
- this.addFriction(this._rearAxisBodyLeft.body,0);
- this.addFriction(this._rearAxisBodyRight.body,0);
- this.addFriction(this._frontAxisBodyLeft.body,1);
- this.addFriction(this._frontAxisBodyRight.body,1);
3) setLinearDamping() и setAngularDamping()
- private function setLinearDamping(linDamp:Number) : void
- {
- this._frontAxisBodyLeft.body.m_linearDamping = linDamp;
- this._frontAxisBodyRight.body.m_linearDamping = linDamp;
- this._rearAxisBodyLeft.body.m_linearDamping = linDamp;
- this._rearAxisBodyRight.body.m_linearDamping = linDamp;
- this._carBody.body.m_linearDamping = linDamp;
- return;
- }
-
- private function setAngularDamping(angDamp:Number) : void
- {
- this._frontAxisBodyLeft.body.m_angularDamping = angDamp;
- this._frontAxisBodyRight.body.m_angularDamping = angDamp;
- this._rearAxisBodyLeft.body.m_angularDamping = angDamp;
- this._rearAxisBodyRight.body.m_angularDamping = angDamp;
- this._carBody.body.m_angularDamping = angDamp;
- return;
- }
4) Ну и самая интересная на мой взгляд процедура addFriction(), реализацию которой я так долго искал и не нашел, и вот реализовал сам:
- function addFriction(targetBody:b2Body, isFront:Number)
- {
- var spd,spd2:b2Vec2;
- spd = targetBody.GetLinearVelocity();
- spd2 = targetBody.GetLocalVector(spd);
- if (this._privod == FWD){//передний привод
- if (isFront == 1){
- spd2.y = spd2.y * 0.5;
- }
- else {
- spd2.y = spd2.y>2.5? spd2.y * 0.45 : 0;
- }
- spd = targetBody.GetWorldVector(spd2);
- targetBody.SetLinearVelocity(spd);
- }
- if (this._privod == RWD){
- if (isFront == 1){
- spd2.y = 0;
- spd2.x = spd2.x * .9;
- spd = targetBody.GetWorldVector(spd2);
- targetBody.SetLinearVelocity(spd);
- }
- else{
- spd2.y = spd2.y>2.5? spd2.y * 0.25 : 0;
- spd = targetBody.GetWorldVector(spd2);
- targetBody.SetLinearVelocity(spd);
- }
- }
- }
Данная процедура раскладывает глобальный вектор скорости spd у каждого колеса на локальный spd2 и прикладывает трения по X и по Y. Единственные момент, который не укладывается в моей голове так это то, почему же все-таки оси повернуты на 90% по часовой? То есть чтобы отучить авто скользить и добавить трения по оси X которую я зарисовывал выше сейчас нам приходится добавлять трение на ось Y (оно же — поперечное трение колеса).
5) Продолжаем дополнять accelerate(), просчитываем углы по которым потом будем прикладывать силы скорости и ускорения (для передних колес):
- var carAngle:Number = this._carBody.body.GetAngle();
- if (this.steer != 0){ // при езде вперед
- this._steerAngle = this._steerAngle + this.steer * this._steeringAcceleration;
- }
- if (this._steerAngle > carAngle + this._steeringRange){
- this._steerAngle = carAngle + this._steeringRange;
- }
- else if (this._steerAngle < carAngle - this._steeringRange){
- this._steerAngle = carAngle - this._steeringRange;
- }
- if (this.drive < 0){//при езде назад
- if (this.steer == 0){
- this._steerAngle = carAngle;
- }
- else if (this.steer == -1){
- this._steerAngle = carAngle - this._steeringRange;
- }
- else if (this.steer == 1){
- this._steerAngle = carAngle + this._steeringRange;
- }
- }
- if (this.steer == 0){
- this._steerAngle = carAngle;
- }
6)Поворачиваем передние колёса:
- this._frontAxisBodyLeft.body.SetXForm(_frontAxisBodyLeft.body.GetXForm().position, this._steerAngle);
- this._frontAxisBodyRight.body.SetXForm(_frontAxisBodyRight.body.GetXForm().position, this._steerAngle);
7)Ну и в зависимости от привода прикладываем необходимые силы к нужным колёсам:
- var carspeedX, carspeedY, carRearX,carRearY:Number;
-
- if (this._privod == FWD){ //передний привод
- carspeedX = this.drive * Math.cos(this._steerAngle) * (this._acceleration);
- carspeedY = this.drive * Math.sin(this._steerAngle) * (this._acceleration);
- }
- if (this._privod == AWD){
- carspeedX = this.drive * Math.cos(this._steerAngle) * (this._acceleration);
- carspeedY = this.drive * Math.sin(this._steerAngle) * (this._acceleration);
- carRearX = this.drive * Math.cos(this._rearAxisBodyRight.angle) * (this._acceleration);
- carRearY = this.drive * Math.sin(this._rearAxisBodyRight.angle) * (this._acceleration);
- }
-
- if (this.drive != 0){
- if (this._privod == FWD){//передний привод, едем (прикладываем скорости)
- this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- }
-
- if (this._privod == AWD)//задний привод, едем (прикладываем скорости)
- {
- this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
-
- this._rearAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carRearX, carRearY));
- this._rearAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carRearX, carRearY));
- }
-
- }
- else{ //замедляемся
- spd = this.speed;
-
- if (spd > this._friction)
- {
- spd = spd - this._friction/1.3;
- }
- else
- {
- spd = 0;
- }
- if (this._privod == FWD)//передний привод, замедляемся (прикладываем скорости)
- {
- carspeedX = Math.cos(this._steerAngle) * spd;
- carspeedY = Math.sin(this._steerAngle) * spd;
- this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- }
- if (this._privod == AWD){ //полный привод, замедляемся (прикладываем скорости)
- carspeedX = Math.cos(this._steerAngle) * spd;
- carspeedY = Math.sin(this._steerAngle) * spd;
- this._frontAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- this._frontAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
-
- carspeedX = Math.cos(this._carBody.angle) * spd;
- carspeedY = Math.sin(this._carBody.angle) * spd;
- this._rearAxisBodyLeft.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- this._rearAxisBodyRight.body.SetLinearVelocity(new b2Vec2(carspeedX, carspeedY));
- }
- }
8) Последний штрих:
- public function get speed() : Number{
- return this._carBody.body.GetLinearVelocity().Length();
- }
Итого
В результате мы получаем примерно такое:
Демо-swf на народ.ру
Демка на megaswf.com(желательно в фуллскрине)
p.s. Полный код по некоторым причинам выложу немного позже.