Skip to content

Tutorial 1 ‐ car (Cpp)

Gombaris edited this page May 27, 2024 · 6 revisions

Tutorial on how to make basic C++ car.

You can find the files belonging to this tutorial and complete code in Outerra/anteworld "Code" section in Tutorials/example_models_cpp/packages/tutorial_car_plugin folder.

Mod files

You can find tutorial on mod files here

Warning: mods working with models (in this case, we are working with car model), need to have the files located under "packages" folder (example: Outerra World Sandbox\mods\example_models_cpp\packages\tutorial_car_cpp)

Creating project

You can find tutorial, on how to create and set project properties here

In .hpp file

Include necessary files

vehicle_script is needed for our vehicle simulation, to provide important tools and features.

#include<ot/vehicle_script.h>

Initialize global variables and mark them as const, as they won't be changed later (will be used only in statements and calculations).

const uint MAX_KMH = 200;
const float SPEED_GAUGE_MIN = 10.0f;
const float RAD_PER_KMH = 0.018325957f;
const float ENGINE_FORCE = 25000.0f;
const float BRAKE_FORCE = 5000.0f;
const float FORCE_LOSS = ENGINE_FORCE / (0.2f * MAX_KMH + 1.f);

Declare static variables, which will represent different parts and features of our aircraft.

It is preferable to group related variables within structs, as it enhances modularity and creates structured and understandable code.

struct wheels
{
	static int FL_wheel;
	static int FR_wheel;
	static int RL_wheel;
	static int RR_wheel;
};

struct bones
{
	static int steer_wheel;
	static int speed_gauge;
	static int accel_pedal;
	static int brake_pedal;
	static int driver_door;
};

struct lights_entity
{
	static uint rev_mask;
	static uint brake_mask;
	static uint turn_left_mask;
	static uint turn_right_mask;
	static uint main_light_offset;
};

struct sounds_entity
{
	static int snd_starter;
	static int snd_eng_running;
	static int snd_eng_stop;

	static int src_eng_start_stop;
	static int src_eng_running;
};

Note: "Static" keyword is used, so that these variables can be shared across all instances (familiar to creating global variable). Without it, the variables would be defined only for the 1. instance/vehicle, because they are set in init_chassis, which is called only the first time.

Create class which inherits from vehicle_script, so that it can implement specific functionalities

class tutorial_car_plugin: public ot::vehicle_script
{
};

Note: "ot" is custom namespace, which contains the class "vehicle_script".

In class tutorial_car_plugin

You can find more about vehicle interface on Outerra wiki in Vehicle .

Override virtual functions from the base class vehicle_script.

These are basic functions needed for creating an vehicle.

ot::chassis_params init_chassis(const coid::charstr& params) override;
void init_vehicle(bool reload) override;
void update_frame(float dt, float engine, float brake, float steering, float parking) override;

**Declare additional functions **

void engine(int flags, uint code, uint channel, int handler_id);
void reverse(int flags, uint code, uint channel, int handler_id);
void emergency_lights(int flags, uint code, uint channel, int handler_id);
void passing_lights(float val, uint code, uint channel, int handler_id);
void turn_lights(float val, uint code, uint channel, int handler_id);
void main_lights(float val, uint code, uint channel, int handler_id);
void open_door(float val, uint code, uint channel, int handler_id);

Some of these functions will be used as callback functions (their function pointer will be passed to action handlers as parameter), therefore their arguments need to align with the parameters specified in the callback function predefinition:.

event handler parameters - function(int flags, uint code, uint channel, int handler_id)

axis handler parameters - function(float val, uint code, uint channel, int handler_id)

Declare variables

bool started;
bool emer;
int eng_dir;
int left_turn;
int right_turn;
int current_camera_mode;
int previous_cam_mode;
float braking_power;
double time;

iref<ot::geomob> geom = nullptr;
iref<ot::sndgrp> sounds = nullptr;

Note: geom and sounds are smart pointers, pointing to geometry and sound group, they are used for managing reference-counted objects.

In .cpp file

Include header file, belonging to this .cpp file

#include "cppcar.hpp"

Static members must be defined outside class body.

For debugging,it's better to define them to *-1.

int wheels::FL_wheel = -1;
int wheels::FR_wheel = -1;
int wheels::RL_wheel = -1;
int wheels::RR_wheel = -1;
int bones::steer_wheel = -1;
int bones::speed_gauge = -1;
int bones::accel_pedal = -1;
int bones::brake_pedal = -1;
int bones::driver_door = -1;
uint lights_entity::rev_mask = -1;
uint lights_entity::brake_mask = -1;
uint lights_entity::turn_left_mask = -1;
uint lights_entity::turn_right_mask = -1;
uint lights_entity::main_light_offset = -1;
int sounds_entity::snd_starter = -1;
int sounds_entity::snd_eng_running = -1;
int sounds_entity::snd_eng_stop = -1;
int sounds_entity::src_eng_start_stop = -1;
int sounds_entity::src_eng_running = -1;

It is neccesary to register our vehicle class as client, for that use macro IFC_REGISTER_CLIENT

  • parameter - derived client interface class
namespace
{
	IFC_REGISTER_CLIENT(tutorial_car_plugin);
}

Namespace here is used, so that the registration is limited in scope and will be visible only within the translation unit, where the namespace is defined.

Define additional functions

In function engine

"engine" function will be used to start/stop the vehicle and play sounds associated with it

Function "set_gain" is used, to set gain value on given emitter

  • 1.parameter - emitter
  • 2.parameter - gain value(this will affect all sounds emitted from this emitter)

Function "set_ref_distance" is used, to set reference distance on given emitter (how far should the sounds be heard)

  • 1.parameter - emitter
  • 2.parameter - reference distance value(this will affect all sounds emitted from this emitter)

Function "play_sound" is used, to play sound once, discarding older sounds

  • 1.parameter - emitter (source ID)
  • 2.parameter - sound (sound ID)

Function "stop" is used, to discards all sounds playing on given emitter

void tutorial_car_plugin::engine(int flags, uint code, uint channel, int handler_id)
{
	//Check, if the "sounds" pointer is not nullptr
	if (!this->sounds)
	{
		return;
	}

	//Ternary operator used, to toggle the "started" value
	this->started = this->started == false ? true : false;
	//Write a fading message on the screen, depending on the "started" value
	fade(this->started == true ? "Engine start" : "Engine stop");

	//Based on the camera mode, set the sound gain value
	float sound_gain = this->current_camera_mode == 0 ? 0.5f : 1.f;

	//Set gain value on given emitter
	this->sounds->set_gain(sounds_entity::src_eng_start_stop, sound_gain);

	//Based on the camera mode, set the reference distance value
	float ref_distance = this->current_camera_mode == 0 ? 0.25f : 1.f;

	//Set reference distance on given emitter
	this->sounds->set_ref_distance(sounds_entity::src_eng_start_stop, ref_distance);

	//Play sounds, depending on the "started" state
	if (this->started)
	{
		//play starter sound
		this->sounds->play_sound(sounds_entity::src_eng_start_stop, sounds_entity::snd_starter);
	}
	else
	{
		//stop engine running sound
		this->sounds->stop(sounds_entity::src_eng_running);
		//play engine stop sound
		this->sounds->play_sound(sounds_entity::src_eng_start_stop, sounds_entity::snd_eng_stop);

		//To not apply force on wheels, when the engine has stopped 
		wheel_force(wheels::FL_wheel, 0);
		wheel_force(wheels::FR_wheel, 0);
	}
}

Note: An check is used in this function that ensures, that the sounds pointer is not null before attempting to use it. Don't forget to protect your mod from crashing due to a null pointer.


In function reverse

"reverse" function will be used to change the direction and turn on/off reverse lights.

"light_mask" function is used to turn lights on/off

  • 1.parameter - light mask value - which bits/lights should be affected
  • 2.parameter - condition, when true, given lights will be affected ("turned on")
  • 3.parameter - offset, from which the light mask will affect bits/lights (optional)
void tutorial_car_plugin::reverse(int flags, uint code, uint channel, int handler_id)
{
	//change direction
	this->eng_dir = this->eng_dir >= 0 ? -1 : 1;
	fade(this->eng_dir > 0 ? "Forward" : "Reverse");
	//turn lights on/off
	light_mask(lights_entity::rev_mask, this->eng_dir < 0);
}

In function emergency_lights

"emergency_lights" function will be used to turn on/off emergency lights.

Bitwise operator XOR (^) is used, to switch the value each time, this function is called.

void tutorial_car_plugin::emergency_lights(int flags, uint code, uint channel, int handler_id)
{
	this->emer ^= 1;
}

In function passing_lights

"passing_lights" function will be used to turn on/off the passing lights

void tutorial_car_plugin::passing_lights(float val, uint code, uint channel, int handler_id)
{
	//if v===1, lights will turn on (light mask will affect the front and tail lights), else it will turn off
	light_mask(0xf, val == 1);
}

Note: this affects first 4 defined lights (because we didn't give an offset as 3. parameter). In this case it will affect 2 front lights and 2 tail lights, because every light is represented as 1 bit and as 1.parameter we used hexadecimal 0x0..0xf which works with 4 bits (0000....1111), we can also use 0x00..0xff which will work with first 8 bits

In function main_lights

"main_lights" function will be used to turn on/off the main lights

void tutorial_car_plugin::main_lights(float val, uint code, uint channel, int handler_id)
{
	light_mask(0x3, val == 1, lights_entity::main_light_offset);
}

Note: Another way to use light mask, is to give light offset (in this case main_light_offset) as 3. parameter, from this offset the light mask will affect next bits/lights


In function turn_lights

"turn_lights" function will be used to toggle turn lights

void tutorial_car_plugin::turn_lights(float val, uint code, uint channel, int handler_id)
{
	if (val == 0)
	{
		//Turn off turn lights
		this->left_turn = 0;
		this->right_turn = 0;
	}
	else if (val < 0)
	{
		//Turn on left turn light
		this->left_turn = 1;
		this->right_turn = 0;
	}
	else
	{
		//Turn on right turn light
		this->left_turn = 0;
		this->right_turn = 1;
	}
}

In function open_door

"open_door" function will be used to open/close door

Function "rotate_joint_orig" is used to rotate joint to given angle and back to default position

  • 1.parameter - bone/joint ID
  • 2.parameter - vec rotation angle in radians
  • 3.parameter - rotation axis vector (must be normalized) - axis around which the bone rotates (in this case around Z axis) and the direction of rotation (-1...1)
void tutorial_car_plugin::open_door(float val, uint code, uint channel, int handler_id)
{
	//Check, if the Geom pointer is not nullptr before attempting to use it, to avoid crashing due to a null pointer
	if (!this->geom)
	{
		return;
	}
	//rotate driver door 
	this->geom->rotate_joint_orig(bones::driver_door, (val * 1.5f), { 0.f, 0.f, -1.f });
}

In init_chassis

"init_chassis" is used, to initialize chassis parameters for our car (invoked once, to define the chassis for all instances of same type)

  • params - custom parameters from objdef
ot::chassis_params tutorial_car_plugin::init_chassis(const coid::charstr& params)
{
...
}

Create an object of type ot::wheel to represent wheel structure and define the wheel parameters by assigning values to its members.

//Define wheel setup
ot::wheel wheel_params = {
//Wheel parameters, that can be used:
.radius1 = 0.31515f,		//outer tire radius [m]
.width = 0.2f,			//tire width [m]
.suspension_max = 0.1f,		//max.movement up from default position [m]
.suspension_min = -0.04f,	//max.movement down from default position [m]
.suspension_stiffness = 50.0f,	//suspension stiffness coefficient
.damping_compression = 0.4f,	//damping coefficient for suspension compression
.damping_relaxation = 0.12f,	//damping coefficient for suspension relaxation
.grip = 1.f,			//relative tire grip compared to an avg.tire, +-1
.slip_lateral_coef = 1.5f,	//lateral slip muliplier, relative to the computed longitudinal tire slip
.differential = true,		//true if the wheel is paired with another through a differential
};

Note: class/struct members need to be defined in the same order, as they are declared. This is the case for wheel parameters, init_chassis return parameters, light parameters etc.

You can find more about wheel parameters here .

Function "add_wheel" adds wheel with vertical suspension (along z axis)

  • 1.parameter - wheel joint/bone pivot
  • 2.parameter - wheel parameters
  • returns ID of the wheel
wheels::FL_wheel = add_wheel("wheel_l0", wheel_params);
wheels::FR_wheel = add_wheel("wheel_r0", wheel_params);
wheels::RL_wheel = add_wheel("wheel_l1", wheel_params);
wheels::RR_wheel = add_wheel("wheel_r1", wheel_params);

Note: you can find model bones in Scene editor -> Entity properties -> Skeleton.

Function "get_joint_id" is used, to get joint/bone ID of given bone

  • parameter - bone name
  • returns joint/bone id or -1 if doesn't exist
bones::steer_wheel = get_joint_id("steering_wheel");
bones::speed_gauge = get_joint_id("dial_speed");
bones::accel_pedal = get_joint_id("pedal_accelerator");
bones::brake_pedal = get_joint_id("pedal_brake");
bones::driver_door = get_joint_id("door_l0");

Define light parameters

Light parameters, that can be defined:

  • float size - diameter of the light source (reflector or bulb)
  • float angle - light field angle [deg], ignored on point lights
  • float edge - soft edge coefficient, 0..1 portion of the light area along the edges where light fades to make it soft
  • float intensity - light intensity (can be left 0 and use the range instead)
  • float4 color - RGB color and near-infrared component of the light emitter
  • float range - desired range of light
  • float fadeout - time to fade after turning off
ot::light_params light_params = { .size = 0.05f, .angle = 120.f, .edge = 0.2f, .color = { 1.f, 1.f, 1.f, 0.f }, .range = 70.f, .fadeout = 0.05f };

If either intensity or range are specified alone, the other one is computed automatically. If neither one is specified, the intensity is taken from the color value, otherwise the colors are normalized. If both intensity and range are specified, the light can have shorter range to avoid leaking outside of cockpits etc.

Note: final intensity of light depends not only on intensity parameter, but also on angle and range.

Define circular spotlight source, using "add_spot_light" function

  • 1.parameter - offset - model-space offset relative to the bone or model pivot
  • 2.parameter - dir - light direction
  • 3.parameter - lp - light parameters
  • 4.parameter - joint - joint name to attach the light to
  • returns light emitter id
//Passing front lights
uint pass_light_offset =
add_spot_light({ -0.55f, 2.2f, 0.68f }, { 0.f, 1.f, 0.f }, light_params);
add_spot_light({ 0.55f, 2.2f, 0.68f }, { 0.f, 1.f, 0.f }, light_params);

//Tail lights
light_params = { .size = 0.07f, .angle = 160.f, .edge = 0.8f, .color = { 1.f, 0.f, 0.f, 0.f } , .range = 150.f, .fadeout = 0.05f };
uint tail_light_offset =
add_spot_light({ -0.05f, -0.06f, 0.f }, { 0.f, 1.f, 0.f }, light_params, "tail_light_l0");
add_spot_light({ 0.05f, -0.06f, 0.f }, { 0.f, 1.f, 0.f }, light_params, "tail_light_r0");

//Brake lights
light_params = { .size = 0.04f, .angle = 120.f, .edge = 0.8f, .color = { 1.f, 0.f, 0.f, 0.f}, .range = 100.f, .fadeout = 0.05f };
uint brake_light_offset =
add_spot_light({ -0.43f, -2.11f, 0.62f }, { 0.f, -1.f, 0.f }, light_params);
add_spot_light({ 0.43f, -2.11f, 0.62f }, { 0.f, -1.f, 0.f }, light_params);

You can specify bit mask (brake_mask) using bit logic.

You can find more about bitmasking, lights and light parameters here

We want the bit mask (brake_mask) to affect both lights, therefore the given value will be "0b11" (in this case, the value is written in binary system, but it can also be written in decimal or hexadecimal system).

Also we want, that the mask starts affecting lights from the first brake light, therefore we have to "left shift" the bit mask by brake light offset

//Reverse lights
light_params = { .size = 0.04f, .angle = 120.f, .edge = 0.8f, .color = { 1.f, 1.f, 1.f, 0.f }, .range = 100.f, .fadeout = 0.05f };
uint rev_light_offset =
add_spot_light({ -0.5f, -2.11f, 0.715f }, { 0.f, -1.f, 0.f }, light_params);
add_spot_light({ 0.5f, -2.11f, 0.715f }, { 0.f, -1.f, 0.f }, light_params);
lights_entity::rev_mask = 3 << rev_light_offset;

We want to affect both lights, when we hit the reverse button, therefore 3 (in this case, the value is written in decimal system)

//Main lights
light_params = { .size = 0.05f, .angle = 110.f, .edge = 0.8f, .color = { 1.f, 1.f, 1.f, 0.f }, .range = 110.f, .fadeout = 0.05f };
lights_entity::main_light_offset =
add_spot_light({ -0.45f, 2.2f, 0.68f }, { 0.f, 1.f, 0.f }, light_params);
add_spot_light({ 0.45f, 2.2f, 0.68f }, { 0.f, 1.f, 0.f }, light_params);	

Define point light source, using add_point_light function

  • 1.parameter - offset - model-space offset relative to the bone or model pivot
  • 2.parameter - lp - light parameters
  • 3.parameter - joint - joint name to attach the light to
  • returns light emitter id

We want lights on the side of the car to glow, when we hit the corresponding left or right turn signal button (in this case, the value is written in hexadecimal system )

//Turn lights
light_params = { .size = 0.1f, .edge = 0.8f, .intensity = 1.f, .color = { 0.4f, 0.1f, 0.f, 0.f}, .range = 0.004f, .fadeout = 0.f };
uint turn_light_offset =
	add_point_light({ -0.71f, 2.23f, 0.62f }, light_params);
add_point_light({ -0.66f, -2.11f, 0.715f }, light_params);
add_point_light({ 0.71f, 2.23f, 0.62f }, light_params);
add_point_light({ 0.66f, -2.11f, 0.715f }, light_params);
	
lights_entity::turn_left_mask = 0x3 << turn_light_offset;
//To create right turn light mask, add the previous defined left turn lights to the offset (or you can make another offset for right turn lights and use that....)
lights_entity::turn_right_mask = 0x3 << (turn_light_offset + 2);

Load sound samples using "load_sound" function

  • parameter - string filename (audio file name, possibly with path)
  • returns sound ID
sounds_entity::snd_starter = load_sound("sounds/starter.ogg");
sounds_entity::snd_eng_running = load_sound("sounds/eng_running.ogg");
sounds_entity::snd_eng_stop = load_sound("sounds/eng_stop.ogg");

Create sound emitters that will be used by the vehicle, using "add_sound_emitter" function

  • 1. parameter - joint/bone, from which we want the sound to emit
  • 2. parameter - sound type (-1 interior only, 0 universal, 1 exterior only)
  • 3. parameter - reference distance (saturated volume distance)
  • returns emitter ID, which is later used in code
sounds_entity::src_eng_start_stop = add_sound_emitter("exhaust_0_end", 0);
sounds_entity::src_eng_running = add_sound_emitter("exhaust_0_end", 0);

Register "action handlers", which are called, when binded input/object changes it's state.

Register event handlers, invoked on key or button press (for example a fire action)

  • 1.parameter - name - path to input binding (config_file/group/action) where: "config_file" - name of .cfg file, in which we have the binding defined(these can be found in "bin/defaults/iomap" directory, where vehicle.cfg is for vehicles, air.cfg is for aircraft etc.) "group" - name of the group, to which it belongs "action" - name of the bound action(this action is bound to shift + L))
  • 2.parameter - handler - function, which is invoked whenever given event occurs or the tracked value changes as the result of the button state.
  • 3.parameter - handler_id - optional extra data for the handler
  • 4.parameter - group - activation group where the action is assigned (can be enabled/disabled together)
  • returns slot id or -1 on fail
register_event_handler("vehicle/engine/on", &tutorial_car_plugin::engine);
register_event_handler("vehicle/engine/reverse", &tutorial_car_plugin::reverse);
register_event_handler("vehicle/lights/emergency", &tutorial_car_plugin::emergency_lights);

Register value handlers, they are receiving a tracked value which is by default in -1..1 range

  • 1.parameter - name - hierarchic action name (config_file/group/action)
  • 2.parameter - handler - handler for changed value
  • 3.parameter - ramp - value limiter parameters
  • 4.parameter - handler_id - optional extra data for the handler
  • 5.parameter - def_val - optional default action value
  • 6.parameter - group - activation group where the action is assigned (can be enabled/disabled together)
  • returns slot id or -1 on fail
register_axis_handler("vehicle/lights/passing", &tutorial_car_plugin::passing_lights, { .minval = 0.f, .maxval = 1.f, .cenvel = 0.f, .vel = 10.f });
register_axis_handler("vehicle/lights/main", &tutorial_car_plugin::main_lights, { .minval = 0.f, .maxval = 1.f, .cenvel = 0.f, .vel = 10.f });
register_axis_handler("vehicle/lights/turn", &tutorial_car_plugin::turn_lights, { .minval = -1.f, .maxval = 1.f, .cenvel = 0.f, .vel = 10.f });
register_axis_handler("vehicle/controls/open", &tutorial_car_plugin::open_door, { .minval = 0.f, .maxval = 1.f, .cenvel = 0.f, .vel = 0.6f });

Ramp parameters, that can be used:

  • float minval - minimum value to clamp to, should be >= -1 for compatibility with joystick
  • float maxval - maximum value to clamp to, should be <= +1 for compatibility with joystick
  • float cenvel - centering speed per second (speed after releasing the button), 0 for freeze on button release
  • float vel - max rate of value change (saturated) per second
  • float acc - max rate of initial velocity change per second
  • uint8 positions - number of positions between min and max values including the endpoints, 0 = no stepping mode
  • uint8 extra_channels - optional number of extra channels (multiple engines etc)

You can find more about action handlers here

Return chassis parameters

	ot::chassis_params chassis_params = {
	//Chassis parameters that can be used: 
	.mass = 1120.0f,					//vehicle mass [kg]
	.com_offset = {0.0f, 0.0f, 0.3f},			//center of mass offset
	.clearance = 0.f,					//clearance from ground, default wheel radius
	.bumper_clearance = 0.f,				//clearance in front and back (train bumpers)
	.steering = {
		//Steering parameters that can be used: 
		.steering_thr = 50.f,				//speed [km/h] at which the steering speed is reduced by 60%
		.centering_thr = 20.f,				//speed [km/h] when the centering acts at 60% already
		}
	};

return chassis_params;

You can find more about vehicle chassis parameters here.

In init_vehicle

"init_vehicle" function is invoked for each new instance of the vehicle (including the first one), it is used to define per-instance parameters.

  • reload - true if object is being reloaded

Function "set_fps_camera_pos" is used to set the camera position, when FPS mode is active

  • 1. parameter - model-space position from the pivot (when the joint id as 2. parameter is not specified, otherwise it works as offset, relative to the joint position)
  • 2. parameter - bone/joint id (optional), to set fps camera position to joint position
  • 3. parameter - joint rotation mode (if the offset should be set based on joint orientation or not, where 0 - Enabled, 1 - Disabled )
void tutorial_car_plugin::init_vehicle(bool reload)
{
	this->started = false;
	this->emer = false;
	this->eng_dir = 1;
	this->left_turn = 0;
	this->right_turn = 0;
	this->current_camera_mode = 0;
	this->previous_cam_mode = 0;
	this->braking_power = 0.f;
	this->time = 0.0;

	//Get geomob interface
	this->geom = get_geomob(0);
	//Get sound interface
	this->sounds = sound();

	//Set FPS camera position
	set_fps_camera_pos({ -0.4f, 0.0f, 1.3f });

	//Example of using bone, to set the FPS camera position
	//this.get_fps_camera_pos({0.f, 0.f, 0.f}, this->get_joint_id("fps_cam_bone"), 1);

	//set initial sound values
	this->sounds->set_pitch(sounds_entity::src_eng_start_stop, 1);
	this->sounds->set_pitch(sounds_entity::src_eng_running, 1);
	this->sounds->set_gain(sounds_entity::src_eng_start_stop, 1);
	this->sounds->set_gain(sounds_entity::src_eng_running, 1);
	this->sounds->set_ref_distance(sounds_entity::src_eng_start_stop, 1);
	this->sounds->set_ref_distance(sounds_entity::src_eng_running, 1);
}

Use "this" keyword to refer to the current instance of the class.

In update_frame

"update_frame" is used, to update model instance each frame

Check, if the geom pointer is not nullptr before attempting to use it, to avoid crashing due to a null pointer

if (!this->geom || !this->sounds)
{
	return;
}

Get current ground speed, using "speed" function

  • returns current speed in m/s (multiply with 3.6 to get km/h)
float kmh = glm::abs(speed() * 3.6f); // We want to get the absolute value 

Use "get_camera_mode" function, to get current camera mode

  • returns - camera mode (0 - FPS camera mode, 1 - TPS camera mode, 2 - TPS follow camera mod )
this->current_camera_mode = this->get_camera_mode();

To not set reference distance every frame, check if the camera mode has changed

if (this->previous_cam_mode != this->current_camera_mode)
{
	float ref_distance;
	//Choose reference distance, based on current camera mode
	if (this->current_camera_mode == 0)
	{
		ref_distance = 0.25f;
	}
	else
	{
		ref_distance = 1.f;
	}

	//Set reference distance
	this->sounds->set_ref_distance(sounds_entity::src_eng_running, ref_distance);

	//set this.previous_cam_mode to current camera mode
	this->previous_cam_mode = this->current_camera_mode;
}

Use "wheel_force" function to apply propelling force on wheel and move the car

  • 1.parameter - wheel, you want to affect (takes the wheel ID, in this case, the car has front-wheel drive)
  • 2.parameter - force, you want to exert on the wheel hub in forward/backward direction (in Newtons)

Note: as "wheel ID", you can also use -1 to affect all wheels

Calculate force, but only if the car has started

if (this->started == true)
{
	//Calculate force value, depending on the direction
	float redux = this->eng_dir >= 0 ? 0.2f : 0.6f;
	//glm library can be used
	engine = ENGINE_FORCE * glm::abs(engine);
	float force = (kmh >= 0) == (this->eng_dir >= 0)
	? engine / (redux * kmh + 1)
	: engine;
	//simulate resistance
	force -= FORCE_LOSS;
	//Make sure, that force can not be negative
	force = glm::max(0.0f, glm::min(force, engine));
	//Calculate force and direction, which will be used to add force to wheels
	engine = force * this->eng_dir;

	//Apply propelling force on wheels
	this->wheel_force(wheels::FL_wheel, engine);
	this->wheel_force(wheels::FR_wheel, engine);

// code belonging to this statement continues bellow

Calculate and set volume pitch and gain for src_eng_running emitter

Use "set_pitch" function to set pitch value on given emitter

  • 1.parameter - emitter
  • 2.parameter - pitch value (this will affect all sounds emitted from this emitter)

Funciton "play_loop" is used to play sound in loop, breaking other sounds

  • 1.parameter - emitter (source ID)
  • 2.parameter - sound (sound ID))

Function "is_playing" checks, if there is sound playing on given emitter

//Code from above continues here

	//Move only when there is no sound playing on given emitter (to not be able to move when car is starting, but after the starter sound ends)
	if (this->sounds->is_playing(sounds_entity::src_eng_start_stop))
	{
		engine = 0;
	}
	else if (!this->sounds->is_looping(sounds_entity::src_eng_running))
	{
		//Calculate and set volume pitch and gain valu for emitter
		float rpm = this->max_rpm();
		float speed_modulation = kmh / 40 + glm::abs(rpm / 200);
		float pitch_gain_factor = rpm > 0 ? glm::floor(speed_modulation) : 0;
		float pitch_gain = speed_modulation + (0.5f * pitch_gain_factor) - pitch_gain_factor;
		
		//Set pitch on given emitter
		this->sounds->set_pitch(sounds_entity::src_eng_running, (0.5f * pitch_gain) + 1.f);

		//Set gain value on given emitter
		this->sounds->set_gain(sounds_entity::src_eng_running, (0.25f * pitch_gain) + 0.5f);
		
		//Play loop (if it's not already playing)
		if (!this->sounds->is_looping(sounds_entity::src_eng_running))
		{
			this->sounds->play_loop(sounds_entity::src_eng_running, sounds_entity::snd_eng_running);
		}
	}
}
// end of the "if (this->started == true)" statement

Define steering sensitivity and steer wheels by setting angle, using "steer" function

  • 1.parameter - wheel ID
  • 2.parameter - angle in radians to steer the wheel
steering *= 0.5f;
steer(wheels::FL_wheel, steering);
steer(wheels::FR_wheel, steering);

Apply brake light mask (brake_mask) on brake lights, when brake value is bigger than 0.

Note: add this code before adding rolling friction to brakes.

light_mask(lights_entity::brake_mask, brake > 0);

Set the braking value, which will be applied on wheels, based on the type of brake.

Note: originally "brake" has value between 0..1, you have to multiply it by "BRAKE_FORCE" to have enough force to brake.

//Set the braking value, which will be applied on wheels, based on the type of brake 
if (parking != 0)
{
	// Apply full braking force when the parking brake is engaged 
	this->braking_power = BRAKE_FORCE;
}
else if (brake != 0)
{
	// Apply proportional braking force when the regular brake is engaged
	this->braking_power = brake * BRAKE_FORCE;
}
else
{
	this->braking_power = 0;
}

Add some resistance, so that the car slowly stops, when not accelerating

this->braking_power += 200;

Use "wheel_brake" to apply braking force on given wheels

  • 1.parameter - wheel ID
  • 2.parameter - braking force, you want to exert on the wheel hub (in Newtons)
wheel_brake(-1, this->braking_power);

When turn/emergency lights are turned on, calculate blinking time for turn signal lights

if (this->left_turn == 1 || this->right_turn == 1 || this->emer == true)
{
	this->time += dt;
	this->time = glm::mod(this->time, 1.0); // Use glm::mod to perform the modulo operation
	//For turn lights blinking effect
	int blink = this->time > 0.47f ? 1 : 0;
	//Apply light mask for turn lights, depending on which action was handled (left turn lights, right turn lights or all turn lights (emergency))
	light_mask(lights_entity::turn_left_mask, blink && (this->left_turn || this->emer));
	light_mask(lights_entity::turn_right_mask, blink && (this->right_turn || this->emer));
}
else
{
	//To turn off the active turn lights
	light_mask(lights_entity::turn_left_mask, false);
	light_mask(lights_entity::turn_right_mask, false);
	this->time = 0;
}

Animate vehicle components

if (kmh > SPEED_GAUGE_MIN)
{
	//Rotate speedometer needle
	this->geom->rotate_joint_orig(bones::speed_gauge, (kmh - SPEED_GAUGE_MIN) * RAD_PER_KMH, { 0.f, 1.f, 0.f });
}
//Rotate steering wheel
this->geom->rotate_joint_orig(bones::steer_wheel, 10.5f * steering, { 0.f, 0.f, 1.f });

For animating wheels is better to use "animate_wheels" function, which simplifies the animation

animate_wheels();

Output files

After creating your code, the next step is to compile the project to generate the necessary output files, including the .dll and .pdb files.

Move the .dll and .pdb files into plugins folder, located in your project file's bin folder.

Warning: for these plugins to work, these files need to have sufix "_plugin".

Test

Clone this wiki locally