Programming
Fundamentals
Setting Up Development Environment
UnigineScript
High-Level Systems
C++
C#
UUSL (Unified UNIGINE Shader Language)
File Formats
Rebuilding the Engine and Tools
GUI
Double Precision Coordinates
API
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
Rendering-Related Classes

Creating a Car with Suspension Joints

A Simple Car with Suspension Joints

A Simple Car with Suspension Joints

This example shows how to:

The basic workflow of creating a simple car with keyboard control is as follows:

  1. Create car geometry: frame and wheels.
  2. Assign rigid bodies and collision shapes to the frame and all the wheels.
  3. Set up masses for the wheels and the frame.
    Notice
    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.
  4. Connect the wheels to the frame using suspension joints. Set up joint parameters.
  5. 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:
    Source code (UnigineScript)
    /// function creating a named box having a specified size, color and transformation with a body and a shape
    Object createBodyBox(string name, Vec3 size, float mass, Vec4 color, Mat4 transform)
    {
    	// creating geometry and setting up its parameters (name, material, property and transformation)
    	ObjectMeshDynamic box = Unigine::createBox(size);
    	engine.editor.addNode(node_remove(box));
    	
    	box.setWorldTransform(transform);
    	box.setMaterial("mesh_base", "*");
    	box.setMaterialParameter("albedo_color", color, 0);
    	box.setProperty("surface_base", "*");
    	box.setName(name);
    
    	// adding physics, i.e. a rigid body and a box shape with specified mass
    	BodyRigid body = class_remove(new BodyRigid(box));
    	ShapeBox shape = class_remove(new ShapeBox(size));
    
        shape.setMass(mass);
        body.addShape(shape, translate(0.0f, 0.0f, 0.0f));
    
    	body.setFreezable(0);
    
    	return box;
    }
  • The second one to create a cylinder with specified parameters representing a wheel:
    Source code (UnigineScript)
    /// function creating a named cylinder having a specified radius, height, color and transformation with a body and a shape	
    Object createBodyCylinder(string name, float radius, float height, float mass, Vec4 color, Mat4 transform)
    {
    	// creating geometry and setting up its parameters (name, material, property and transformation)
    	ObjectMeshDynamic cylinder = Unigine::createCylinder(radius, height, 1, 32);
    	engine.editor.addNode(node_remove(cylinder));
    	
    	cylinder.setWorldTransform(transform);
    	cylinder.setMaterial("mesh_base", "*");
    	cylinder.setMaterialParameter("albedo_color", color, 0);
    	cylinder.setProperty("surface_base", "*");
    	cylinder.setName(name);
    
    	// adding physics, i.e. a rigid body and a cylinder shape with specified mass	
    	BodyRigid body = class_remove(new BodyRigid(cylinder));
    	ShapeCylinder shape = class_remove(new ShapeCylinder(radius, height));
    
        shape.setMass(mass);
    	body.addShape(shape, translate(0.0f, 0.0f, 0.0f));
    
    	return cylinder;
    }

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.

Source code (UnigineScript)
// defining DynamicMesh objects for the frame and four wheels
	ObjectMeshDynamic car_frame;
	ObjectMeshDynamic wheels[4];			

		// creating car frame
		car_frame = createBodyBox("car_frame", Vec3(frame_length, frame_width, frame_height), 64.0f, 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, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(- frame_length/2 + wheel_radius, -(frame_width + wheel_width)/2 - delta,   -frame_height/2) * rotateX(90.0f));

		// front right wheel
		wheels[1] = createBodyCylinder("car_wheel_f_r", wheel_radius, wheel_width, 25.0f, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(- frame_length/2 + wheel_radius,  (frame_width + wheel_width)/2 + delta,   -frame_height/2) * rotateX(90.0f));

		// rear left wheel
		wheels[2] = createBodyCylinder("car_wheel_r_l", wheel_radius, wheel_width, 25.0f, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(0.25f * frame_length,         -(frame_width + wheel_width)/2 - delta,   -frame_height/2) * rotateX(90.0f));

		// rear left wheel
		wheels[3] = createBodyCylinder("car_wheel_r_r", wheel_radius, wheel_width, 25.0f, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(0.25f * frame_length, 		  (frame_width + wheel_width)/2 + delta,	  -frame_height/2) * 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:

  1. Rigid body of the car frame
  2. Rigid body of the wheel
  3. Anchor point coordinates (this point is determined by the position(translation) of the wheel relative to the frame)
    Notice

    To extract wheel translation we can multiply wheel transformation matrix by a vec3 zero-vector.

    See Matrix Transformations.

  4. Coordinates of suspension axis (a vertical axis along which a wheel moves vertically and rotates when steering)
  5. Coordinates of wheel spindle axis (a horizontal around which a wheel rotates when moving forward or backward)
Source code (UnigineScript)
for(int i = 0; i < 4; i++)
{
	suspension[i] = class_remove(new JointSuspension(car_frame.getBodyRigid(), // car frame body
													wheels[i].getBodyRigid(), // car frame body
													wheels[i].getTransform() * Vec3_zero, // anchor coordinates
													vec3(0.0f,0.0f,1.0f), // suspension axis
													vec3(0.0f,1.0f,0.0f))); // wheel spindle axis
	// 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.
See the following code:
Source code (UnigineScript)
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:

Source code (UnigineScript)
suspension[0].setAxis10(rotateZ(angle_0) * vec3(0.0f,1.0f,0.0f));
suspension[1].setAxis10(rotateZ(angle_1) * 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:

Source code (UnigineScript)
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.

Source code (UnigineScript)
int update()
{	
		// forward and backward movement by setting joint motor's velocity and torque
		if(engine.controls.getState(CONTROLS_STATE_FORWARD) || engine.controls.getState(CONTROLS_STATE_TURN_UP)) {
			// TODO: increase velocity by delta
			// set desired torque
		} else if(engine.controls.getState(CONTROLS_STATE_BACKWARD) || engine.controls.getState(CONTROLS_STATE_TURN_DOWN)) {
			// TODO: decrease velocity by delta
			// set desired torque
		} else {
			// TODO: decrease velocity gradually
		}
		
		// clamp velocity value
		velocity = clamp(velocity,-90.0f,90.0f);
		
		// 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);

		// steering left and right by changing Axis01 for front wheel joints
		if(engine.controls.getState(CONTROLS_STATE_MOVE_LEFT) || engine.controls.getState(CONTROLS_STATE_TURN_LEFT)) {
			// TODO: increase steering angle by some delta
		} else if(engine.controls.getState(CONTROLS_STATE_MOVE_RIGHT) || engine.controls.getState(CONTROLS_STATE_TURN_RIGHT)) {
			// TODO: decrease steering angle
		} else {
			// TODO: return steering angle to zero value gradually
		}

		// clamp steering angle value
		angle = 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(rotateZ(angle_0) * vec3(0.0f,1.0f,0.0f));
		suspension[1].setAxis10(rotateZ(angle_1) * vec3(0.0f,1.0f,0.0f));
		
		if(engine.controls.getState(CONTROLS_STATE_USE)) {
			// 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
		}
	}			
}

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:

Source code (UnigineScript)
#include <core/scripts/primitives.h>
Car car = NULL;

/// function, creating a named box having a specified size, color and transformation with a body and a shape
Object createBodyBox(string name, Vec3 size, float mass, Vec4 color, Mat4 transform)
{
	// creating geometry and setting up its parameters (name, material, property and transformation)
	ObjectMeshDynamic box = Unigine::createBox(size);
	engine.editor.addNode(node_remove(box));
	
	box.setWorldTransform(transform);
	box.setMaterial("mesh_base", "*");
	box.setMaterialParameter("albedo_color", color, 0);
	box.setProperty("surface_base", "*");
	box.setName(name);

	// adding physics, i.e. a rigid body and a box shape with specified mass
	BodyRigid body = class_remove(new BodyRigid(box));
	ShapeBox shape = class_remove(new ShapeBox(size));

    shape.setMass(mass);
    body.addShape(shape, translate(0.0f, 0.0f, 0.0f));

	body.setFreezable(0);

	return box;
}

/// function, creating a named cylinder having a specified radius, height, color and transformation with a body and a shape	
Object createBodyCylinder(string name, float radius, float height, float mass, Vec4 color, Mat4 transform)
{
	// creating geometry and setting up its parameters (name, material, property and transformation)
	ObjectMeshDynamic cylinder = Unigine::createCylinder(radius, height, 1, 32);
	engine.editor.addNode(node_remove(cylinder));
	
	cylinder.setWorldTransform(transform);
	cylinder.setMaterial("mesh_base", "*");
	cylinder.setMaterialParameter("albedo_color", color, 0);
	cylinder.setProperty("surface_base", "*");
	cylinder.setName(name);

	// adding physics, i.e. a rigid body and a cylinder shape with specified mass	
	BodyRigid body = class_remove(new BodyRigid(cylinder));
	ShapeCylinder shape = class_remove(new ShapeCylinder(radius, height));

    shape.setMass(mass);
	body.addShape(shape, translate(0.0f, 0.0f, 0.0f));

	return cylinder;
}

/// Car class definition
class Car
{
	// car parameters
	float frame_width;
	float frame_height;
	float frame_length;
	float wheel_radius;
	float wheel_width;

	// car elements
	ObjectMeshDynamic car_frame;
	ObjectMeshDynamic wheels[4];
	JointSuspension suspension[4];

	// initialization of movement parameters
	float angle = 0.0f;
	float velocity = 0.0f;

	/// Car constructor creating a car with specified frame and wheel parameters
	Car(float blength, float bwidth, float bheight, float wradius, float wwidth, Mat4 transform)
	{
		//position = pos;
		frame_width = bwidth;
		frame_height = bheight;
		frame_length = blength;
		wheel_radius = wradius;
		wheel_width = wwidth;
		float delta = 0.2f;

		car_frame = createBodyBox("car_frame", Vec3(frame_length, frame_width, frame_height), 64.0f, 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, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(- frame_length/2 + wheel_radius, -(frame_width + wheel_width)/2 - delta,   -frame_height/2) * rotateX(90.0f));
		wheels[1] = createBodyCylinder("car_wheel_f_r", wheel_radius, wheel_width, 25.0f, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(- frame_length/2 + wheel_radius,  (frame_width + wheel_width)/2 + delta,   -frame_height/2) * rotateX(90.0f));
		wheels[2] = createBodyCylinder("car_wheel_r_l", wheel_radius, wheel_width, 25.0f, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(0.25f * frame_length,         -(frame_width + wheel_width)/2 - delta,   -frame_height/2) * rotateX(90.0f));
		wheels[3] = createBodyCylinder("car_wheel_r_r", wheel_radius, wheel_width, 25.0f, Vec4(0.1f, 0.1f, 0.1f, 1.0f), transform * translate(0.25f * frame_length, 		  (frame_width + wheel_width)/2 + delta,	  -frame_height/2) * rotateX(90.0f));

		// initialization of suspension joints
		for(int i=0; i < 4; i++)
		{
			suspension[i] = class_remove(new JointSuspension(car_frame.getBodyRigid(), wheels[i].getBodyRigid(), wheels[i].getTransform() * Vec3_zero, vec3(0.0f, 0.0f, 1.0f), 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);
		}
	
	}

	/// method updating current car state with a keyboard control handler
	void update()
	{
		float ifps = engine.game.getIFps();
		
		// forward and backward movement by setting joint motor's velocity and torque
		float torque = 0.0f;
		if(engine.controls.getState(CONTROLS_STATE_FORWARD) || engine.controls.getState(CONTROLS_STATE_TURN_UP)) {
			velocity = max(velocity,0);
			velocity += ifps * 50.0f;
			torque = 5.0f;
		} else if(engine.controls.getState(CONTROLS_STATE_BACKWARD) || engine.controls.getState(CONTROLS_STATE_TURN_DOWN)) {
			velocity = min(velocity,0);
			velocity -= ifps * 50.0f;
			torque = 5.0f;
		} else {
			velocity *= exp(-ifps);
		}
		velocity = clamp(velocity,-90.0f,90.0f);

		suspension[2].setAngularVelocity(velocity);
		suspension[3].setAngularVelocity(velocity);
		
		suspension[2].setAngularTorque(torque);
		suspension[3].setAngularTorque(torque);

		// steering left and right by changing Axis01 for front wheel joints
		if(engine.controls.getState(CONTROLS_STATE_MOVE_LEFT) || engine.controls.getState(CONTROLS_STATE_TURN_LEFT)) {
			angle += ifps * 100.0f;
		} else if(engine.controls.getState(CONTROLS_STATE_MOVE_RIGHT) || engine.controls.getState(CONTROLS_STATE_TURN_RIGHT)) {
			angle -= ifps * 100.0f;
		} else {
			if(abs(angle) < 0.25f) angle = 0.0f;
			else angle -= sign(angle) * ifps * 45.0f;
		}
		angle = clamp(angle,-30.0f,30.0f);
		
		// calculating steering angles for front joints (angle_0 and angle_1) 
		float base = 3.3f;
		float width = 3.0f;
		float angle_0 = angle;
		float angle_1 = angle;
		if(abs(angle) > EPSILON) {
			float radius = base / tan(angle * DEG2RAD);
			angle_0 = atan(base / (radius + width / 2.0f)) * RAD2DEG;
			angle_1 = atan(base / (radius - width / 2.0f)) * RAD2DEG;
		}
		
		suspension[0].setAxis10(rotateZ(angle_0) * vec3(0.0f, 1.0f, 0.0f));
		suspension[1].setAxis10(rotateZ(angle_1) * vec3(0.0f, 1.0f, 0.0f));
		
		// enabling or disabling a brake
		if(engine.controls.getState(CONTROLS_STATE_USE)) {
			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);
		}
	}

};

int init() {
	
	// setting up physics parameters
	engine.physics.setGravity(vec3(0.0f,0.0f,-9.8f * 2.0f));
	engine.physics.setFrozenLinearVelocity(0.1f);
	engine.physics.setFrozenAngularVelocity(0.1f);

	//creating a car with specified parameters
	car = new Car(4.0f, 2.0f, 0.5f, 0.5f, 0.5f, translate(Vec3(0.0f, 0.0f, 1.0f)));
	
	// setting up player and controls
	PlayerPersecutor player = new PlayerPersecutor();
	player.setFixed(1);
	player.setTarget(car.car_frame);
	player.setMinDistance(6.0f);
	player.setMaxDistance(11.0f);
	player.setPosition(Vec3(10.0f, 0.0f, 6.0f));
	player.setControlled(0);
	engine.game.setPlayer(player);

	return 1;
}

int update() {

	// updating current car state
	car.update();

	return 1;
}
Last update: 2017-07-03