This page has been translated automatically.
Video Tutorials
Interface
Essentials
Advanced
How To
UnigineEditor
Interface Overview
Assets Workflow
Settings and Preferences
Working With Projects
Adjusting Node Parameters
Setting Up Materials
Setting Up Properties
Lighting
Landscape Tool
Sandworm
Using Editor Tools for Specific Tasks
Extending Editor Functionality
Built-in Node Types
Nodes
Objects
Effects
Decals
Light Sources
Geodetics
World Nodes
Sound Objects
Pathfinding Objects
Players
Programming
Fundamentals
Setting Up Development Environment
C++
C#
UnigineScript
UUSL (Unified UNIGINE Shader Language)
Plugins
File Formats
Rebuilding the Engine 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
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
IG Plugin
CIGIConnector Plugin
Rendering-Related Classes
Content Creation
Content Optimization
Materials
Material Nodes Library
Art Samples
Tutorials

Creating a Car with Wheel Joints

Car with wheel joints

Car with wheel joints

This example shows how to:

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

  1. Create the car visual geometry: frame and wheels.
  2. Set up a third-person camera that follows the car and orbits around it.
  3. Assign rigid bodies and collision shapes to the wheels and the frame. Set the appropriate masses for them.
  4. 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.
  5. Connect the wheels to the frame using wheel joints. Set up joint parameters.
  6. Enable the car movement using joint motors by setting up movement controls with corresponding keyboard keys.

Step 1. Importing Vehicle Geometry#

The first thing we are going to address in this tutorial is the geometry of our car. It is possible to use any other custom made model for the vehicle appearance. Every wheel and the frame will be individual independent nodes in the world, so make sure that your vehicle model includes separate meshes for the wheels and the frame.

Right now we are going to take the vehicle model from the content addon included in the SDK and then add some physical parameters to the geometry to achieve a viable driving simulation.

  1. Let's create an empty C++ project.
  2. Install the Eastern European Countryside add-on (available via UNIGINE SDK Browser in the Add-Ons section) and add it to your project by clicking Other Actions -> Configure Project -> Add-ons in the My Projects section of UNIGINE SDK Browser. Drag it into the In Use field and click OK.

  3. Launch the Editor via the Open Editor button. For your vehicle you can choose any of the minibus nodes located in data/countryside/minibus or import your own model in any supported file format.

    A minibus vehicle model has the body frame as the parent node and its wheels as child nodes.

  4. In the Asset Browser, right-click on a minibus node you like the most and choose Place as Node Content. Place the vehicle on the ground in the world. As we don't plan to use multiple instances of the car, we simply put all its parts directly to the scene, without the NodeReference container.

Step 2. Setting Up Camera#

Let’s create a camera for a third-person view. While the camera follows the car, the player can control the viewing direction (rotate the camera) with the mouse. To achieve this, perform the following:

  1. In the World Nodes window delete the first_person_controller containing the default camera.
  2. Create a custom Persecutor Camera to follow the car. Go to Create -> Camera -> Persecutor and then place it near the vehicle.
  3. Check the Main Player option in the Parameters tab of the PlayerPersecutor to make it the default camera in the world.
  4. Delete the default Persecutor_Target node from the World Nodes window hierarchy and set our minibus as the target node for the PlayerPersecutor camera instead.

  5. To put the camera closer to the vehicle, set the Max Distance parameter for the PlayerPersecutor to 8.

Step 3. Adding Some Physics#

In order to give our car some physical properties, we must assign physical bodies and approximating volumetric shapes to its frame and wheels. To achieve this, do the following:

Notice
Do not change the scale of the frame or wheel nodes, since the physics simulation does not support it. The scale of all nodes should be equal to 1 along all axes, to ensure correct physics simulation.
  1. Go to the Parameters tab of the minibus frame (parent node) and in the Physics tab assign a physical rigid body by selecting Body -> Rigid. Also, add a box shape to the body (Shape -> Box).

  2. Set the mass of ShapeBox to 100.

  3. Disable the Shape-Based option for the frame’s rigid body to turn off automatic calculation and customize mass distribution of the vehicle. Set the third component of the CMass parameter to 0.25 effectively lowering the center of mass.

  4. For each of the wheels (child nodes), go to the Parameters tab and in the Physics section assign a rigid body with a cylinder shape that roughly approximates the shape of the wheel mesh. You may need to rotate ShapeCylinder and adjust its radius and height to position it properly (see the image below).

    Notice
    In the World Nodes window, you can select multiple wheel nodes at the same time by holding Shift key and selecting the first and the last one. This way you will be able to add bodies and shapes to all wheels simultaneously.

Step 4. Setting Up Joints#

For the wheels to be connected to the frame and behave realistically, we must connect them using wheel joints. These joints not only remove degrees of freedom providing motion constraints, but also simulate vehicle suspension and motor torque for wheels.

We need to connect the body of every wheel to the frame’s body using wheel joints.

  1. To do so select the frame of the minibus (parent node) in the World Nodes hierarchy window and go to the Joints section of the Physics tab. There you must specify the Wheel type for the joint and click the Add button.

  2. You shall see the following dialog window. Connect the frame body to every wheel’s body by selecting every wheel one by one. The specified rigid body will be connected to the rigid body of the car using a wheel joint.

You will end up with four wheel joints connecting corresponding wheels’ bodies to the frame’s body (you may name them accordingly).

To configure the setup, do the following for every wheel joint (select them one by one and tweak settings below):

  1. Press Fix 0 button to fix Anchor 0 point to the wheel position. This will place the joint in the correct position for this wheel.

  2. Set the number of joint solving Iterations to 16 making the physical simulation more stable.

    Notice
    You can select multiple joints at the same time by holding Shift key and selecting the first and the last one. This way you will be able to set the same settings to all joints simultaneously.
  3. Next, set the following values to the joint’s parameters to achieve a plausible simulation behavior.

  4. Wheel joints are based on physics raycasting. To make them interact with the ground properly, enable Physics Intersection for the surface of the ground node in the Parameters tab.

Step 5. Applying Velocity to Wheels#

Now let’s create a C++ component to implement driving and steering functionality for the car. To achieve this, do the following:

  1. To start writing code we should open our project in an IDE. Go to SDK Browser and choose the Open Code IDE button.

  2. In an IDE create a new C++ class (*.h and *.cpp files) and call it Car. Make sure it inherits from the Unigine::ComponentBase class.
  3. Get references to joints from the wheel nodes and define variables for the velocity, acceleration and torque values.

    Car.h (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    
    #include <UniginePhysics.h>
    #include <UniginePlayers.h>
    #include <UnigineControls.h>
    #include <UnigineGame.h>
    #include <UnigineMathLib.h>
    
    
    class Car : public Unigine::ComponentBase
    {
    	public:
    		// declare constructor and destructor for our class and define a property name
    		COMPONENT_DEFINE(Car, ComponentBase)
    		// declare methods to be called at the corresponding stages of the execution sequence
    		COMPONENT_INIT(init);
    		COMPONENT_UPDATE(update);
    		COMPONENT_UPDATE_PHYSICS(updatePhysics);
    
    		// driving parameters
    		PROP_PARAM(Float, acceleration, 50.0f);
    		PROP_PARAM(Float, max_velocity, 90.0f);
    		PROP_PARAM(Float, default_torque, 5.0f);
    
    		// wheel joints
    		PROP_PARAM(Node, wheel_bl);
    		PROP_PARAM(Node, wheel_br);
    		PROP_PARAM(Node, wheel_fl);
    		PROP_PARAM(Node, wheel_fr);
    
    	protected:
    		void init();
    		void update();
    		void updatePhysics();
    
    	private:
    		// wheel joints
    		Unigine::JointWheelPtr joint_wheel_bl;
    		Unigine::JointWheelPtr joint_wheel_br;
    		Unigine::JointWheelPtr joint_wheel_fl;
    		Unigine::JointWheelPtr joint_wheel_fr;
    
    		// settings of input controls 
    		Unigine::ControlsPtr controls;
    
    		// current driving parameters
    		float current_velocity = 0.0f;
    		float current_torque = 0.0f;
    };
    Car.cpp (C++)
    #include "Car.h"
    
    using namespace Unigine;
    
    REGISTER_COMPONENT(Car);
    
    void Car::init()
    {
    	// get the wheel joints from the nodes
    	if (wheel_bl)
    		joint_wheel_bl = checked_ptr_cast<JointWheel>(wheel_bl->getObjectBody()->getJoint(0));
    
    	if (wheel_br)
    		joint_wheel_br = checked_ptr_cast<JointWheel>(wheel_br->getObjectBody()->getJoint(0));
    
    	if (wheel_fl)
    		joint_wheel_fl = checked_ptr_cast<JointWheel>(wheel_fl->getObjectBody()->getJoint(0));
    
    	if (wheel_fr)
    		joint_wheel_fr = checked_ptr_cast<JointWheel>(wheel_fr->getObjectBody()->getJoint(0));
    
    	// get the settings of input controls relevant to the player (camera)
    	controls = Game::getPlayer()->getControls();
    }
  4. Then add control inputs processing to determine the current torque and velocity to be applied to wheel joints.

    Car.cpp (C++)
    /* ... */
    void Car::update()
    {
    	// get the time it took to render the previous frame
    	float deltaTime = Game::getIFps();
    
    	// process control inputs
    	if (controls)
    	{
    		// set the torque and increase the current velocity if the forward button is pressed
    		if (controls->getState(Controls::STATE_FORWARD) != 0)
    		{
    			current_torque = default_torque;
    			current_velocity = Math::max(current_velocity, 0.0f);
    			current_velocity += deltaTime * acceleration;
    		}
    		// set the torque and decrease the current velocity if the backward button is pressed
    		else if (controls->getState(Controls::STATE_BACKWARD) != 0)
    		{
    			current_torque = default_torque;
    			current_velocity = Math::min(current_velocity, 0.0f);
    			current_velocity -= deltaTime * acceleration;
    		}
    		// exponentially decrease the current velocity when the gas or brakes are not engaged
    		else
    		{
    			current_velocity *= Math::exp(-deltaTime);
    		}
    		/* ... */
    	}
    }
    /* ... */
  5. To move the car forward or backward, we are going to use joint motors of the front wheels:

    • Setting a positive velocity value will move our car forward.
    • Setting a negative velocity value will move our car backward.

    Each physics frame, we apply current angular velocity and torque to the wheel joints based on the user input.

    Car.cpp (C++)
    /* ... */
    // actually apply the torque and velocity to the front wheel joints
    void Car::updatePhysics()
    {
    	joint_wheel_fl->setAngularVelocity(current_velocity);
    	joint_wheel_fr->setAngularVelocity(current_velocity);
    
    	joint_wheel_fl->setAngularTorque(current_torque);
    	joint_wheel_fr->setAngularTorque(current_torque);
    }

Step 6. Implementing Steering and Handbrake#

To provide the ability to turn left or right as well as the ability to slow down with a handbrake, we shall implement some additional logic that turns the front wheels and stops the rear ones. To achieve this, do the following:

  1. Add the following code to the Car component to add the front wheels turning and handbrake support.

    Car.h (C++)
    #include <UnigineGame.h>
    #include <UnigineMathLib.h>
    
    class Car : public Unigine::ComponentBase
    {
    	public:
    	/* ... */
    		PROP_PARAM(Float, turn_speed, 50.0f);
    		PROP_PARAM(Float, car_base, 3.0f);
    		PROP_PARAM(Float, car_width, 2.0f);
    	/* ... */
    
    	private:
    		float max_turn_angle = 30.0f;
    
    		float current_turn_angle = 0.0f;
    };
    Car.cpp (C++)
    /* ... */
    void Car::update()
    {
    	/* ... */
    	current_torque = 0.0f;
    
    	// process control inputs
    	if (controls)
    	{
    		/* ... */
    		
    		// turn the front wheels based on the direction input
    		if (controls->getState(Controls::STATE_MOVE_LEFT) != 0)
    			current_turn_angle += deltaTime * turn_speed;
    		else if (controls->getState(Controls::STATE_MOVE_RIGHT) != 0)
    			current_turn_angle -= deltaTime * turn_speed;
    		else
    		{
    			// get rid of the front wheel wiggle
    			if (Math::abs(current_turn_angle) < 0.25f)
    				current_turn_angle = 0.0f;
    
    			// align the front wheels if there are no more direction input
    			current_turn_angle -= Math::sign(current_turn_angle) * turn_speed * deltaTime;
    		}
    
    		// engage handbrake by maxing out the angular damping if the handbrake button is pressed
    		if (controls->getState(Controls::STATE_USE) != 0)
    		{
    			joint_wheel_bl->setAngularDamping (10000.0f);
    			joint_wheel_br->setAngularDamping(10000.0f);
    		}
    		else
    		{
    			joint_wheel_bl->setAngularDamping(0.0f);
    			joint_wheel_br->setAngularDamping (0.0f);
    		}
    	}
    	
    	// clamp the velocity and the front wheels turn angle
    	current_velocity = Math::clamp(current_velocity, -max_velocity, max_velocity);
    	current_turn_angle = Math::clamp(current_turn_angle, -max_turn_angle, max_turn_angle);
    
    	// figure out the correct turn angle for the front wheels
    	float angle_0 = current_turn_angle;
    	float angle_1 = current_turn_angle;
    	if (Math::abs(current_turn_angle) > Math::Consts::EPS)
    	{
    		float radius = car_base / Math::tan(current_turn_angle * Math::Consts::DEG2RAD);
    		float radius_0 = radius - car_width * 0.5f;
    		float radius_1 = radius + car_width * 0.5f;
    
    		angle_0 = Math::atan(car_base / radius_0) * Math::Consts::RAD2DEG;
    		angle_1 = Math::atan(car_base / radius_1) * Math::Consts::RAD2DEG;
    	}
    
    	joint_wheel_fr->setAxis10(Math::rotateZ(angle_1).getColumn3(0));
    	joint_wheel_fl->setAxis10(Math::rotateZ(angle_0).getColumn3(0));
    }
    /* ... */
  2. Before you can use the C++ component you should initialize the Component System. Modify the init() method of the AppSystemLogic class as shown below (AppSystemLogic.cpp file).

    AppSystemLogic.cpp (C++)
    #include <UnigineComponentSystem.h>
    
    int AppSystemLogic::init()
    {
    	// initialize ComponentSystem and register all components
    	ComponentSystem::get()->initialize();
    
    	return 1;
    }
  3. Build and run the project via your IDE (press Ctrl + F5 in Visual Studio), the Component System will generate a property-file for the Car component.
  4. Switch back to UnigineEditor and select the root minibus node in the World Nodes window. Then click Add New Property in the Parameters window and assign the newly generated Car property.
  5. Next, assign wheels (ObjectMeshStatic nodes) to the corresponding fields of the Car property as specified below.

Congratulations! Now you can finally drive the car around the world. If the current physics of the car is not suitable for your needs, you can always fine tune the parameters of the wheel joints and achieve the results you need. Also, see the Basic Programming How-To Livestream for the hands-on vehicle implementation and discussions. The full source code of the Car component is given below.

Car.h (C++)
#pragma once
#include <UnigineComponentSystem.h>

#include <UniginePhysics.h>
#include <UniginePlayers.h>
#include <UnigineControls.h>
#include <UnigineGame.h>
#include <UnigineMathLib.h>


class Car : public Unigine::ComponentBase
{
	public:
		// declare constructor and destructor for our class and define a property name
		COMPONENT_DEFINE(Car, ComponentBase)
		// declare methods to be called at the corresponding stages of the execution sequence
		COMPONENT_INIT(init);
		COMPONENT_UPDATE(update);
		COMPONENT_UPDATE_PHYSICS(updatePhysics);

		// driving parameters
		PROP_PARAM(Float, acceleration, 50.0f);
		PROP_PARAM(Float, max_velocity, 90.0f);
		PROP_PARAM(Float, turn_speed, 50.0f);
		PROP_PARAM(Float, default_torque, 5.0f);
		PROP_PARAM(Float, car_base, 3.0f);
		PROP_PARAM(Float, car_width, 2.0f);

		// wheel joints
		PROP_PARAM(Node, wheel_bl);
		PROP_PARAM(Node, wheel_br);
		PROP_PARAM(Node, wheel_fl);
		PROP_PARAM(Node, wheel_fr);

	protected:
		void init();
		void update();
		void updatePhysics();

	private:
		// wheel joints
		Unigine::JointWheelPtr joint_wheel_bl;
		Unigine::JointWheelPtr joint_wheel_br;
		Unigine::JointWheelPtr joint_wheel_fl;
		Unigine::JointWheelPtr joint_wheel_fr;

		// settings of input controls 
		Unigine::ControlsPtr controls;

		// current driving parameters
		float current_velocity = 0.0f;
		float current_torque = 0.0f;
		float current_turn_angle = 0.0f;

		float max_turn_angle = 30.0f;
};
Car.cpp (C++)
#include "Car.h"

using namespace Unigine;

REGISTER_COMPONENT(Car);

void Car::init()
{
	// get the wheel joints from the nodes
	if (wheel_bl)
		joint_wheel_bl = checked_ptr_cast<JointWheel>(wheel_bl->getObjectBody()->getJoint(0));

	if (wheel_br)
		joint_wheel_br = checked_ptr_cast<JointWheel>(wheel_br->getObjectBody()->getJoint(0));

	if (wheel_fl)
		joint_wheel_fl = checked_ptr_cast<JointWheel>(wheel_fl->getObjectBody()->getJoint(0));

	if (wheel_fr)
		joint_wheel_fr = checked_ptr_cast<JointWheel>(wheel_fr->getObjectBody()->getJoint(0));

	// get the settings of input controls relevant to the player (camera)
	controls = Game::getPlayer()->getControls();
}

void Car::update()
{
	// get the time it took to render the previous frame
	float deltaTime = Game::getIFps();

	current_torque = 0.0f;

	// process control inputs
	if (controls)
	{
		// set the torque and increase the current velocity if the forward button is pressed
		if (controls->getState(Controls::STATE_FORWARD) != 0)
		{
			current_torque = default_torque;
			current_velocity = Math::max(current_velocity, 0.0f);
			current_velocity += deltaTime * acceleration;
		}
		// set the torque and decrease the current velocity if the backward button is pressed
		else if (controls->getState(Controls::STATE_BACKWARD) != 0)
		{
			current_torque = default_torque;
			current_velocity = Math::min(current_velocity, 0.0f);
			current_velocity -= deltaTime * acceleration;
		}
		// exponentially decrease the current velocity when the gas or brakes are not engaged
		else
		{
			current_velocity *= Math::exp(-deltaTime);
		}

		// turn the front wheels based on the direction input
		if (controls->getState(Controls::STATE_MOVE_LEFT) != 0)
			current_turn_angle += deltaTime * turn_speed;
		else if (controls->getState(Controls::STATE_MOVE_RIGHT) != 0)
			current_turn_angle -= deltaTime * turn_speed;
		else
		{
			// get rid of the front wheel wiggle
			if (Math::abs(current_turn_angle) < 0.25f)
				current_turn_angle = 0.0f;

			// align the front wheels if there are no more direction input
			current_turn_angle -= Math::sign(current_turn_angle) * turn_speed * deltaTime;
		}

		// engage handbrake by maxing out the angular damping if the handbrake button is pressed
		if (controls->getState(Controls::STATE_USE) != 0)
		{
			joint_wheel_bl->setAngularDamping (10000.0f);
			joint_wheel_br->setAngularDamping(10000.0f);
		}
		else
		{
			joint_wheel_bl->setAngularDamping(0.0f);
			joint_wheel_br->setAngularDamping (0.0f);
		}
	}

	// clamp the velocity and the front wheels turn angle
	current_velocity = Math::clamp(current_velocity, -max_velocity, max_velocity);
	current_turn_angle = Math::clamp(current_turn_angle, -max_turn_angle, max_turn_angle);

	// figure out the correct turn angle for the front wheels
	float angle_0 = current_turn_angle;
	float angle_1 = current_turn_angle;
	if (Math::abs(current_turn_angle) > Math::Consts::EPS)
	{
		float radius = car_base / Math::tan(current_turn_angle * Math::Consts::DEG2RAD);
		float radius_0 = radius - car_width * 0.5f;
		float radius_1 = radius + car_width * 0.5f;

		angle_0 = Math::atan(car_base / radius_0) * Math::Consts::RAD2DEG;
		angle_1 = Math::atan(car_base / radius_1) * Math::Consts::RAD2DEG;
	}

	joint_wheel_fr->setAxis10(Math::rotateZ(angle_1).getColumn3(0));
	joint_wheel_fl->setAxis10(Math::rotateZ(angle_0).getColumn3(0));
}

// actually apply the torque and velocity to the front wheel joints
void Car::updatePhysics()
{
	joint_wheel_fl->setAngularVelocity(current_velocity);
	joint_wheel_fr->setAngularVelocity(current_velocity);

	joint_wheel_fl->setAngularTorque(current_torque);
	joint_wheel_fr->setAngularTorque(current_torque);
}
Last update: 2021-11-30
Build: ()