Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Irrlicht car simulation

4.93/5 (19 votes)
21 Oct 2009CPOL8 min read 83.8K   8.9K  
A car simulation demo using the Irrlicht game engine and the Newton physics engine.

IPHY

Introduction

Don't have a car? Don't even have a driving license? You do not need them; all you need is to run a car simulation program to drive the most expensive and speedy cars. I will introduce you to a simple car simulation program using Irrlicht, the most popular Open Source game engine, and Newton, the great physics engine.

To make a car simulation program, you need a game engine to render the car body, the wheels, the track, and any other graphics items, but the idea behind the simulation is not to render the graphics items, but how to put these items in a physics environment to simulate reality. To do that, we will use a physics engine which will drop the car if it reaches the end of a cliff, or flip it if you try to pass a sharp turn ... etc. I used the Newton physics engine in this simulator, and to make our task easier, I also used the IPhysics Irrlicht Newton wrapper, which will make the link between Irrlicht and Newton so easy.

Background

The Irrlicht engine is an Open Source high performance real time 3D engine written and usable in C++. I used for this demo, the 1.4.2 version; the earlier versions couldn't render my .3ds models well. The IPhysics library (a set of classes for using Newton and Irrlicht together) was released only for the earlier versions, so I had to recompile this library to be compatible with the 1.4.2 version, and I have attached the new lib with the code sample.

Using the code

Open the solution file with Visual Studio 2005 and run the project. It will show a message telling you that it couldn't find the irrlicht.dll file. Just copy irrlicht.dll and newton.dll (you will find them in the attached sample executable) files to the release folder, and run again.

Preparing the code

Include these header files needed in the project:

C++
#include <irrlicht.h>
#include <newton.h>
#include <IPhysics.h> 
#include "EventReciever.h"

Use the #pragma comment statement to load the lib files (you can also add the lib files from the project setting).

C++
#pragma comment (lib , "irrlicht.lib") 
#pragma comment (lib , "newton.lib")

In the Irrlicht engine, everything can be found in a namespace. So if you want to use a class, you have to write its namespace before the name of the class, so we use the "using namespace" statement to avoid having to write the namespaces before every class.

C++
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

Irrlicht basics

To load a mesh, we only have to get the mesh from the Scene Manager with the getMesh() function, passing to it the path of the mesh, and add a SceneNode to display the mesh with addAnimatedMeshSceneNode().

We disable lighting because we do not have a dynamic light in here, and the mesh would be totally black otherwise:

C++
/*
* Function to load the car
*/
IAnimatedMeshSceneNode* LoadCar(scene::ISceneManager* smgr )
{
   char* carModel = "../media/Vehicle/impreza.3ds";
   IAnimatedMesh* carMesh = smgr->getMesh(carModel);
   IAnimatedMeshSceneNode* carSceneNode = 
        smgr->addAnimatedMeshSceneNode(carMesh);
   carSceneNode->setMaterialFlag(video::EMF_LIGHTING, false);
   return carSceneNode;
}

We do the same for the car wheels.

Get a pointer to the Video Driver, the Scene Manager, and the graphical user interface environment, so that we do not always have to write device->getVideoDriver() or device->getSceneManager().

Note: I used Direct3D 9 as my renderer; you can use other renderers like Direct3D 8 or even OpenGL:

C++
// make direct3d9 our renderer
IrrlichtDevice* device = createDevice( EDT_DIRECT3D9);
if (device == 0)
    return ;
// get the video driver
IVideoDriver* driver = device->getVideoDriver();
// get the scene manager
ISceneManager* smgr = device->getSceneManager();

The map of the game is a Quake 3 map which is packed into .pk3 files, which are nothing other than .zip files.

By calling the addZipFileArchive function, we are able to read from the files in that archive as if they are directly stored on the disk.

Now we can load the mesh by calling getMesh (Quake 3 maps are not really animated, they are only a huge chunk of static geometry with some materials attached; our game map exists in the Irrlicht SDK). The .bsp is the main model, and the other files are textures or XML files needed for this model, so we load the map as follows:

C++
//Load the game map
device->getFileSystem()->addZipFileArchive(pk3Map);
IAnimatedMesh* mapMesh = smgr->getMesh(mapName);
IAnimatedMeshSceneNode* gameMap = smgr->addAnimatedMeshSceneNode(mapMesh)

To look at the mesh, we place a camera into the 3D space at the position:

C++
//ICameraSceneNode* camera = smgr->addCameraSceneNode();
camera->setPosition(vector3df(0, 30.0f, -30.0f));

To show a window message, we use the device getGUIEnvironment function to add a message to the scene:

C++
// tell the user a message , MessageText and Caption are declaired at the top of the file
device->getGUIEnvironment()->addMessageBox(MessageText.c_str(),Caption.c_str());

The physics enhancements

The physics part is the most tricky part: how to make the loaded car behave as if it is real.

Newton is an advanced physics engine for real-time simulation of rigid bodies, created by Julio Jerez 2003. Linking between Newton and Irrlicht is not an easy task, so we use IPhysics (a set of interface classes for easily integrating Newton and Irrlicht).

To use IPhysics, make a CPhysics object, then call the init function that creates the physics world:

C++
// init the physics world
CPhysics physics;
physics.init(device->getTimer());

Creating a car follows the same approach as creating anything else. However, there are a lot of fields to fill out:

  • carBodyOffset: This is the offset position for the car body. For example, if you want the car body to be a box that is 5 units above the ground, this should be vector3df(0, 5.0f, 0).
  • carBodySize: This is the size of the cube that will represent the car body. For example, if you want the car body to be a box that is 10x5x8, this should be vector3df(10.0f, 5.0f, 8.0f).
  • carMass: The mass of the car body (not the wheels).
  • frontAxleOffset: This is the distance (along the x axis) that the front axle is from the origin. For example, if your car is 10 units long, and you want the wheels to be at either end, this should be set to 5.0f, as should the field below.
  • rearAxleOffset: Same as above, but for the rear axle. Note that this should be a positive number.
  • axleWidth: The width of the axle determines how far apart the left and right wheels are. This should be about the width of the car body.
  • tireMass: Mass of the tires.
  • tireWidth: Width of the tires.
  • tireRadius: Radius of the tires.
  • carBodyNode: Pointer to the Irrlicht scene node representing the car body.
  • wheelNode_FL: Front left wheel scene node.
  • wheelNode_FR: Front right wheel scene node.
  • wheelNode_RL: Rear left wheel scene node.
  • wheelNode_RR: Rear right wheel scene node.
  • tireSuspensionShock: This value is passed straight to Newton. From the Newton docs: "Parameterized damping constant for a spring, mass, damper system. A value of one corresponds to a critically damped system."
  • tireSuspensionSpring: This value is passed straight to Newton. From the Newton docs: "Parameterized spring constant for a spring, mass, damper system. A value of one corresponds to a critically damped system."
  • tireSuspensionLength: This value is passed straight to Newton. From the Newton docs: "Distance from the tire set position to the upper stop on the vehicle body frame. The total suspension length is twice that."
  • maxSteerAngle: The maximum angle the wheels are allowed to turn for steering.
  • maxTorque: The maximum torque for the wheels.
  • maxBrakes: The maximum brakes for the wheels.

So, we make the SPhysicsCar object and fill these values:

C++
// init the car parameter
SPhysicsCar carData ;

carData.carBodyOffset = vector3df(0, 0.0f, 0);
carData.carBodySize = vector3df(1.2f, 01.85f, 0.2f);
carData.carMass = 3000.0f;
carData.frontAxleOffset = 01.5f;
carData.rearAxleOffset = 01.1f;
carData.axleWidth = 01.7f;
carData.tireMass = 20.0f;
carData.tireRadius = 0.98f;
carData.tireWidth = 01.0f;
carData.maxSteerAngle = 0.6f;
carData.maxTorque = 2000.0f;
carData.maxBrakes = 50.0f;


carData.tireSuspensionLength = 0.20f;
carData.tireSuspensionSpring = 
  (carData.tireMass * 1.0f * 9.8f) / carData.tireSuspensionLength;
carData.tireSuspensionShock = sqrt(carData.tireSuspensionSpring) * 1.0f;

carData.carBodyNode = LoadCar(smgr);
carData.carBodyNode->setScale(vector3df(.943,.943,.943));

carData.tireNode_FL = LoadCarWheel(smgr ,driver);
carData.tireNode_FL->setScale(vector3df(carData.tireRadius, 
                                 carData.tireRadius, carData.tireWidth));

carData.tireNode_FR = LoadCarWheel(smgr ,driver);
carData.tireNode_FR->setScale(vector3df(carData.tireRadius, 
                                 carData.tireRadius, carData.tireWidth));

carData.tireNode_RL = LoadCarWheel(smgr ,driver);
carData.tireNode_RL->setScale(vector3df(carData.tireRadius, 
                                 carData.tireRadius, carData.tireWidth));

carData.tireNode_RR = LoadCarWheel(smgr ,driver);
carData.tireNode_RR->setScale(vector3df(carData.tireRadius, 
                                 carData.tireRadius, carData.tireWidth));

The main program body

After setting up, the scene lets us draw everything. We run the device in a while() loop, and everything is drawn between the beginScene() and endScene() call. The SetTarget camera member function is to make the camera follow the car body. Switch between the two camera modes using the staticCamera boolean. Draw all initialized graphics items by calling the drawAll() ISceneManager member function. Draw the GUI stuff (the message box) by calling the drawAll() GuiEnvironment member function. Update the physics environment and then drop the device object.

C++
// the game main loop
while(device->run())
{
    driver->beginScene(true, true, SColor(255,100,101,140));
    //make the camera follow the car body
    camera->setTarget(vector3df(carData.carBodyNode->getPosition().X, 
    carData.carBodyNode->getPosition().Y+3, 
    carData.carBodyNode->getPosition().Z ));    

    // switch between the two cameras
    if(staticCamera)
        camera->setPosition(vector3df(carData.carBodyNode->getPosition().X, 
           carData.carBodyNode->getPosition().Y+3, 
           carData.carBodyNode->getPosition().Z + 7));
    
    smgr->drawAll();

    device->getGUIEnvironment()->drawAll();         
    driver->endScene();
    physics.update();
}

device->drop();

Adding balls

What about the balls I added? The balls in the demo is to feel the reality, as the motion of the balls make the scene so real. IPhysics enables you to add dynamic spheres in an easy manner as follows:

Make a sphere node and add the beach ball texture to it:

C++
// make a sphere node
ISceneNode* dynamicShereNode = smgr->addSphereSceneNode(0.70f);
// make the sphere node take the ball shap by adding a beach ball texture
dynamicShereNode->setMaterialFlag(video::EMF_LIGHTING, false);
ITexture * ballTexture = driver->getTexture("../media/Balls/BeachBallColor.jpg");
dynamicShereNode->setMaterialTexture(0,ballTexture);

Make a SPhysicsSphere object and initialize its members like mass, x radius, y radius, z radius, then add the sphere node created before to the node member.

Note: EBT_DYNAMIC means that the ball is a dynamic entity or can move around.

C++
// make a dynamic sphere
SPhysicsSphere dynamicSphere;
dynamicSphere.bodyType = EBT_DYNAMIC;
dynamicSphere.mass = 1.1;
dynamicSphere.radius_x = .70f;
dynamicSphere.radius_y = .70f;
dynamicSphere.radius_z = .70f;
dynamicSphere.node = dynamicShereNode;

Add the SPhysicsSphere object to the physics world:

C++
// add the ball to the physics world
IPhysicsEntity* dynamicSphereEntity = physics.addEntity(&dynamicSphere);

Init the ball position just above the car:

C++
// set the ball position just above the car
dynamicSphereEntity->setPosition(vector3df(car->getPosition().X, 
                                    car->getPosition(    ).Y+ 12, car->getPosition().Z));

Event handling

How do we move the car forward, backward, left, and right?

The CEventReciever class does the task for you. We have to overwrite the OnEvent virtual function to make our custom events. This function takes a const SEvent object as a parameter, which stores the key value pressed by the user, so we do our checking on this parameter to know what key user has pressed.

C++
virtual bool OnEvent(const SEvent& event)

If the user pressed the up arrow key, we set the IPhysicsCar throttle percent by a positive value, and if he pressed the down arrow key, we set the IPhysicsCar throttle percent by a negative value. This value determines the speed of the car.

C++
if(event.EventType == EET_KEY_INPUT_EVENT)
{
    // if user press UP arrow
    if(event.KeyInput.Key == KEY_UP)
    {
        if(!m_keys[KEY_UP])
        {
            m_keys[KEY_UP] = true;
            // go forward
            m_car->setThrottlePercent(1.0f);
        }
        else if(event.KeyInput.PressedDown == false)
        {
            m_keys[KEY_UP] = false;
        }
    }
    // if user press Down arrow
    if(event.KeyInput.Key == KEY_DOWN)
    {
        if(!m_keys[KEY_DOWN])
        {
            m_keys[KEY_DOWN] = true;
            // go backword
            m_car->setThrottlePercent(-100.0f);
        }
        else if(event.KeyInput.PressedDown == false)
        {
            m_keys[KEY_DOWN] = false;
        }
    }

For the right and arrow keys, we set the steering percent for the IPhysicsCar object instead of the throttle percent, in the same manner. If the user presses two opposite keys at once, it will stop the car.

C++
if(!m_keys[KEY_LEFT] && !m_keys[KEY_RIGHT])
{
    m_car->setSteeringPercent(0.0f);
}

if(!m_keys[KEY_UP] && !m_keys[KEY_DOWN])
{
    m_car->setThrottlePercent(0.0f);

}

Also, I use the C key to drop a ball and the V key to flip the car, and F2, F3 keys to switch the camera mode.

C++
// if user press C button
if(event.KeyInput.Key == KEY_KEY_C)
{
    // drop the sphere
    dropDynSphere(m_smgr,m_driver,m_physics,m_car);
} // if user press V button
else if(event.KeyInput.Key == KEY_KEY_V)
{
    // flib the car position
    m_car->setPosition(vector3df(m_car->getPosition().X , 
       m_car->getPosition().Y + 3 ,m_car->getPosition().Z));
    m_car->setRotation(vector3df(m_car->getRotation().X +90,
      m_car->getRotation().Y,m_car->getRotation().Z));
}
else if(event.KeyInput.Key == KEY_F3)
{
    // active the static camera
    staticCamera= true;
}
else if(event.KeyInput.Key == KEY_F2)
{
    // active the dynamic camera
    staticCamera = false;
}

Points of interest

I wrote this article for those who share me love for game programming, so this article will be useful for developers who have a game programming background and some C++ skills.

Notes

You must install the VS2005 redistributable package if you have not installed Visual Studio 2005 on your PC; you can download the package from here.

History

  • Sept. 27, 2009 - Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)