Tutorial 1 ‐ car (JavaScript) - Outerra/anteworld GitHub Wiki

Tutorial on how to make basic car using JavaScript.

image

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

Version 0 - Info

You should use at least "init_chassis", "init_vehicle" and "update_frame" events.

vehicle_script

vehicle_script API provides pre-defined events (e.g. init_chassis, init_vehicle, update_frame, etc.) and methods (wheel_force, wheel_brake, etc.), which can be used in vehicle script.

You can find these events and methods in vehicle_script.

To be able to use vehicle_script events and methods in script, the belonging .objdef file needs to have "physics" set to "vehicle" (see tutorial ‐ mod files).

init_chassis

Event "init_chassis" is invoked when the model is first time loaded

  • Can take extra parameters (string "param") from .objdef file.

  • Returns vehicle chassis structure.

It is used to define the vehicle construction, binding bones, adding wheels, lights, sounds, sound emitters, action handlers, etc.

Those can be defined only in init_chassis.

Example:

function init_chassis(param)
{ 
	let wheel_params= {
		radius: 0.31515,
		width: 0.2,
		suspension_max: 0.1,
		suspension_min: -0.12,
		suspension_stiffness: 30.0,
		damping_compression: 0.4,
		damping_relaxation: 0.12,
		grip: 1,
	};	
	this.register_event("vehicle/engine/reverse", reverse_action);
	return {
		mass: 1120.0,
		com: {x: 0.0, y: 0.0, z: 0.3},
		steering_params:{
			steering_ecf: 50,
		},
}

Wheel structure

For ground vehicles to work correctly, they need to have defined wheel parameters.

For more information, see Wheel structure.

Action handlers

Action handlers are events, which are called, when binded input/object changed it's state.

For more information, see Action handlers.

Warning: value handlers (register_axis, register_handler, etc.) get called with value 0, when the script is loaded/reloaded.

event handlers (register_event) are not called on script load/reload.

When using action handlers in Javascript, you have two options for specifying the "handler" function:

  • Using an existing function: you can provide the name of an existing function
this.register_event("vehicle/engine/reverse", reverse_action); 
  • Defining the function directly: alternatively, you can define the functionality inline using an anonymous function directly within the call
this.register_event("vehicle/lights/emergency", function() { 
	this.emer ^= 1; 
}); 

Warning: action handlers can be handled by engine or by user (but not both).

Some handlers are automatically handled by engine (for example default parameters used by "update_frame", such as dt, engine, brake, steering and parking).

When you declare an action handler for an event, that the engine already handles internally, you take over control of handling that event, and the engine will no longer manage it internally

Example

this.register_event("vehicle/engine/brake", brake_action);

Example: here we have action handler, that checks, if user pressed brake button ('S'), originally it would be automatically handled by engine, and in "update_frame" you would get the braking value from "brake" parameter, which can be then used, but now the "brake" value will be always 0.

init_chassis return parameters

You should assign some return parameters, because if not, they will be set to default parameters.

You can find information about chassis structure in Vehicle chassis parameters.

Effective steering angle is reduced with vehicle speed by exp(-kmh/steering_threshold). Wheel auto-centering speed depends on the vehicle speed by 1 - exp(-kmh/centering_threshold), e.g with higher speed the wheel centers faster.


init_vehicle

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

When initializing variables within your object or model instance, use the "this" keyword. This ensures that changes affect only the specific instance of your object/model where the code is being executed.

Additionally, "this" is used to reference Outerra's internal interface, which provides various methods that can be invoked using this.method_name().

function init_vehicle()
{	
	this.started = 0;
	this.set_fps_camera_pos({x:-0.4, y:0.16, z:1.3});
}

Fps camera position should be defined in "init_vehicle".


update_frame

"update_frame" is invoked each frame to handle the internal state of the object

function update_frame(dt, engine, brake, steering, parking)
{	
}

update_frame has following parameters:

  • dt - delta time from previous frame
  • engine - gas pedal state (0..1)
  • brake - brake pedal state (0..1)
  • steering - steering state (-1..1)
  • parking - hand brake state (0..1)

Warning: "update_frame" updates these parameters only in case, they are not handled by user (mentioned in init_chassis, while declaring action handlers).


simulation_step

"simulation_step" can be used instead of "update_frame", but it is invoked 60 times per second and has only parameter dt.

Note: simulation_step can not be used in aircraft interface.

function simulation_step(dt)
{	
}

Note: for debugging purposes, you can use "$log()" to write info on console.

$log("Brake value: " + brake); 

Version 1 - Moving

In .objdef file

For our vehicle to work, it is needed, to make some configuration in .objdef file of our model.

For information on how to configure the .obfdej file, refer to tutorial ‐ mod files.

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_js\packages\tutorial_car_js)

In .js script

Define global variables (global variables will be mainly used to store values, that don't change at runtime by given instance, like const used for calculations, ID of wheels, sounds, emitters, bones etc.).

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

const ENGINE_FORCE = 25000.0;
const BRAKE_FORCE = 5000.0;
const MAX_KMH = 200;
const FORCE_LOSS = ENGINE_FORCE / (0.2*MAX_KMH + 1);

let wheels = {
	FLwheel : -1, 
	FRwheel : -1, 
	RLwheel : -1, 
	RRwheel : -1, 
};

Note: for debug purpose, it's better to define these variables to -1.


Create function engine_action

This function will start/stop the vehicle when corresponding button is pressed (in this case 'E')).

"fade" function is used, to write fading message on screen.

function engine_action()
{
	//Check the actual "started" value and toggle it (using ternary operator)
	this.started = this.started === 0 ? 1 : 0;
	 //Write fading message on the screen using "fade" funciton
	this.fade(this.started  === 1  ? "Engine start" : "Engine stop");
    
	//To not apply force on wheels, when the engine has stopped 
	if(this.started === 0)
	{
		this.wheel_force(wheels.FLwheel, 0);
		this.wheel_force(wheels.FRwheel, 0);
	}
}

Create function reverse_action

This function will change the direction of the vehicle, when reverse button is pressed ( in this case 'R').

Can take "v" as parameter through action handler.

function reverse_action(v)
{
	this.eng_dir= this.eng_dir >=0 ? -1 : 1;
	this.fade(this.eng_dir > 0 ? "Forward" : "Reverse");
}

Create init_chassis

function init_chassis()	
{
}

In init_chassis

Define physical wheel parameters (see Wheel structure).

let wheel_params = {
	radius: 0.31515,
	width: 0.2,
	suspension_max: 0.1,
	suspension_min: -0.04,
	suspension_stiffness: 50.0,
	damping_compression: 0.4,
	damping_relaxation: 0.12,
	grip: 1,
};

Bind model wheel joint/bone and add wheel parameters

Function "add_wheel" returns ID of the wheel

1.parameter - wheel joint/bone to which you want to bind

2.parameter - wheel physical parameters

wheels.FLwheel = this.add_wheel('wheel_l0', wheel_params ); //front left wheel (will have ID 0)
wheels.FRwheel = this.add_wheel('wheel_r0', wheel_params ); //front right wheel (will have ID 1)
wheels.RLwheel = this.add_wheel('wheel_l1', wheel_params ); //rear left wheel (will have ID 2)
wheels.RRwheel = this.add_wheel('wheel_r1', wheel_params ); //rear right wheel (will have ID 3)

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

Create Action handlers.

Note: the hand brake and power are handled through script, to avoid bug, where parking brake cannot be engaged right after the power input was released, (because by default, the centering is lower, and takes some time, until the value drops to 0)

//engine_action function will be called, when engine toggle button is presssed ('E')
this.register_event("vehicle/engine/on", engine_action);

//reverse_action function will be called, when reverse button is presssed ('R').
this.register_event("vehicle/engine/reverse", reverse_action);

//Toggle the state of "this.hand_brake_input" each time, the hand brake button ('Space') is pressed  
this.register_event("vehicle/controls/hand_brake", function(v){
    this.hand_brake_input ^= 1;
}); 

//When accelerating (holding 'W' button), store the value of "power" action in "this.power_input"
this.register_axis("vehicle/controls/power", {minval: 0, center: Infinity}, function(v){
    this.power_input = v;
}); 

Define chassis return parameters (if not defined, they will be set to default parameters).

return {
	mass: 1120.0,
	com: {x: 0.0, y: -0.2, z: 0.3},
	steering_params:{
		steering_ecf: 50,
	},

Create init_vehicle

function init_vehicle()
{
}

In init_vehicle

Initialize instance-related variables ( should be initialized within "init_vehicle")

this.started = 0;
this.eng_dir = 1;
this.braking_power = 0;
this.power_input = 0;
this.hand_brake_input = 1;

Set FPS camera position

Function "set_fps_camera_pos" set's 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 )

this.set_fps_camera_pos({x = -0.4, y = 0.16, z = 1.3});

-- Example of using bone, to set the FPS camera position
-- this.set_fps_camera_pos({x = 0, y = 0, z = 0}, this.get_joint_id("fps_cam_bone"), 1);

Create update_frame

function update_frame(dt, engine, brake, steering, parking)
{
}

In function update_frame

Define local variable and and get current speed, using "speed" function, that returns current speed in m/s (multiplied by 3.6 to get km/h)

//We want to get the absolute value
let kmh = Math.abs(this.speed() * 3.6);

Calculate force and direction (which will be applied on wheels to move the car, when the car has started), then release hand brake and apply propelling force on given wheel.

For applying force, use "wheel_force" function

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: for calculations, you can use Math library.

if (this.started === 1)
{ 
	// Calculate force, which will be applied on wheels to move the car
	let redux = this.eng_dir >= 0 ? 0.2 : 0.6;
	let eng_power = ENGINE_FORCE * this.power_input;
	// Determine the force value based on whether the car should move forward or backward
	let force = (kmh >= 0) == (this.eng_dir >= 0) ? (eng_power / (redux * kmh + 1)) : eng_power; 
	// Add wind resistance
	force = force - FORCE_LOSS;
	// Make sure, that force can not be negative
	force = Math.max(0.0, Math.min(force, eng_power));
	//Multiply with this.eng_dir (will be 1 or -1), to set direction
	force *= this.eng_dir;

        //Release the parking brake, when accelerating, while started... 
        if(this.hand_brake_input !== 0 && force > 0)
        {
            this.hand_brake_input = 0;
        } 

	// Apply propelling force on given wheels
	this.wheel_force(wheels.FLwheel, force);
	this.wheel_force(wheels.FRwheel, force);
}

Note: as "wheel ID", you can also use -1 to affect all wheels, or -2 to affect first 2 defined wheels.

Define steering sensitivity and the steering, using "steer" function, which steers wheels by given angle

1.parameter - wheel ID

2.parameter - angle in radians, to steer the wheel

steering = steering * 0.3;	
this.steer(wheels.FLwheel, steering);	// front left wheel
this.steer(wheels.FRwheel, steering);	// front right wheel

Set the braking value, which will be applied on wheels, based on the type of brake (parking or regular brake)

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

if(this.hand_brake_input !== 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.

Use the braking_power in "wheel_brake" function, to apply braking force on given wheels

1.parameter - wheel ID (in this case we want to affect all wheels, therefore -1)

2.parameter - braking force

// resistance
this.braking_power = this.braking_power + 200;

this.wheel_brake(-1, this.braking_power);

Version 2 - Bone rotation and movement (using geomob)

This tutorial focused on rotating/moving bones using geomob.

Create additional global variables

const SPEED_GAUGE_MIN = 10.0;
const RAD_PER_KMH = 0.018325957;

//create object containing bones/joints
let bones = {
	steer_wheel : -1, 
	speed_gauge : -1, 
	accel_pedal : -1, 
	brake_pedal : -1, 
	driver_door : -1,
};

In init_chassis

**Get joint/bone ID **, for that use "get_joint_id" function

parameter - bone name

bones.steer_wheel = this.get_joint_id('steering_wheel');	//Steering wheel
bones.speed_gauge = this.get_joint_id('dial_speed');		//Speed gauge 
bones.accel_pedal = this.get_joint_id('pedal_accelerator');	//Accelerator pedal
bones.brake_pedal = this.get_joint_id('pedal_brake');		//Brake pedal
bones.driver_door = this.get_joint_id('door_l0');		//Driver's door

Declare additional action handler for opening/closing door

For rotating joint/bone, you can use rotate_joint_orig or rotate_joint.

  • rotate_joint_orig - for bone to rotate to given angle (from default orientation)

1.param - bone/joint ID

2.param - rotation angle in radians

3.param - 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).

  • rotate_joint - for bone to rotate by given angle every time it's invoked (given angle is added to joint current orientation (incremental)).

This function takes same 3 parameters as rotate_joint_orig, but can also take

4.parameter - bool value - true if rotation should go from the bind pose, otherwise accumulate (false by default).

//open/close driver's door (when 'O' is pressed)
this.register_axis("vehicle/controls/open", {minval:0, maxval: 1, center:0, vel:0.6}, function(v) {
	//Define around which axis and in which direction the door will move
	let door_dir= {z:-1};
	//Multiplied with 1.5 to fully open the door
	let door_angle= v * 1.5;
	this.geom.rotate_joint_orig(bones.driver_door, door_angle, door_dir);
}); 

Note: action handlers use geomob (geom) functionality for current instance, which was initialized in init_vehicle.


In init_vehicle

Get instance geometry interface, which will be used for current instance (to rotate bone, move bone, etc. )

"get_geomob" function is used to get geometry interface

parameter - ID of geometry object (default 0, which represents main body)

this.geom = this.get_geomob(0);	

In update_frame

Define additional local variables

let brake_dir = {x: 1};
// Brake pedal rotation angle will depend on the brake value
let brake_angle = brake * 0.4;	
// You can also use more than one axis
let accel_dir = {y: (-this.power_input * 0.02), z: (-this.power_input * 0.02)}

Use geomob functions to rotate/move joints

Function "move_joint_orig" is used to move joint to given position

1.parameter - joint you want to move

2.parameter - movement axis and direction

3.parameter - bool value - true if movement should go from the bind pose, otherwise accumulate (false by default).

// Rotate brake pedal
this.geom.rotate_joint_orig(bones.brake_pedal, brake_angle, brake_dir);
// move accelerator pedal
this.geom.move_joint_orig(bones.accel_pedal, accel_dir)
// Rotate speed gauge
if (kmh > SPEED_GAUGE_MIN)
{ 
        this.geom.rotate_joint_orig(bones.speed_gauge, (kmh - SPEED_GAUGE_MIN) * RAD_PER_KMH, {x: 0,y: 1,z: 0});    
}
	
this.geom.rotate_joint_orig(bones.steer_wheel, 10.5*steering, {z: 1});

Use "animate_wheels" function to animate wheels. This method simplifies the animation of wheels for basic cases, without needing to animate the model via the geomob.

this.animate_wheels();

Version 3 - Lights

To add lights, you have to do following steps.

Create object containing light related members

let light_entity = {
	brake_mask : 0, 
	rev_mask : 0, 
	turn_left_mask : 0, 
	turn_right_mask : 0,
	main_light_offset : 0
};

In function reverse_action

Apply reverse light mask (rev_mask) on reverse lights, when engine direction has value -1 (activate 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 bits/lights will be affected

this.light_mask(light_entity.rev_mask, this.eng_dir < 0);

Every time you press reverse button, parameter "v" switches it's value (in this case between minval and maxval, but it can also switch between positions, if they are defined)


In init_chassis

Define light parameters and assign them to "light_props".

While defining light parameters, you can use parameters specified in light_params.

let light_props = {size: 0.05, angle: 120, edge: 0.2, range: 70, fadeout: 0.05 };

Use "add_spot_light" function to add lights

1.parameter - model-space offset relative to bone or model pivot

2.parameter - light direction

3.parameter - light properties

4.parameter - string name of the bone, to which you want to bind the light (this will make the lights offset and direction to be relative to the defined bone instead of model pivot)

// Add front lights (offset relative to model pivot is given for the lights and direction is set to forward by {y: 1})
this.add_spot_light({x: -0.55, y: 2.2, z: 0.68}, {y: 1.0}, light_props);  // left front light
this.add_spot_light({x: 0.55, y: 2.2, z: 0.68}, {y: 1.0}, light_props);   // right front light

Add tail lights

// Change the light properties in "lightProp" and use them for another lights
light_props = { size: 0.1, angle: 160, edge: 0.8, color: { x: 1.0 }, range: 150, fadeout: 0.05 };
// add tail lights
this.add_spot_light({x: -0.05, y: -0.06, z: 0.0}, {y: 1.0}, light_props, "tail_light_l0");  // left tail light
this.add_spot_light({x: 0.05, y: -0.06, z: 0.0}, {y: 1.0}, light_props, "tail_light_r0");   // right tail light 		

Warning: In this case, the direction of this light is now opposite to front lights, even though direction is still {y: 1}, because the light is now relative to tail light bone, which has opposite direction.

Here's another example regarding light direction: while the brake lights are relative to the model pivot, the direction is specified as {y: -1}, indicating the opposite direction, therefore the lights will illuminate in the backward direction.

Add brake lights and store the offset of the first light in "brake_light_offset"

light_props = { size: 0.04, angle: 120, edge: 0.8, color: { x: 1.0 }, range: 100, fadeout: 0.05 };
let brake_light_offset =  
this.add_spot_light({x: -0.43, y: -2.11, z: 0.62}, {y: -1.0}, light_props);   // left brake light (0b01)
this.add_spot_light({x: 0.43, y: -2.11, z: 0.62}, {y: -1.0}, light_props);    // right brake light (0b10)
	

Now we have to specify bit mask (brake_mask), for that we have to use bit logic.

You can find informations about bitmasking on Outerra wiki in light_params

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.

//Binary system used
light_entity.brake_mask = 0b11 << brake_light_offset;

Add reverse lights, we want to manipulate both lights, when we hit the reverse button (in this case, the value is written in decimal system)

light_props = { size: 0.04, angle: 120, edge: 0.8, range: 100, fadeout: 0.05 };
let rev_light_offset =
this.add_spot_light({x: -0.5, y: -2.11, z: 0.715}, {y: -1.0}, light_props);	 // left reverse light (0b01)
this.add_spot_light({x: 0.5, y: -2.11, z: 0.715}, {y: -1.0}, light_props);	 // right reverse light (0b10)
// Decimal system used
light_entity.rev_mask = 3 << rev_light_offset;

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

In this case the "add_point_light" function is used, because we don't need this light to shine in specific direction.

add_point_light also takes position and light properties as parameters, same as add_spot_light, but without direction.

light_props = {size: 0.1, edge: 0.8, intensity: 1, color:{x: 0.4, y: 0.1, z: 0}, range: 0.004, fadeout: 0 };
let turn_light_offset =
this.add_point_light({x: -0.71, y: 2.23, z: 0.62}, light_props); 	// left front turn light (0b0001)
this.add_point_light({x: -0.66, y: -2.11, z: 0.715}, light_props); 	// left rear turn light (0b0010)
this.add_point_light({x: 0.71, y: 2.23, z: 0.62}, light_props);  	// right front turn light (0b0100)
this.add_point_light({x: 0.66, y: -2.11, z: 0.715}, light_props); 	// right rear turn light (0b1000)
// Hexadecimal system used
// When the left turn signal button was pressed, we want turn lights on the left side to glow 
light_entity.turn_left_mask = 0x3 << turn_light_offset;
// When the right turn signal button was pressed, we want turn lights on the right side to glow 
light_entity.turn_right_mask = 0x3 << turn_light_offset + 2;

Note: in "turn_right_mask" we added previous left turn lights to the offset (you can also make another offset for right turn lights and use that...).

Add main lights, here you don't have to identify lights for bit mask, because they were added as 4.parameter in "add_spot_light" function while creating action handler.

light_props = { size: 0.05, angle: 110, edge: 0.08, range: 110, fadeout: 0.05 };
light_entity.main_light_offset = 
this.add_spot_light({x: -0.45, y: 2.2, z: 0.68}, {y: 1.0}, light_props);  // left main light
this.add_spot_light({x: 0.45, y: 2.2, z: 0.68}, {y: 1.0}, light_props);   // right main light

**Add following action handlers**
```js
//Handle this action, when passing lights button is pressed ('L')
this.register_axis("vehicle/lights/passing", {minval: 0, maxval: 1, center: 0, vel: 10, positions: 2  }, function(v) {	
	this.light_mask(0xf, v === 1); 
});

Note: this action handler 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.

Another way to use light mask, is to give the light offset as 3.parameter in light_mask.

//This action is handled, when you press Ctrl + L 
this.register_axis("vehicle/lights/main", {minval: 0, maxval: 1, center: 0, vel: 10, positions: 2 }, function(v) {	
	this.light_mask(0x3, v === 1, light_entity.main_light_offset);
});

Turn signals can have -1/0/1 values, when 'Shift' + 'A' is pressed, the "v" value switches between 0 and -1, but when 'Shift' + 'D' is pressed, the value moves between 0 and 1.

this.register_axis("vehicle/lights/turn", {minval: -1, maxval: 1, center: 0, vel: 10}, function(v) {
	if(v === 0)
	{
		this.left_turn = 0;
		this.right_turn = 0;
	}
	else if(v < 0)
	{
		this.left_turn = 1; 
		this.right_turn = 0;
	}
	else
	{
		this.left_turn = 0;
		this.right_turn = 1; 
	}
});

"this.emer ^= 1" toggles the value of "emer" between 0 and 1

//Handle this action, when emergency lights buttons are pressed ('Shift' + 'W')
this.register_event("vehicle/lights/emergency", function(v) { 
	this.emer ^= 1; 
});

In init_vehicle

Initialize additional variables

this.time = 0;
this.left_turn = 0;
this.right_turn = 0;
this.emer = 0;

In update_frame

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.

this.light_mask(light_entity.brake_mask, brake > 0);

Calculate blinking time, which will affect turn signal lights and apply light mask for turn lights, depending of which action was handled (left turn lights, right turn lights or all turn lights (emergency)), they will then turn on and off, depending on the "blink" value (true or false).

if (this.left_turn || this.right_turn || this.emer)
{
        // When turn/emergency lights are turned on, calculate blinking time (in this case between 0 and 1) for turn signal lights
	this.time += dt;
        this.time %= 1;

        -- For turn lights blinking effect
        let blink = this.time > 0.47 ? 1 : 0;
        
	-- Apply light mask for turn lights, depending of which action was handled (left turn lights, right turn lights or all turn lights (emergency)), which will then turn on and off, depending on the "blink" value (1 or 0)
	this.light_mask(light_entity.turn_left_mask, (blink && (this.left_turn || this.emer)) );
	this.light_mask(light_entity.turn_right_mask, (blink && (this.right_turn || this.emer)) );
}
else
{
	// To turn off the active turn lights
	this.light_mask(light_entity.turn_left_mask, false);
	this.light_mask(light_entity.turn_right_mask, false);
        this.time = 0;
}

Version 4 - Sounds

To add sounds, you have to do following steps.

Create object containing sound related members

let sound_entity = {
	snd_starter : -1, 
	snd_eng_running : -1, 
	snd_eng_stop : -1, 
	src_eng_start_stop : -1, 
	src_eng_running : -1,
};

In function engine_action

Get camera mode, using function "get_camera_mode" and based on that, set the sound gain value

returns - camera mode (0, when the first person view, inside the vehicle is active)

let sound_gain = this.get_camera_mode() == 0 ? 0.5 : 1;

Use "set_gain" function to set gain value on given emitter

1.parameter - emitter

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

this.snd.set_gain(sound_entity.src_eng_start_stop, sound_gain);

Set reference distance value, based on the camera mode

let ref_distance = this.current_camera_mode == 0 ? 0.25 : 1;

Use set_ref_distance 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)

this.snd.set_ref_distance(sound_entity.src_eng_start_stop, ref_distance);

Add functionality to play/stop engine sounds

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

1.parameter - emitter (source ID)

2.parameter - sound (sound ID))

Function "stop" discards all sounds playing on given emitter.

if(this.started === 0)
{
	this.snd.stop(sound_entity.src_eng_running);
	this.snd.play_sound(sound_entity.src_eng_start_stop, sound_entity.snd_eng_stop);
        
        this.wheel_force(wheels.FLwheel, 0);
        this.wheel_force(wheels.FRwheel, 0);
}
else if(this.started === 1) 
{
	this.snd.play_sound(sound_entity.src_eng_start_stop, sound_entity.snd_starter);
}

In init_chassis

Load sound samples (located in "Sounds" folder) using "load_sound" function

parameter - string filename (audio file name, possibly with path)

returns- sound ID

sound_entity.snd_starter = this.load_sound("Sounds/starter.ogg");		// will have ID 0
sound_entity.snd_eng_running = this.load_sound("Sounds/eng_running.ogg");	// will have ID 1
sound_entity.snd_eng_stop = this.load_sound("Sounds/eng_stop.ogg");	        // will have ID 2

Create sound emitters, using "add_sound_emitter" function

1.parameter - joint/bone name to attach to

2.parameter - sound type: -1 interior only, 0 universal, 1 exterior only

3.parameter - reference distance (saturated volume distance)

returns - emitter ID

sound_entity.src_eng_start_stop = this.add_sound_emitter("exhaust_0_end");  // will have ID 0
sound_entity.src_eng_running = this.add_sound_emitter("exhaust_0_end");	    // will have ID 1

In init_vehicle

Get sound interface, using "sound" function.

this.snd = this.sound();

Set initial sound values

this.snd.set_pitch(sound_entity.src_eng_start_stop, 1);
this.snd.set_pitch(sound_entity.src_eng_running, 1);
this.snd.set_gain(sound_entity.src_eng_start_stop, 1);
this.snd.set_gain(sound_entity.src_eng_running, 1);
this.snd.set_ref_distance(sound_entity.src_eng_start_stop, 1);
this.snd.set_ref_distance(sound_entity.src_eng_running, 1);

In update_frame

Get camera mode

Function "get_camera_mode"

returns - current camera mode (0 - FPS camera mode, 1 - TPS camera mode, 2 - TPS follow camera mod )

this.current_camera_mode = this.get_camera_mode();

Check if the camera mode has changed, to not set reference distance every frame.

Change the ref_distance, if the camera mode has changed.

if (this.previous_cam_mode != this.current_camera_mode)
{   
        let ref_distance;
        //Choose reference distance, based on current camera mode
        if(this.current_camera_mode == 0)
        {
		ref_distance = 0.25;
        }
        else
        {
		ref_distance = 1;
        }
        //Set reference distance
        this.snd.set_ref_distance(sound_entity.src_eng_running, ref_distance);

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

Inside the existing "if (this.started === 1)" statement add code, to 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).

Calculate pitch and gain, and play loop if the car has started and there isn't active loop on given emitter.

Function "max_rpm" returns rpm of the fastest revolving wheel.

Function "set_pitch" is used, to set pitch value on given emitter

1.parameter - emitter

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

Function "play_loop" is used, to play sound in loop, breaking previous sounds

1.parameter - emitter (source ID)

2.parameter - sound (sound ID))

if (this.started === 1)
{
...
	if(this.snd.is_playing(sound_entity.src_eng_start_stop))
	{
		force = 0;
	} //If car has started and there isn't active loop on given emitter, play loop
	else
	{
		//Calculate and set volume pitch and gain for emitter
		let rpm = this.max_rpm();
		let speed_modulation = kmh / 40 + Math.abs(rpm / 200.0);
		let pitch_gain_factor = rpm > 0 ? Math.floor(speed_modulation) : 0;
		let pitch_gain = speed_modulation + (0.5 * pitch_gain_factor) - pitch_gain_factor;

		// Set pitch
		this.snd.set_pitch(sound_entity.src_eng_running, (0.5 * pitch_gain) + 1.0);

		// Set gain
		this.snd.set_gain(sound_entity.src_eng_running, (0.25 * pitch_gain) + 0.5);    

		if(!this.snd.is_looping(sound_entity.src_eng_running))
		{
   		         this.snd.play_loop(sound_entity.src_eng_running, sound_entity.snd_eng_running);
		}
	}
}  

Another loop function, that can be used is "enqueue_loop" to enqueue looped sound

1.parameter - emitter (source ID)

2.parameter - sound (sound ID)

Example:

this.snd.enqueue_loop(sound_entity.src_eng_running, sound_entity.snd_eng_running);