Skip to content

Tutorial 1 ‐ car (Lua)

Gombaris edited this page May 24, 2024 · 2 revisions

Tutorial on how to make basic car using Lua.

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

Version 0 - Info

It is needed to implement interface, to be able to use interface functionality (in this case ot.vehicle_script), such as "init_chassis", "init_vehicle", "update_frame", "simulation_step" etc.

implements("ot.vehicle_script")

You should use at least "init_chassis", "init_vehicle" and "update_frame" or "simulation_step" functions.

init_chassis

Function "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".

When using functions implemented from "ot.vehicle_script", use following syntax:

ot.vehicle_script:function_name(parameters)

Example:

function ot.vehicle_script:init_chassis(param) 
	local 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,
	};
	
	self:register_event("vehicle/engine/reverse", reverse_action);

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

end

Wheel structure

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

You can find wheel parameters here

Action handlers

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

You can find information about action handlers here.

When using action handlers in Lua, you have to specify the "handler", using an existing function:

self:register_event("vehicle/engine/reverse", reverse_action); 

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" function, 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

self: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" function 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 here.

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

Function "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 "self" keyword (with dot). This ensures that changes affect only the specific instance of your object/model where the code is being executed.

Additionally, "self:" (with colon) is used to reference Outerra's internal interface, which provides various methods that can be invoked using syntax:

self:method_name()

Note: "self:" (with colon) passes self as the first parameter - "self:some_function()" is the same as "self.some_function(self)".

You can find more information about these member functions and about vehicle interface here.

function ot.vehicle_script:init_vehicle()	
	self.started = false;
	self:set_fps_camera_pos({x = -0.4, y = 0.0, z = 1.2});
end

Fps camera position should be defined in "init_vehicle" function.


update_frame

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

function ot.vehicle_script:update_frame(dt, engine, brake, steering, parking)

end

Function 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

Function "simulation_step" can be used instead of "update_frame". The difference is, that this function is invoked 60 times per second and has only parameter dt.

Note: this function can not be used in aircraft interface.

function ot.vehicle_script:simulation_step(dt)

end

Note: for debugging purposes, you can use "log()" to write info on console. For example, in "update_frame" function, you can write the brake value on console each frame.

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 mod files tutorial.

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_lua\packages\tutorial_car_lua)

In .lua 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.

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

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')).

Function takes "self" as parameter, to be able to refer to the current instance of an object.

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

function engine_action(self)
	-- Toggle the "started" value each time this function is called
	self.started = not self.started;
	-- Write fading message on the screen
	self:fade(self.started and "Engine start" or "Engine stop");
    
	-- To not apply force on wheels, when the engine has stopped 
	if self.started == false then
            self:wheel_force(wheels.FLwheel, 0);
            self:wheel_force(wheels.FRwheel, 0);
	end
end

Create function reverse_action

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

Can take "v" as parameter through action handler (not used in this function).

function reverse_action(self, v)
	-- Switch the direction every time this function is called
	self.eng_dir = (self.eng_dir >= 0) and -1 or 1;
	self:fade(self.eng_dir > 0 and "Forward" or "Reverse");
end


Create function init_chassis

function ot.vehicle_script:init_chassis()	

end

In function init_chassis

Define physical wheel parameters.

local 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 = self:add_wheel('wheel_l0', wheel_params ); -- front left wheel (will have ID 0)
wheels.FRwheel = self:add_wheel('wheel_r0', wheel_params ); -- front right wheel (will have ID 1)
wheels.RLwheel = self:add_wheel('wheel_l1', wheel_params ); -- rear left wheel (will have ID 2)
wheels.RRwheel = self: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.

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

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

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 function init_vehicle

function ot.vehicle_script:init_vehicle()

end

In function init_vehicle

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

self.started = false;
self.eng_dir = 1;
self.braking_power = 0;

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 )

self:set_fps_camera_pos({x = -0.4, y = 0.0, z = 1.3});

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

Create function update_frame

function ot.vehicle_script:update_frame(dt, engine, brake, steering, parking)

end

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)

local kmh = math.abs(self:speed() * 3.6);

Calculate force and direction (which will be applied on wheels to move the car, when the car has started)

For calculations, you can use math library.

if self.started == true then 
	-- Calculate force, which will be applied on wheels to move the car
	local redux = self.eng_dir >= 0 and 0.2 or 0.6;
	engine = ENGINE_FORCE * math.abs(engine);
	-- Determine the force value based on whether the car should move forward or backward
	local force = (kmh >= 0) == (self.eng_dir >= 0) and (engine / (redux * kmh + 1)) or engine;
	-- Add wind resistance
	force = force - FORCE_LOSS;
	-- Make sure, that force can not be negative
	force = math.max(0.0, math.min(force, engine));
	-- Calculate force and direction, which will be used to add force to wheels
	engine = force * self.eng_dir;
end

Apply propelling force on given wheel, for that 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)

	self:wheel_force(wheels.FLwheel, engine);
	self:wheel_force(wheels.FRwheel, engine);

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;	
self:steer(wheels.FLwheel, steering);	-- front left wheel
self:steer(wheels.FRwheel, steering);	-- front right wheel

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

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 parking ~= 0 then 
        -- Apply full braking force when the parking brake is engaged 
        self.braking_power = BRAKE_FORCE; 
    elseif brake ~= 0 then
        -- Apply proportional braking force when the regular brake is engaged
        self.braking_power = brake * BRAKE_FORCE;
    else 
        self.braking_power = 0;
    end

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

Use the self.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
self.braking_power = self.braking_power + 200;

self:wheel_brake(-1, self.braking_power);

Version 2 - Bone rotation and movement (using geomob)

This tutorial focused on rotating/moving bones using geomob.

Create additional global variables

SPEED_GAUGE_MIN = 10.0;
RAD_PER_KMH = 0.018325957;

-- Create table containing bones/joints
bones = {
    steer_wheel = -1, 
    speed_gauge = -1, 
    accel_pedal = -1, 
    brake_pedal = -1, 
    driver_door = -1,
};

Create function door_action

Create additional function 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).

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

function door_action(self, v)
    local door_dir = {z = -1};
    -- Multiplied with 1.5 to fully open the door
    local door_angle = v * 1.5;

    self.geom:rotate_joint_orig(bones.driver_door, door_angle, door_dir);
end

In function init_chassis

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

parameter - bone name

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

Declare additional action handler for opening/closing door

-- open/close driver's door (when 'O' is pressed)
self:register_axis("vehicle/controls/open", {minval = 0, maxval = 1, center = 0, vel = 0.6}, door_action ); 

In function 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 instance geometry interface

parameter - ID of geometry object (default 0)

self.geom = self:get_geomob(0);	

In function update_frame

Define additional local variables

local brake_dir = {x = 1};
-- Brake pedal rotation angle will depend on the brake value
local brake_angle = brake * 0.4;	
-- You can also use more than one axis
local accel_dir= {y = (-engine * 0.02), z = (-engine * 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
self.geom:rotate_joint_orig(bones.brake_pedal, brake_angle, brake_dir);
-- move accelerator pedal
self.geom:move_joint_orig(bones.accel_pedal, accel_dir)
-- Rotate speed gauge
if kmh > SPEED_GAUGE_MIN then
        self.geom:rotate_joint_orig(bones.speed_gauge, (kmh - SPEED_GAUGE_MIN) * RAD_PER_KMH, {x = 0,y = 1,z = 0});    
end
	
self.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.

self:animate_wheels();

Version 3 - Lights

To add lights, you have to do following steps.

Create object containing light related members

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

self:light_mask(light_entity.rev_mask, self.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)

Create function passing_lights_action

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

function passing_lights_action(self, v)
    self:light_mask(0xf, v == 1);
end

Create function main_lights_action

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

function main_lights_action(self, v)
    self:light_mask(0x3, v == 1, light_entity.main_light_offset);
end

Create function turn_lights_action

Function used to toggle the left/right turn lights

function turn_lights_action(self, v)
    if v == 0 then
        self.left_turn = 0;
        self.right_turn = 0;
    elseif v < 0 then
        self.left_turn = 1; 
        self.right_turn = 0;
    else
        self.left_turn = 0;
        self.right_turn = 1; 
    end
end

Create function emergency_lights_action

Function used to toggle the emergency lights

function emergency_lights_action(self, v)
    self.emer = self.emer == 0 and 1 or 0; 
end

In function init_chassis

Define light parameters and assign them to "light_props".

While defining light parameters, you can use parameters specified here.

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

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})
self:add_spot_light({x = -0.55, y = 2.2, z = 0.68}, {y = 1}, light_props);  -- left front light
self:add_spot_light({x = 0.55, y = 2.2, z = 0.68}, {y = 1}, light_props);   -- right front light

Add tail lights

-- Change the light properties in "lightProp" and use them for another lights
light_props = { size = 0.07, angle = 160, edge = 0.8, fadeout = 0.05, range = 150, color = { x = 1.0 } };
-- add tail lights
self:add_spot_light({x = -0.05, y = -0.06, z = 0}, {y = 1}, light_props, "tail_light_l0");  -- left tail light
self:add_spot_light({x = 0.05, y = -0.06, z = 0}, {y = 1}, 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, fadeout = 0.05, range = 100, color = { x = 1.0 } };
local brake_light_offset =  
self:add_spot_light({x = -0.43, y = -2.11, z = 0.62}, {y = -1}, light_props);   -- left brake light (0b01)
self:add_spot_light({x = 0.43, y = -2.11, z = 0.62}, {y = -1}, 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 light parameters and bitmasking on Outerra wiki in Light parameters

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

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.

For bit shifting, you can use bit library

light_entity.brake_mask = bit.lshift(3, 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, fadeout = 0.05, range = 100 };
local rev_light_offset =
self:add_spot_light({x = -0.5, y = -2.11, z = 0.715}, {y = -1}, light_props);	 --left reverse light (0b01)
self:add_spot_light({x = 0.5, y = -2.11, z = 0.715}, {y = -1}, light_props);	 --right reverse light (0b10)
-- Decimal system used
light_entity.rev_mask = bit.lshift(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 I used "add_point_light" function, 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, fadeout = 0, color = {x = 0.4, y = 0.1, z = 0}, range = 0.004,  intensity = 1 };
local turn_light_offset =
self:add_point_light({x = -0.71, y = 2.23, z = 0.62}, light_props); 	-- left front turn light (0b0001)
self:add_point_light({x = -0.66, y = -2.11, z = 0.715}, light_props); 	-- left rear turn light (0b0010)
self:add_point_light({x = 0.71, y = 2.23, z = 0.62}, light_props);  	-- right front turn light (0b0100)
self: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 = bit.lshift(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 = bit.lshift(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, fadeout = 0.05, range = 110 };
light_entity.main_light_offset = 
self:add_spot_light({x = -0.45, y = 2.2, z = 0.68}, {y = 1}, light_props);  -- left main light
self:add_spot_light({x = 0.45, y = 2.2, z = 0.68}, {y = 1}, light_props);   -- right main light

**Add following action handlers**

```lua
-- Handle this action, when passing lights button is pressed ('L')
self:register_axis("vehicle/lights/passing", {minval = 0, maxval = 1, vel = 10, center = 0, positions = 2 }, passing_lights_action);

-- This action is handled, when you press Ctrl + L 
self:register_axis("vehicle/lights/main", {minval = 0, maxval = 1, vel = 10, center = 0, positions = 2 }, main_lights_action);

-- 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. 
self:register_axis("vehicle/lights/turn", {minval = -1, maxval = 1, vel = 10, center = 0 }, turn_lights_action);

-- Handle this action, when emergency lights buttons are pressed ('Shift' + 'W')
self:register_event("vehicle/lights/emergency", emergency_lights_action);
***
### In function init_vehicle
**Initialize additional variables**
```lua
self.time = 0;
self.left_turn = 0;
self.right_turn = 0;
self.emer = 0;

In function 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.

self: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 self.left_turn or self.right_turn or self.emer then
-- When turn/emergency lights are turned on, calculate blinking time (in this case between 0 and 1) for turn signal lights
	self.time = (self.time + dt) % 1;
        
        -- Checks, to get boolean values (following light_mask conditions need boolean true/false values)
        local left_turn_active = self.left_turn == 1;
        local right_turn_active = self.right_turn == 1; 
        local emer_active = self.emer == 1;
        -- For turn lights blinking effect
        local blink = self.time > 0.47 and true or false;
        
	-- 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 (true or false)
	self:light_mask(light_entity.turn_left_mask, (blink and (left_turn_active or emer_active)));
	self:light_mask(light_entity.turn_right_mask, (blink and (right_turn_active or emer_active)));
else
	-- To turn off the active turn lights
	self:light_mask(light_entity.turn_left_mask, false);
	self:light_mask(light_entity.turn_right_mask, false);
        self.time = 0;
end

Version 4 - Sounds

To add sounds, you have to do following steps.

Create object containing sound related members

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)

local sound_gain = self.current_camera_mode == 0 and 0.5 or 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)

self.snd:set_gain(sound_entity.src_eng_start_stop, sound_gain);

Set reference distance value, based on the camera mode

local ref_distance = self.current_camera_mode == 0 and 0.25 or 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)

self.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 self.started == true then 
    self.snd:play_sound(sound_entity.src_eng_start_stop, sound_entity.snd_starter);
else 
    self.snd:stop(sound_entity.src_eng_running);
    self.snd:play_sound(sound_entity.src_eng_start_stop, sound_entity.snd_eng_stop);
        
    self:wheel_force(wheels.FLwheel, 0);
    self:wheel_force(wheels.FRwheel, 0);
end

Note: previously this was "if self.started == false" statement, but it was changed for better readability


In function 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 = self:load_sound("Sounds/starter.ogg");		-- will have ID 0
sound_entity.snd_eng_running = self:load_sound("Sounds/eng_running.ogg");	-- will have ID 1
sound_entity.snd_eng_stop = self: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 = self:add_sound_emitter("exhaust_0_end");	-- will have ID 0
sound_entity.src_eng_running = self:add_sound_emitter("exhaust_0_end");	    -- will have ID 1

In function init_vehicle

Get sound interface, using "sound" function.

self.snd = self:sound();

Set initial sound values

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

In function 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 )

self.current_camera_mode = self: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 self.previous_cam_mode ~= self.current_camera_mode then   
    local ref_distance;
    -- Choose reference distance, based on current camera mode
    if self.current_camera_mode == 0 then
        ref_distance = 0.25;
        else
        ref_distance = 1;
    end
    -- Set reference distance
    self.snd:set_ref_distance(sound_entity.src_eng_running, ref_distance);

    -- set self.previous_cam_mode to current camera mode
    self.previous_cam_mode = self.current_camera_mode;
end

Inside the "if self.started == true" 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 self.started == true then
...
    if self.snd:is_playing(sound_entity.src_eng_start_stop) then
        engine = 0;
    else
        local rpm = self:max_rpm();
        local speed_modulation = kmh/40 + math.abs(rpm/200.0);
        local pitch_gain_factor= rpm > 0 and math.floor(speed_modulation) or 0;
        local pitch_gain= speed_modulation + (0.5 * pitch_gain_factor) - pitch_gain_factor;
        self.snd:set_pitch(sound_entity.src_eng_running, (0.5 * pitch_gain) + 1.0);

        -- Set gain
        self.snd:set_gain(sound_entity.src_eng_running, (0.25 * pitch_gain) + 0.5);    

        if self.snd:is_looping(sound_entity.src_eng_running) == false then
            self.snd:play_loop(sound_entity.src_eng_running, sound_entity.snd_eng_running);
        end
    end
end  

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:

self.snd:enqueue_loop(sound_entity.src_eng_running, sound_entity.snd_eng_running);

Test

Clone this wiki locally