Cars are among the most complex physics objects you can create in your game. Although you only need 4 joints, the number of variations on suspension, engine power, friction, and so on are nearly endless. Before you can experiment with your car to find the perfect balance, you first need to understand how to set up an ODE car and learn about CFM and ERP.
ShiVa uses the Open Dynamics Engine (ODE) as its physics subsystem. Since our API documentation cannot go into ODE details, it is often worth having a look at the ODE Documentation. Not only does this site explain the basics like joints, but you will also learn about the mathematical formulas used to simulate your game physics. Furthermore, you will get recommendations for number values to plug into our API, like suggestions for masses, friction coefficients, spring tension and a lot more.
ODE Car Setup
For the most basic car, you need to have two separate models, one for the cat and one single wheel. For this tutorial, we will use the following objects:
Although you could define coordinates for the wheels purely by script, I found it a lot easier to create dummy models and parent them to the car, so they can serve as attachment points to the wheels:
In the same way, you can define additional attachment points for SFX like exhaust smoke, break lights, skidmarks, trails and so forth.
Next, you need to define a collision body for the wheels. Since ShiVa does not support cylindrical physics bodies for the time being, you need to either approximate your wheels using a sphere or a cylinder body. Since we want our wheels to be able to roll upright on their own, we decided on capsules:
As for the collision body of the car, you need to make a decision: how accurate do you need it to be? The easiest and fastest way would be to use a single box, like so:
If you need more precision, you can use multiple boxes to approximate the body shape more cleanly. Colliders on the other hand are not useful for this, since colliders are made for static objects like level geometry.
Collision boxes can be moved and scaled directly inside the Stage, however they cannot be rotated.
ODE Car Physics
Now that we have created the parts of our car, we need to assemble them in our scene. First, we need to dynamically create all our wheels at the appropriate attachment points on our car. We know the first 4 children of the car object are our attachment points. We also need to stick all the wheel object handles into a table to have a means of accessing them easily.
- local wheeltable = this._tWheels ( )
- local wheelmodel = this.sModelWheel ( )
- table.empty ( wheeltable )
- table.reserve ( wheeltable, 4 )
- for i = 0, 3 do
- local wheelAttachmentPoint = object.getChildAt ( body, i )
- local wheelTemp = scene.createRuntimeObject ( hScene, wheelmodel )
- if wheelTemp ~= nil then
- table.add ( wheeltable, wheelTemp )
- this.initWheel ( wheelTemp, wheelAttachmentPoint, "wheel"..i, body )
Both car and wheels are separate physics objects which overlap. By default, this will be interpreted as a very hard collision, and the pieces of the car would fly apart with some considerable force. To prevent this from happening, the wheels need to be assigned a different collision category and a different masking bit:
- function FRAMEWORK_CAR_simplePlayerCar.initWheel ( hWheel, hAnchor, jointname, hCar )
- -- disable dynamics in order to change it
- dynamics.enableDynamics ( hWheel, false )
- dynamics.enableCollisions ( hWheel, false )
- dynamics.enableGravity ( hWheel, false )
- -- make sure the wheels cannot collide with the car body
- dynamics.setCollisionCategoryBit ( hWheel, 0, false )
- dynamics.setCollisionMaskBit ( hWheel, 0, false )
- dynamics.setCollisionCategoryBit ( hWheel, 1, true )
- dynamics.setCollisionMaskBit ( hWheel, 1, true )
Now we move the wheels into place. We also know that two of the wheels (ID 1 and 3) need to be rotated by 180 degrees, since they are on the right side.
- -- position and rotate wheels
- object.matchTranslation ( hWheel, hAnchor, object.kGlobalSpace )
- object.matchRotation ( hWheel, hAnchor, object.kGlobalSpace )
- -- it's a right wheel, turn it around
- if jointname == "wheel1" or jointname == "wheel3" then
- object.setRotation ( hWheel, 0,180,0, object.kLocalSpace )
Then we define some standard physics parameters like object mass, damping, friction and bounciness. These can (and should) be individually set for cars and wheels, as the bounciness of a steel body in a crash is different from the bounciness of a rubber wheel going over a bump.
- dynamics.setMass ( hWheel, t0 )
- dynamics.setLinearDamping ( hWheel, t1 )
- dynamics.setAngularDamping ( hWheel, t2 )
- dynamics.setFriction ( hWheel, t3 )
- dynamics.setBounce ( hWheel, t4 )
After the creation of the wheels, we need to attach them to the body using a special ODE joint called Hinge2. This joint is the same as two hinges connected in series, with different hinge axes. In the picture below, which is taken from the ODE documentation, you can see the steering wheel of a car, where one axis allows the wheel to be steered and the other axis allows the wheel to rotate.
In ShiVa, you can create such a joint through dynamics.createHinge2Joint and a number of subsequent functions that define the anchor and axis as mentioned above.
- if ( dynamics.createHinge2Joint ( hCar, hWheel, jointname ) ) then
- local x,y,z = object.getTranslation ( hAnchor, object.kGlobalSpace )
- local xx,xy,xz = object.getXAxis ( hCar, object.kGlobalSpace )
- local yx,yy,yz = object.getYAxis ( hCar, object.kGlobalSpace )
- dynamics.setHinge2JointAnchor ( hCar, jointname, x , y , z, object.kGlobalSpace )
- dynamics.setHinge2JointAxis1 ( hCar, jointname, yx, yy, yz, object.kGlobalSpace )
- dynamics.setHinge2JointAxis2 ( hCar, jointname, xx, xy, xz, object.kGlobalSpace )
- dynamics.setHinge2JointAxis1AngleLimitMin ( hCar, jointname, 0 )
- dynamics.setHinge2JointAxis1AngleLimitMax ( hCar, jointname, 0 )
- dynamics.setHinge2JointAxis1AngleLimitCFM ( hCar, jointname, 0 )
- dynamics.setHinge2JointAxis1AngleLimitERP ( hCar, jointname, 0.4 )
- dynamics.setHinge2JointAxis1SuspensionCFM ( hCar, jointname, 0.0015 )
- dynamics.setHinge2JointAxis1SuspensionERP ( hCar, jointname, 0.2 )
- log.warning ( "Joint " ..jointname .."creation failed!" )
Axis1 is the Y axis that connects the wheel to the car, Axis2 is the axis of the wheel that it freely rotates around. By default, you want the wheels to be straight, therefor setHinge2JointAxis1AngleLimitMin/Max is set to 0.
In the end, you only need to activate dynamics.* again and you should be ready to go.
- -- re-enable dynamics
- dynamics.enableDynamics ( hWheel, true )
- dynamics.enableCollisions ( hWheel, true )
- dynamics.enableGravity ( hWheel, true )
ERP, or Error Reduction Parameter, is an important concept in ODE. When a joint attaches two bodies, those bodies are required to have certain positions and orientations relative to each other. However, it is possible for the bodies to be in positions where the joint constraints are not met. Small errors in positioning can build up over time and need to be corrected.
During each simulation step, each joint applies a special force to bring its bodies back into correct alignment. This force is controlled by the error reduction parameter (ERP), which has a value between 0 and 1. If ERP=0, then no correcting force is applied and the bodies will eventually drift apart as the simulation proceeds. If ERP=1, then the simulation will attempt to fix all joint error during the next time step. At first glance, it may seem than setting ERP=1 is the best approach, because then all joint errors will be fully corrected at each time step. However, ODE uses various approximations in its integrator, so ERP=1 will not usually fix 100% of the joint error. ERP=1 can work in some cases, but it can also result in instability in some systems. In these cases you have the option of reducing ERP to get a better behaving system. A value of ERP=0.1 to 0.8 is recommended (0.2 is the default).
Just for fun, here is what ERP=0 looks like on Axis1. The wheels do essentially what they want:
In the code above, we have 2 ERP values for Axis1. setHinge2JointAxis1AngleLimitERP influences the precision of the steering, and setHinge2JointAxis1SuspensionERP handles error in the suspension calculation. If you have wobble in any of these systems, you should use higher values than the default 0.2, otherwise leave it as low as possible.
The other strange looking functions deal with CFM, which stands for Constant Force Mixing.
Most constraints are by nature "hard", meaning the objects that are attached through joints should always stay together as defined. For example, the ball must always be in the socket, and the two parts of the hinge must always be lined up - and if something goes wrong, ERP is there to save us. However, there are some joints that simulate materials other than hard steel, such as the suspension of our car. That is where CFM, or Constant Force Mixing, comes into play.
If CFM is set to zero, the constraint will be hard. If CFM is set to a positive value, it will be possible to violate the constraint by "pushing on it" - in other words the constraint will be soft, and the softness will increase as CFM increases. What is actually happening here is that the constraint is allowed to be violated by an amount proportional to CFM times the restoring force that is needed to enforce the constraint. Note that setting CFM to a negative value can have undesirable bad effects, such as instability.
By adjusting the values of ERP and CFM, you can achieve various effects. For example you can simulate springy constraints, where the two bodies oscillate as though connected by springs. Or you can simulate more spongy constraints, without the oscillation. Since our example toy car weighs only 50 units and is very small, a CFM of 0.0015 seems just right.
You can steer the car by manipulating the angle limits in Axis1.
- --wheel turning
- local curSteer = this._nCurrentSteering ( )
- dynamics.setHinge2JointAxis1AngleLimitMin ( hCar, "wheel0", curSteer )
- dynamics.setHinge2JointAxis1AngleLimitMax ( hCar, "wheel0", curSteer )
- dynamics.setHinge2JointAxis1AngleLimitMin ( hCar, "wheel1", curSteer )
- dynamics.setHinge2JointAxis1AngleLimitMax ( hCar, "wheel1", curSteer )
The variable nCurrentSteering is calculated very frame by taking the normalized user input (-1..1 for left..right) from either a keypress or a joystick input, and multiplying it by a constant factor which represents your maximum steering angle.
A Choice of Engines
There are two ways to power your car. The simple way uses a single force vector which is applied to the car, either directly in the direction of the car's local Z-axis (rear wheel drive) or along the direction of the steered wheels (front wheel drive).
- local sp = this._nCurrentSpeed ( ) * this.nEnginePower ( )
- dynamics.addForce ( hCar, 0,0,sp, object.kLocalSpace )
This kind of simple engine is fine for many arcade games, but it's far from being an accurate simulation. If you want more realism, you can actually power a Hinge2Joint with a motor. Functions like setHinge2JointAxis2MotorSpeedLimit and setHinge2JointAxis2MotorAcceleration must be calculated for every wheel and also adjusted for turning.
- local oSpeed = dynamics.getLinearSpeed ( this.getObject ( ) )
- local speed = this.nMotorSpeed ( )
- local power = this.nMotorPower ( )
- local bFactor = 0.5 - math.min ( 40, oSpeed ) / 40 * 0.5
- local lFactor
- local rFactor
- if ( this.bTurnLeft ( ) ) then lFactor = 0.5 rFactor = 1.5
- elseif ( this.bTurnRight ( ) ) then lFactor = 1.5 rFactor = 0.5
- else lFactor = 1.0 rFactor = 1.0 bFactor = 0.5
- dynamics.setHinge2JointAxis2MotorSpeedLimit ( o, "Wheel1", speed * 1.0 )
- dynamics.setHinge2JointAxis2MotorSpeedLimit ( o, "Wheel2", speed * 1.0 )
- dynamics.setHinge2JointAxis2MotorSpeedLimit ( o, "Wheel3", speed * lFactor )
- dynamics.setHinge2JointAxis2MotorSpeedLimit ( o, "Wheel4", speed * rFactor )
- dynamics.setHinge2JointAxis2MotorAcceleration ( o, "Wheel1", power * bFactor )
- dynamics.setHinge2JointAxis2MotorAcceleration ( o, "Wheel2", power * bFactor )
- dynamics.setHinge2JointAxis2MotorAcceleration ( o, "Wheel3", power * lFactor )
- dynamics.setHinge2JointAxis2MotorAcceleration ( o, "Wheel4", power * rFactor )
If you want to come to a complete stop eventually, you should set dynamics.setAngularVelocity to 0,0,0 once you are at a low enough speed.
Simulating a car, there are other effects you need to think about, if you strive for accuracy. For instance, as the speed increases, wind plays an increasingly big role. Race cars with carefully designed spoilers and aerodynamic bodies develop develop a lot of downforce, only this way it is possible for them to achieve the staggering cornering speeds without drifting out of the lane. A modern Formula 1 car for instance is capable of developing 3.5 g lateral cornering force (three and a half times its own weight) thanks to aerodynamic downforce. Theoretically, at high speeds, they could drive upside down.
Track conditions change all the time, tires change grip/friction with temperatures. If you have a track with offroad sections, make sure your friction and engine power change accordingly.
It is your job as a game programmer to decide which effects you wish to include in your simulation. Most forces can be simulated with additional force vectors acting on the model, others require changing of established parameters like friction.