Creating a Car with Suspension Joints
This example shows how to:
- Create a simple car with a frame and wheels attached to it using suspension joints.
- Enable car movement using joint motors.
- Use keyboard to control car movement.
The basic workflow of creating a simple car with keyboard control is as follows:
- Create car geometry: frame and wheels.
- Assign rigid bodies and collision shapes to the frame and all the wheels.
- Set up masses for the wheels and the frame.Do not use real masses (e.g. 2000 kg for the frame and 10 kg for the wheels), as joints may become unstable! It might be better to use 64 kg for the body and 25 kg for each wheel to provide realistic behavior.
- Connect the wheels to the frame using suspension joints. Set up joint parameters.
- Enable car movement using joint motors and assign movement control to corresponding keyboard keys.
Creating Geometry and Adding Some Physics
The first thing we are going to address in this tutorial is the geometry of our car. We are going to create a rectangular car frame and four wheels. We are also going to add some physical parameters to the geometry. So, we need two functions:
- The first one to create a box with specified parameters representing the car frame:
/// method, creating a named box having a specified size, color and transformation with a body and a shape
ObjectMeshDynamic createBodyBox(String name, vec3 size, float mass, vec4 color, dmat4 transform)
{
// creating geometry and setting up its parameters (name, material, property and transformation)
ObjectMeshDynamic OMD = Primitives.createBox(size);
OMD.setWorldTransform(transform);
OMD.setMaterial("mesh_base", "*");
OMD.setMaterialParameter("albedo_color", color, 0);
OMD.setProperty("surface_base", "*");
OMD.setName(name);
// adding physics, i.e. a rigid body and a box shape with specified mass
BodyRigid body = new BodyRigid(OMD.getObject());
body.addShape(new ShapeBox(size).getShape(), MathLib.translate(new vec3(0.0f)));
OMD.getBody().getShape(0).setMass(mass);
return OMD;
}
- The second one to create a cylinder with specified parameters representing a wheel:
/// method, creating a named cylinder having a specified size, color and transformation with a body and a shape
ObjectMeshDynamic createBodyCylinder(String name, float radius, float height, float mass, vec4 color, dmat4 transform)
{
// creating geometry and setting up its parameters (name, material, property and transformation)
ObjectMeshDynamic OMD = Primitives.createCylinder(radius, height, 1, 32);
OMD.setWorldTransform(transform);
OMD.setMaterial("mesh_base", "*");
OMD.setMaterialParameter("albedo_color", color, 0);
OMD.setProperty("surface_base", "*");
OMD.setName(name);
// adding physics, i.e. a rigid body and a cylinder shape with specified mass
BodyRigid body = new BodyRigid(OMD.getObject());
body.addShape(new ShapeCylinder(radius, height).getShape(), MathLib.translate(new vec3(0.0f)));
OMD.getBody().getShape(0).setMass(mass);
return OMD;
}
Now, using these functions we can create our car. We are going to use DynamicMesh objects for the frame and four wheels. For a proper mass balance let us set frame mass equal to 64 kg and the mass of each wheel - to 25 kg.
// defining DynamicMesh objects for the frame and four wheels
ObjectMeshDynamic car_frame;
ObjectMeshDynamic[] wheels = new ObjectMeshDynamic[4];
// creating car frame
car_frame = createBodyBox("car_frame", new vec3(frame_length, frame_width, frame_height), 64.0f, new vec4(1.0f, 0.1f, 0.1f, 1.0f), transform);
// creating car wheels
// front left wheel
wheels[0] = createBodyCylinder("car_wheel_f_l", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * dmat4(translate(-frame_length / 2 + wheel_radius, -(frame_width + wheel_width) / 2 - delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
// front right wheel
wheels[1] = createBodyCylinder("car_wheel_f_r", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * dmat4(translate(-frame_length / 2 + wheel_radius, (frame_width + wheel_width) / 2 + delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
// rear left wheel
wheels[2] = createBodyCylinder("car_wheel_r_l", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * dmat4(translate(0.25f * frame_length, -(frame_width + wheel_width) / 2 - delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
// rear left wheel
wheels[3] = createBodyCylinder("car_wheel_r_r", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * dmat4(translate(0.25f * frame_length, (frame_width + wheel_width) / 2 + delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
Adding and Setting Up Joints
Now that we have the frame and four wheels we can attach each wheel to the frame with a suspension joint.
To create a suspension joint for a wheel using a JointSuspension() constructor we must specify the following parameters:
- Rigid body of the car frame
- Rigid body of the wheel
- Anchor point coordinates (this point is determined by the position(translation) of the wheel relative to the frame)
To extract wheel translation we can multiply wheel transformation matrix by a vec3 zero-vector.
- Coordinates of suspension axis (a vertical axis along which a wheel moves vertically and rotates when steering)
- Coordinates of wheel spindle axis (a horizontal around which a wheel rotates when moving forward or backward)
for (int i = 0; i < 4; i++)
{
suspension[i] = new JointSuspension(car_frame.getBody(), wheels[i].getBody(), new dvec3(wheels[i].getTransform() * new dvec3(0.0f)), new vec3(0.0f, 0.0f, 1.0f), new vec3(0.0f, 1.0f, 0.0f));
// setting restitution parameters
suspension[i].setLinearRestitution(0.1f);
suspension[i].setAngularRestitution(0.1f);
// setting linear damping and spring rigidity
suspension[i].setLinearDamping(2.0f);
suspension[i].setLinearSpring(40.0f);
// setting lower and upper suspension ride limits [-1.0; 0.0]
suspension[i].setLinearLimitFrom(-1.0f);
suspension[i].setLinearLimitTo(0.0f);
// setting number of iterations
suspension[i].setNumIterations(8);
}
Using Joint Motors
So, we have a car, let us make it move now.
To move the car forward or backward we are going to use joint motors of the rear wheels (2 and 3):
- setting a positive velocity value will move our car forward.
- setting a negative velocity value will move our car backward.
suspension[2].setAngularVelocity(velocity);
suspension[3].setAngularVelocity(velocity);
suspension[2].setAngularTorque(torque);
suspension[3].setAngularTorque(torque);
To steer the car left or right we are going to change Axis10 coordinates of the front wheels (0 and 1) using setAxis10() method:
suspension[0].setAxis10(MathLib.rotateZ(angle_0) * new vec3(0.0f, 1.0f, 0.0f));
suspension[1].setAxis10(MathLib.rotateZ(angle_1) * new vec3(0.0f, 1.0f, 0.0f));
To stop the car we are going to set a high angular damping value for all wheels via setAngularDamping() method:
suspension[0].setAngularDamping(20000.0f);
suspension[1].setAngularDamping(20000.0f);
suspension[2].setAngularDamping(20000.0f);
suspension[3].setAngularDamping(20000.0f);
Adding Keyboard Controls
To add keyboard control we are going to make a handler for the keys that we need. The handler must be put to the world script update() function to be called for each frame. We are going to update car physics (joint motors) in the flush() method.
int update()
{
// forward and backward movement by setting joint motor's velocity and torque
if ((controls.getState(Controls.STATE_FORWARD) == 1) || (controls.getState(Controls.STATE_TURN_UP) == 1))
{
// TODO: increase velocity by delta
// set desired torque
}
else if ((controls.getState(Controls.STATE_BACKWARD) == 1) || (controls.getState(Controls.STATE_TURN_DOWN) == 1))
{
// TODO: decrease velocity by delta
// set desired torque
}
else {
// TODO: decrease velocity gradually
}
// clamp velocity value
velocity = MathLib.clamp(velocity, -90.0f, 90.0f);
// steering left and right by changing Axis01 for front wheel joints
if ((controls.getState(Controls.STATE_MOVE_LEFT) == 1) || (controls.getState(Controls.STATE_TURN_LEFT) == 1))
{
// TODO: increase steering angle by some delta
}
else if ((controls.getState(Controls.STATE_MOVE_RIGHT) == 1)|| (controls.getState(Controls.STATE_TURN_RIGHT) == 1))
{
// TODO: decrease steering angle
}
else {
// TODO: return steering angle to zero value gradually
}
// clamp steering angle value
angle = MathLib.clamp(angle,-30.0f,30.0f);
// TODO: calculate steering angles for front joints (angle_0 and angle_1)
// set new Axis10 coordinates for front joints
suspension[0].setAxis10(MathLib.rotateZ(angle_0) * new vec3(0.0f, 1.0f, 0.0f));
suspension[1].setAxis10(MathLib.rotateZ(angle_1) * new vec3(0.0f, 1.0f, 0.0f));
if (controls.getState(Controls.STATE_USE) == 1)
{
// TODO: set a very high angular damping value for all joints
// set velocity to zero
}
else
{
// TODO: set angular damping value to zero for all joints
}
return 1;
}
int flush()
{
// set angular velocity for rear joints
suspension[2].setAngularVelocity(velocity);
suspension[3].setAngularVelocity(velocity);
// set torque for rear joints
suspension[2].setAngularTorque(torque);
suspension[3].setAngularTorque(torque);
return 1;
}
Putting it All Together
In this section let us sum up all described above and create a new class for our car. The final code for our tutorial will be as follows:
Add a new empty Car.cs file to the project, and insert the following code:
// Car.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unigine;
namespace UnigineApp
{
class Car
{
// car parameters
float frame_width;
float frame_height;
float frame_length;
float wheel_radius;
float wheel_width;
// movement parameters
float angle = 0.0f;
float velocity = 0.0f;
float torque = 0.0f;
// car elements
ObjectMeshDynamic car_frame;
ObjectMeshDynamic[] wheels = new ObjectMeshDynamic[4];
JointSuspension[] suspension = new JointSuspension[4];
Controls controls;
/// method, creating a named box having a specified size, color and transformation with a body and a shape
ObjectMeshDynamic createBodyBox(String name, vec3 size, float mass, vec4 color, dmat4 transform)
{
// creating geometry and setting up its parameters (name, material, property and transformation)
ObjectMeshDynamic OMD = Primitives.createBox(size);
OMD.setWorldTransform(transform);
OMD.setMaterial("mesh_base", "*");
OMD.setMaterialParameter("albedo_color", color, 0);
OMD.setProperty("surface_base", "*");
OMD.setName(name);
// adding physics, i.e. a rigid body and a box shape with specified mass
BodyRigid body = new BodyRigid(OMD.getObject());
body.addShape(new ShapeBox(size).getShape(), MathLib.translate(new vec3(0.0f)));
OMD.getBody().getShape(0).setMass(mass);
return OMD;
}
/// method, creating a named cylinder having a specified size, color and transformation with a body and a shape
ObjectMeshDynamic createBodyCylinder(String name, float radius, float height, float mass, vec4 color, dmat4 transform)
{
// creating geometry and setting up its parameters (name, material, property and transformation)
ObjectMeshDynamic OMD = Primitives.createCylinder(radius, height, 1, 32);
OMD.setWorldTransform(transform);
OMD.setMaterial("mesh_base", "*");
OMD.setMaterialParameter("albedo_color", color, 0);
OMD.setProperty("surface_base", "*");
OMD.setName(name);
// adding physics, i.e. a rigid body and a cylinder shape with specified mass
BodyRigid body = new BodyRigid(OMD.getObject());
body.addShape(new ShapeCylinder(radius, height).getShape(), MathLib.translate(new vec3(0.0f)));
OMD.getBody().getShape(0).setMass(mass);
return OMD;
}
/// Initializing a car with specified frame and wheel parameters
public int init(float blength, float bwidth, float bheight, float wradius, float wwidth, dmat4 transform)
{
frame_width = bwidth;
frame_height = bheight;
frame_length = blength;
wheel_radius = wradius;
wheel_width = wwidth;
float delta = 0.2f;
car_frame = createBodyBox("car_frame", new vec3(frame_length, frame_width, frame_height), 64.0f, new vec4(1.0f, 0.1f, 0.1f, 1.0f), transform);
// initialization of wheels
wheels[0] = createBodyCylinder("car_wheel_f_l", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * new dmat4(MathLib.translate(-frame_length / 2 + wheel_radius, -(frame_width + wheel_width) / 2 - delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
wheels[1] = createBodyCylinder("car_wheel_f_r", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * new dmat4(MathLib.translate(-frame_length / 2 + wheel_radius, (frame_width + wheel_width) / 2 + delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
wheels[2] = createBodyCylinder("car_wheel_r_l", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * new dmat4(MathLib.translate(0.25f * frame_length, -(frame_width + wheel_width) / 2 - delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
wheels[3] = createBodyCylinder("car_wheel_r_r", wheel_radius, wheel_width, 25.0f, new vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * new dmat4(MathLib.translate(0.25f * frame_length, (frame_width + wheel_width) / 2 + delta, -frame_height / 2) * MathLib.rotateX(90.0f)));
// initialization of suspension joints
for (int i = 0; i < 4; i++)
{
suspension[i] = new JointSuspension(car_frame.getBody(), wheels[i].getBody(), new dvec3(wheels[i].getTransform() * new dvec3(0.0f)), new vec3(0.0f, 0.0f, 1.0f), new vec3(0.0f, 1.0f, 0.0f));
// setting restitution parameters
suspension[i].setLinearRestitution(0.1f);
suspension[i].setAngularRestitution(0.1f);
// setting linear damping and spring rigidity
suspension[i].setLinearDamping(2.0f);
suspension[i].setLinearSpring(40.0f);
// setting lower and upper suspension ride limits [-1.0; 0.0]
suspension[i].setLinearLimitFrom(-1.0f);
suspension[i].setLinearLimitTo(0.0f);
// setting number of iterations
suspension[i].setNumIterations(8);
}
// setting up player and controls
PlayerPersecutor player = new PlayerPersecutor();
player.release();
player.setFixed(1);
player.setTarget(car_frame.getNode());
player.setMinDistance(6.0f);
player.setMaxDistance(11.0f);
player.setPosition(new dvec3(10.0f, 0.0f, 6.0f));
player.setControlled(0);
Game.get().setPlayer(player.getPlayer());
Game.get().setEnabled(1);
controls = player.getControls();
return 1;
}
/// method updating current car state with a keyboard control handler
public int update()
{
float ifps = Game.get().getIFps();
// forward and backward movement by setting joint motor's velocity and torque
if ((controls.getState(Controls.STATE_FORWARD) == 1) || (controls.getState(Controls.STATE_TURN_UP) == 1))
{
velocity = MathLib.max(velocity, 0.0f);
velocity += ifps * 50.0f;
torque = 5.0f;
}
else if ((controls.getState(Controls.STATE_BACKWARD) == 1) || (controls.getState(Controls.STATE_TURN_DOWN) == 1))
{
velocity = MathLib.min(velocity, 0.0f);
velocity -= ifps * 50.0f;
torque = 5.0f;
}
else
{
velocity *= MathLib.exp(-ifps);
}
velocity = MathLib.clamp(velocity, -90.0f, 90.0f);
// steering left and right by changing Axis01 for front wheel joints
if ((controls.getState(Controls.STATE_MOVE_LEFT) == 1) || (controls.getState(Controls.STATE_TURN_LEFT) == 1))
angle += ifps * 100.0f;
else if ((controls.getState(Controls.STATE_MOVE_RIGHT) == 1)|| (controls.getState(Controls.STATE_TURN_RIGHT) == 1))
angle -= ifps * 100.0f;
else
{
if (MathLib.abs(angle) < 0.25f) angle = 0.0f;
else angle -= MathLib.sign(angle) * ifps * 45.0f;
}
angle = MathLib.clamp(angle, -30.0f, 30.0f);
// calculating steering angles for front joints (angle_0 and angle_1)
float base_a = 3.3f;
float width = 3.0f;
float angle_0 = angle;
float angle_1 = angle;
if (MathLib.abs(angle) > MathLib.EPSILON)
{
float radius = base_a / MathLib.tan(angle * MathLib.DEG2RAD);
angle_0 = MathLib.atan(base_a / (radius + width / 2.0f)) * MathLib.RAD2DEG;
angle_1 = MathLib.atan(base_a / (radius - width / 2.0f)) * MathLib.RAD2DEG;
}
suspension[0].setAxis10(MathLib.rotateZ(angle_0) * new vec3(0.0f, 1.0f, 0.0f));
suspension[1].setAxis10(MathLib.rotateZ(angle_1) * new vec3(0.0f, 1.0f, 0.0f));
// enabling or disabling a brake
if (controls.getState(Controls.STATE_USE) == 1)
{
suspension[0].setAngularDamping(20000.0f);
suspension[1].setAngularDamping(20000.0f);
suspension[2].setAngularDamping(20000.0f);
suspension[3].setAngularDamping(20000.0f);
velocity = 0.0f;
}
else
{
suspension[0].setAngularDamping(0.0f);
suspension[1].setAngularDamping(0.0f);
suspension[2].setAngularDamping(0.0f);
suspension[3].setAngularDamping(0.0f);
}
return 1;
}
/// method updating car physics
public int flush()
{
// set angular velocity for rear joints
suspension[2].setAngularVelocity(velocity);
suspension[3].setAngularVelocity(velocity);
// set torque for rear joints
suspension[2].setAngularTorque(torque);
suspension[3].setAngularTorque(torque);
return 1;
}
}
}
Insert the following code into the AppWorldLogic.cs file.
// AppWorldLogic.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unigine;
namespace UnigineApp
{
class AppWorldLogic : WorldLogic
{
Car car;
/* .. */
public override int init()
{
// setting up physics parameters
Physics.get().setGravity(new vec3(0.0f, 0.0f, -9.8f * 2.0f));
Physics.get().setFrozenLinearVelocity(0.1f);
Physics.get().setFrozenAngularVelocity(0.1f);
// creating and initializing our car
car = new Car();
car.init(4.0f, 2.0f, 0.5f, 0.5f, 0.5f, MathLib.translate(new dvec3(0.0f, 0.0f, 1.0f)));
return 1;
}
// start of the main loop
public override int update()
{
//updating our car
car.update();
return 1;
}
public override int flush()
{
// updating car physics
car.flush();
return 1;
}
/* .. */
}
}