Vehicle physics

Using ODE to create a vehicle that can be driven by the player is fairly easy, as long as it does not require balancing like a bicycle.

Step 1: The car

Our car should have four wheels and will be created from five bodies and five geoms that are attached with four hinges at the frame. Our four wheels will be spheres (though graphically represented by cylinders) and the frame will be a box.

We'll put the car wheels and the frame into one space, so we can switch off the collision testing between the objects.

globalspace = dspacehash.new()
staticspace = dspacehash.new(globalspace) 
  -- add it to globalspace
staticspace:collidetest(false)

-- create the frame
local cw,cl,ch = 2,3,1 -- width, length, height
car = dbody.new()
car.space = dspacehash.new(globalspace)
car.space:collidetest(false)
car.actor = actornode.new("frame")
car.actor:link(car)
car.geom = dgeombox.new(cw,cl,ch,car.space)
car.geom:body(car)
car.l3d = l3dprimitive.newbox("box",cw,cl,ch)
car.l3d:linkinterface(car.actor)
car.l3d:rfLitSun(true)

Now, we'll just need to attach the wheels. However, adding four wheels again, so let's write a function that attachs a new wheel. When connecting bodies with joints, the bodies should be in their right relative coordinates.

function car:addwheel(x,y,z,rad,h)
  local wheel = dbody.new()
  wheel:pos(x,y,z)
  wheel.actor = actornode.new("wheel")
  wheel.actor:link(wheel)
  wheel.geom = dgeomsphere.new(rad,car.space)
  wheel.geom:body(wheel)
  wheel.l3d = l3dprimitive.newcylinder("cyl",rad,rad,h)
  wheel.l3d:linkinterface(wheel.actor)
  wheel.l3d:uselocal(true)
  wheel.l3d:localrotdeg(0,90,0)
  wheel.l3d:rfLitSun(true)
  wheel.l3d:color(1,1,0,0)
  wheel:masscylinder(1,1, rad,h)
  wheel.hinge = djointhinge2.new()
  wheel.hinge:attach(self,wheel)
  wheel.hinge:anchor(x,y,z)
  wheel.hinge:axis1(0,0,1)
  wheel.hinge:axis2(1,0,0)
  return wheel
end

car.fl = car:addwheel(-1, 1,-.5,.5,.25)
car.fr = car:addwheel( 1, 1,-.5,.5,.25)
car.bl = car:addwheel(-1,-1,-.5,.5,.25)
car.br = car:addwheel( 1,-1,-.5,.5,.25)

Step 2: Steering the car

Our car requires now only some input from the user. We will modify the think function that we used earlier:

function think ()
  local left = Keyboard.isKeyDown("LEFT")
  local right = Keyboard.isKeyDown("RIGHT")
  local up = Keyboard.isKeyDown("UP")
  local down = Keyboard.isKeyDown("DOWN")
  local fx = (left and 1 or 0) - (right and 1 or 0)
  local fy = (up and 1 or 0) - (down and 1 or 0)
  -- fx,fy: movement directions

-- this is new  
  -- let the joint steer to a given angle
  local function steerto (hinge,to)
    local a = hinge:angle1()
    local dif = to - a
    hinge:fmax(0.5) -- maximum applied steering force 
    local maxvel = math.max(-.05,math.min(.05,dif))
    -- limit the applied turnrate
    hinge:velocity(maxvel)
  end
  steerto(car.fl.hinge,-fx*.4) -- use left/right
  steerto(car.fr.hinge,-fx*.4) -- as steering
  steerto(car.bl.hinge,0) -- stabilize backwheels
  steerto(car.br.hinge,0) -- otherwise they will turn

  -- backwheel rotation
  car.bl.hinge:fmax2(.004)
  car.br.hinge:fmax2(.004)
  car.bl.hinge:velocity2(fy)
  car.br.hinge:velocity2(fy)
-- till here

  dworld.collidetest(globalspace)
  dworld.makecontacts()
  dworld.quickstep(1)
end

Pushing the arrow keys will now change the car acceleration and its steering.

We are using for all wheels the hinge2, which offers two axis of rotation. This is because only the hinge2 joint has suspension parameters where we can addjust the shocks.

Step 3: Suspension and surface parameters

We have not yet defined how the surface interacts with our wheels. This is quite important if we want to build a more realistic physics model. Especially when the car is sliding, we will see that it drifts to more or less random directions. To avoid this, we need to set the friction direction vector for the wheel geoms. The internal calculation is rather complex, but all we need to know now is, that we have to set the normal of the friction direction vector:
 wheel.geom:fdirnormal(1,0,0)
Even if we won't see the effect in this simple example, it is important to know this problem, otherwise you will likely encounter problems when building more complex simluations. The friction direction has however to be activated in the surface properties.

We can however visualize what the friction direction vector does: We can set two different mu values (friction) for each perpendicular direction of the contact.

The surface parameters that are used for the contacts can be configured individually. We can define 255 different surfaces and can define, which surface parameters are to be used if two surfaces are in contact. Each geom can be assigned a surface id, ranging from 0-255. Per default, this is set to 0. We can set it to a different value by calling this function:

 wheel.geom:surfaceid(1) 
The surface id of the wheel's geom is now set to 1.

Durin initialisation (anywhere in the script), we can define the surface properties. First, we want to define that if a surface id of value 0 is in contact with a surface id of 1, should use the parameters of surface 1:

dworld.surfacecombination(0,1, 1) -- 0-1 combination, use surfacetype 1

We only need to set the surface properties now. There are bitflags which toggle certain options on or off, and in most cases, these will use number values as properties that we can define by a second call. The mu property is always active for example, the mu2 property however must be activated:

-- at the top of the script
dworld.surfacecombination(0,1, 1) -- 0/1 combination, use surfacetype 1

dworld.surfacebitfdir(1,true) -- enable the friction direction
dworld.surfacemu(1,50)
dworld.surfacebitmu2(1,true) -- enable mu2
dworld.surfacemu2(1,0.1) -- low friction in the other direction

The mu values are set to quite extreme values, so that it will be easily recognizable what the friction direction vector does. The car will now accelerate quite normaly, however we can hardly steer and the car tends to slide sideways, since this is the direction of the lowest resistance.

Finally, I want to show how to edit the suspension of our car:

function car:addwheel(x,y,z,rad,h)
  (...)
  wheel.hinge:suspensionerp(.5)
  wheel.hinge:suspensioncfm(15)
end

The suspensionerp (error reduction parameter) factor must be ranged between 0 and 1 while the cfm (constraint force mixing) value can be set to any value. A high ERP will result in a stronger pushback, while a higher cfm value will make the suspension softer. The given example is quite bouncy, but playing around with the values should give you a feeling how the values influence the behavior: