Tutorial ‐ Interactive scripting (Lua) - Outerra/anteworld GitHub Wiki

This is short tutorial on scripting functionality for interactive controls in Lua.

It is build on the original DA40-NG aircraft ( Tutorial 2 ‐ Aircraft (Lua) ), we recommed to review that one first.

You can find more info on interactive controls on Outerra wiki in part Interactive .

Changes in original DA40-NG.lua code

Declare action variables, which will be used to store the id of action handlers

implements("ot.aircraft_script")

PI = 3.14159265358979323846;

bones = {
        propeller = -1,
        wheel_front = -1,
        wheel_right = -1,
        wheel_left = -1,
        elevator_right = -1,
        elevator_left = -1,
        rudder = -1,
        aileron_right = -1,
        aileron_left = -1,
        rudder_pedal_left = -1,
        rudder_pedal_right = -1,
        brake_pedal_left = -1,
        brake_pedal_right = -1,
        throttle_handle = -1,
        flap_left = -1,
        flap_right = -1,  
};

meshes = {
        prop_blur = -1,
	blade_one = -1,
	blade_two = -1,
	blade_three = -1,
};

sounds = {
        rumble = -1,
        eng_int = -1,
        eng_ext = -1,
        prop_int = -1,
        prop_ext = -1,
};

sources = {
        rumble_int = -1,
        rumble_ext = -1,
        eng_int = -1,
        eng_ext = -1,
        prop_int = -1,
        prop_ext = -1,
};

-- Declare action variables
actions = {
        act_throttle_lever = -1,
        act_rudder_pedal_L = -1,
        act_rudder_pedal_R = -1,
        act_brake_pedal_L = -1,
        act_brake_pedal_R = -1,
};

Modify existing action handling functions.

function clamp(val, min, max) 
	if val < min then return min; 
	    elseif val > max then return max; 
	    else return val;
        end
end

function engine(self)
        self.started = not self.started;
    
        if self.started == true then
            self.jsbsim:set_property('propulsion/starter_cmd', 1);
            self.jsbsim:set_property('propulsion/magneto_cmd', 1);
        else
            self.jsbsim:set_property('propulsion/starter_cmd', 0);
            self.jsbsim:set_property('propulsion/magneto_cmd', 0);
        end
end

function landing_lights_action(self, v)  
        self:light_mask( 0x3, v > 0);
end

function nav_lights_action(self, v) 
	self:light_mask( 0x3, v > 0, self.nav_light_offset)
end

-- Aileron action (keyboard input 'A' and 'D'), and also knob ( controlled in interactive mode with "roll_lever")
function aileron_action(self, v) 
        self.jsbsim:set_property('fcs/aileron-cmd-norm', v);
    
        -- Bone animations were moved here from update_frame to avoid bugs (e.g. when the knob is grabbed in interactive mode, it is in conflict with the bone position setting/animating through script)
        self.geom:rotate_joint_orig(bones.aileron_right, self.jsbsim:get_property('fcs/right-aileron-pos-rad'), {x = -1});
        self.geom:rotate_joint_orig(bones.aileron_left, self.jsbsim:get_property('fcs/left-aileron-pos-rad'), {x = 1});
end

-- Elevator action (keyboard input 'W' and 'S')
function elevator_action(self, v)
        self.jsbsim:set_property('fcs/elevator-cmd-norm', -v);
    
        -- Bone animations were moved here from update_frame to avoid bugs (e.g. when the knob is grabbed in interactive mode, it is in conflict with the bone position setting/animating through script)
        self.geom:rotate_joint_orig(bones.elevator_right, self.jsbsim:get_property('fcs/elevator-pos-rad'), {x = 1});
        self.geom:rotate_joint_orig(bones.elevator_left, self.jsbsim:get_property('fcs/elevator-pos-rad'), {x = 1});
    
end

-- Brake action (keyboard input 'B')
function brake_action(self, v)
        -- Previous code for setting properties was moved to the brake pedals knobs, and those knobs are set instead (they then set the properties)
        -- Set target values of left and right brake pedal actions
        self:set_action_value(actions.act_brake_pedal_L, v, false)
        self:set_action_value(actions.act_brake_pedal_R, v, false)
end

Add functions for handling actions

Method "set_action_value" was used in this case, but method "set_instant_action_value" can be also used.

Note: both will rotate/move the interactive elements in the model in game.

Method "set_action_value" is used for setting the target value of the action handler and executing it's code

  • 1.param - action id (returned from methods "register_handler", "register_axis", etc., when defining them )
  • 2.param - target value, to which the action should go (the value is not set instantly, the value of the action handler changes based on it's own velocity and acceleration, until it reaches the target value )
  • 3.param - used in "set_action_value" method, to determine, if the value should be set (if true, centering of the action is disabled)

Funtion "set_instant_action_value" is mainly used for setting the target value of the action handler without executing it's code (when "notify" is false)

  • 1.param - action id (returned from methods "register_handler", "register_axis", etc., when defining them )
  • 2.param - target value, to which the action should go (the value is not set instantly, the value of the action handler changes based on it's own velocity and acceleration, until it reaches the target value )
  • 3.param - used in "set_instant_action_value" to determine, if the action should be invoked or not.

If "invoked" is true, the code in the action handler is executed (same as using "set_action_value")

If "invoked" is false, the action handler's value is changed, without executing it's code (useful when working with interactive knobs, because changing the knob's action value means, that the knob (elements like switch, lever, etc.) in the model will move/rotate)

Note: in this case, the "act_rudder_pedal_R" needs to be set to positive value and "act_rudder_pedal_L" to negative value, so that they will be animated correctly (interactive model elements are animated, when their value changes).


-- Aileron action (keyboard input 'A' and 'D'), and also knob ( controlled in interactive mode with "roll_lever")
function aileron_action(self, v) 
        self.jsbsim:set_property('fcs/aileron-cmd-norm', v);
    
        -- Bone animations were moved here from update_frame to avoid bugs (e.g. when the knob is grabbed in interactive mode, it is in conflict with the bone position setting/animating through script)
        self.geom:rotate_joint_orig(bones.aileron_right, self.jsbsim:get_property('fcs/right-aileron-pos-rad'), {x = -1});
        self.geom:rotate_joint_orig(bones.aileron_left, self.jsbsim:get_property('fcs/left-aileron-pos-rad'), {x = 1});
end

-- Elevator action (keyboard input 'W' and 'S')
function elevator_action(self, v)
        self.jsbsim:set_property('fcs/elevator-cmd-norm', -v);
    
        -- Bone animations were moved here from update_frame to avoid bugs (e.g. when the knob is grabbed in interactive mode, it is in conflict with the bone position setting/animating through script)
        self.geom:rotate_joint_orig(bones.elevator_right, self.jsbsim:get_property('fcs/elevator-pos-rad'), {x = 1});
        self.geom:rotate_joint_orig(bones.elevator_left, self.jsbsim:get_property('fcs/elevator-pos-rad'), {x = 1});
    
end

-- Brake action (keyboard input 'B')
function brake_action(self, v)
        -- Previous code for setting properties was moved to the brake pedals knobs, and those knobs are set instead (they then set the properties)
        -- Set target values of left and right brake pedal actions
        self:set_action_value(actions.act_brake_pedal_L, v, false)
        self:set_action_value(actions.act_brake_pedal_R, v, false)
end

-- Add functions for handling actions

-- Throttle action (keyboard inputs 'PgUp' and 'PgDn')
function throttle_action(self, v)
        self:set_action_value(actions.act_throttle_lever, v, true);
end

-- Rudder action (keyboard inputs 'Z' and 'X')
function rudder_action(self, v)
        -- Based on, if the value is moving in positive or negative direction (as the rudder values are moving between -1 and 1), set the value of corresponding action handler.
        -- If the value is moving in positive direction ('X' was pressed), set value "v" (this handler's value) to the action handler "act_rudder_pedal_R" (action will also be called)
        -- If the value is moving in negative direction ('Z' was pressed), set value "-v" (this handler's value, but negative) to the action handler "act_rudder_pedal_R" (action will also be called)
        self:set_action_value(v > 0 and actions.act_rudder_pedal_R or actions.act_rudder_pedal_L, v > 0 and v or -v, false)
end

-- Knobs 

-- Throttle lever interactive knob 
function throttle_knob(self, v)
        -- Set throttle jsbsim property
        self.jsbsim:set_property('fcs/throttle-cmd-norm', v);
        -- Write fading message on the screen, showing the throttle value
        self:fade("Throttle: " .. math.floor(v * 100) .. "%")
end

-- Rudder left pedal interactive knob 
function rudder_knob_L(self, v)
        -- Set rudder jsbsim property 
        self.jsbsim:set_property('fcs/rudder-cmd-norm', v);
end

-- Rudder right pedal interactive knob 
function rudder_knob_R(self, v)
        -- Set rudder jsbsim property 
        self.jsbsim:set_property('fcs/rudder-cmd-norm', -v);
end

-- Brake left pedal interactive knob 
function brake_knob_L(self, v)
        -- Store braking value
        self.braking = v;
        -- Set left brake jsbsim property 
        self.jsbsim:set_property('fcs/left-brake-cmd-norm', v);
end

-- Brake right pedal interactive knob 
function brake_knob_R(self, v)
        -- Store braking value
        self.braking = v;
        -- Set right brake jsbsim property 
        self.jsbsim:set_property('fcs/right-brake-cmd-norm', v);
end

Register handlers

Knobs are called either when the knob is controlled in interactive mode (for example when throttle lever is grabbed and moved), or when their value is changed from script, using method "set_action_value" of "set_instant_action_value".

Note: in this case the pitch/roll handle rotates without the need of scripting (animating through geomob), because it's binded with actions in the model. Similarly, the throttle lever, pedals, and switches in the model will still animate even if not defined in the script, but they won't perform the intended functionality.

If knob does have defined action name, then that name is used as the action name.

Example: this model has interactive knob called "roll_lever", which has action name defined as "air/controls/aileron" (defined action in air.cfg IOMap configuration) assigned to it, therefore it is referenced to as "air/controls/aileron".

If knob does not have defined action name, automatic name is assigned, so that it can be refered by script. Format of automatic generated name is "knob_action_[bone_name]", for example for bone attribute with name (knob name) "pedal_left" it is "knob_action_rudder_pedal_left".

Note: when knob handler's value is changed through script, the knob (interactive element in model) moves to the set position. This way the knobs can be "animated" without using animating functionality from geomob...

function ot.aircraft_script:init_chassis()
	bones.propeller = self:get_joint_id('propel');
	bones.wheel_right = self:get_joint_id('right_wheel');
	bones.wheel_front = self:get_joint_id('front_wheel');
	bones.wheel_left = self:get_joint_id('left_wheel');
	bones.elevator_right = self:get_joint_id('elevator_right'); 
	bones.elevator_left = self:get_joint_id('elevator_left');
	bones.rudder = self:get_joint_id('rudder');
	bones.aileron_right = self:get_joint_id('aileron_right'); 
	bones.aileron_left = self:get_joint_id('aileron_left');
        bones.flap_left = self:get_joint_id('flap_left');
        bones.flap_right = self:get_joint_id('flap_right');
	bones.rudder_pedal_left = self:get_joint_id('rudder_pedal_left');
	bones.rudder_pedal_right = self:get_joint_id('rudder_pedal_right');
	bones.brake_pedal_left = self:get_joint_id('brake_pedal_left');
	bones.brake_pedal_right = self:get_joint_id('brake_pedal_right');
	bones.throttle_handle = self:get_joint_id('throttle_lever');
	
	meshes.prop_blur = self:get_mesh_id('propel_blur');
	meshes.blade_one = self:get_mesh_id('main_blade_01#0@0');
	meshes.blade_two = self:get_mesh_id('main_blade_02#0@0');
	meshes.blade_three = self:get_mesh_id('main_blade_03#0@0');

	local light_params = {color = {x = 1, y = 1, z = 1}, angle = 100, size = 0.1, edge = 0.25, intensity = 5, fadeout = 0.05};
	self:add_spot_light({x = 4.5, y = 1.08, z = 0.98}, {x = -0.1, y = 1, z = 0.3}, light_params);
	self:add_spot_light({x = -4.5, y = 1.08, z = 0.98}, {x = 0.1, y = 1, z = 0.3}, light_params);


	light_params = {color = {x = 0, y = 1, z = 0}, size = 0.035, edge = 1, range = 0.0001, intensity = 20, fadeout = 0.1};
	self.nav_light_offset =
	self:add_point_light({x = 5.08, y = 0.18, z = 1.33}, light_params);
	light_params.color = {x = 1, y = 0, z = 0};
	self:add_point_light({x = -5.08, y = 0.18, z = 1.33}, light_params);
	

        sounds.rumble = self:load_sound("Sounds/engine/engn1.ogg");
	sounds.eng_ext = self:load_sound("Sounds/engine/engn1_out.ogg");
	sounds.prop_ext = self:load_sound("Sounds/engine/prop1_out.ogg");

	sounds.eng_int = self:load_sound("Sounds/engine/engn1_inn.ogg"); 
	sounds.prop_int = self:load_sound("Sounds/engine/prop1_out.ogg");
	
        sources.rumble_int = self:add_sound_emitter_id(bones.propeller, -1, 0.5);
	sources.eng_int = self:add_sound_emitter_id(bones.propeller, -1, 0.5);
	sources.prop_int = self:add_sound_emitter_id(bones.propeller, -1, 0.5);
    
        sources.rumble_ext = self:add_sound_emitter_id(bones.propeller, 1, 3);
	sources.eng_ext = self:add_sound_emitter_id(bones.propeller, 1, 3);
	sources.prop_ext = self:add_sound_emitter_id(bones.propeller, 1, 3);
    
	self:register_axis("air/lights/landing_lights", {minval = 0, maxval = 1, vel = 10, center = 0 }, landing_lights_action );
        self:register_axis("air/lights/nav_lights", {minval = 0, maxval = 1, vel = 10, center = 0 }, nav_lights_action );
   	  
        self:register_event("air/engines/on", engine);
	self:register_axis("air/controls/elevator", { minval = -1, maxval = 1, center = 0.5, vel = 0.5, positions = 0 }, elevator_action );
        self:register_axis("air/controls/brake", { minval = 0}, brake_action );
        self:register_axis("air/controls/aileron", { minval = -1, maxval = 1, center = 0.5, vel = 0.5, positions = 0 }, aileron_action );

        -- Actions 
    
	self:register_axis("air/engines/throttle", {center = 0.0}, throttle_action );
        self:register_axis("air/controls/rudder", {}, rudder_action );
       
        -- Knobs

        -- Throttle lever interactive knob 
        actions.act_throttle_lever = self:register_axis("air/engine/throttle", {}, throttle_knob );
    
        -- Rudder left pedal interactive knob 
        actions.act_rudder_pedal_L = self:register_handler("knob_action_rudder_pedal_left", rudder_knob_L );
        -- Rudder right pedal interactive knob 
        actions.act_rudder_pedal_R = self:register_handler("knob_action_rudder_pedal_right", rudder_knob_R );
        -- Brake left pedal interactive knob 
        actions.act_brake_pedal_L = self:register_handler("knob_action_brake_pedal_left", brake_knob_L );
        -- Brake right pedal interactive knob 
        actions.act_brake_pedal_R = self:register_handler("knob_action_brake_pedal_right", brake_knob_R );

	return {
		mass = 1310,
		com = {x = 0.0, y = 0.0, z = 0.2},
	};
end

Note: in case of throttle lever, the knob has action name "air/engine/throttle", even though that action does not exist in air.cfg IOMap configuration (has "engine" instead of "engines" in name). Therefore handler for throttle action ("air/engines/throttle") has been previously added, so that it can react on changes in the throttle value. (for example when controlled through keyboard input).

Note: List of existing interactive knobs on the model can be found in Outerra -> Plugins -> Entity properties -> Bone attributes (model has to be selected).

Rest of the code

Rest of the code remains almost unchanged, the only difference is, that the elevator, ailerons, rudder pedals, throttle handle and brake pedals animations were either removed (if there is no need to animate other parts, than the knobs, e.g. throttle handle and brake pedals), or moved into handlers, to avoid bugs (e.g. when the knob is grabbed in interactive mode, it is in conflict with the bone position setting/animating through script).

function ot.aircraft_script:initialize()
	self.geom = self:get_geomob(0);
	self.jsbsim = self:jsb();
	self.snd = self:sound();
	
	self:set_fps_camera_pos({x = 0, y = 1, z = 1.4});
    
        self.started = false;
        self.braking = 0;
	
	self.jsbsim:set_property('propulsion/starter_cmd', 0);
	self.jsbsim:set_property('propulsion/magneto_cmd', 0);
	self.jsbsim:set_property('fcs/throttle-cmd-norm[0]', 0);
        self.jsbsim:set_property('fcs/mixture-cmd-norm', 0);
	self.jsbsim:set_property('fcs/right-brake-cmd-norm', 0);
 	self.jsbsim:set_property('fcs/left-brake-cmd-norm', 0);
    
        self.snd:set_pitch(sources.rumble_ext, 1);
        self.snd:set_pitch(sources.rumble_int, 1);
        self.snd:set_pitch(sources.eng_ext, 1);
        self.snd:set_pitch(sources.eng_int, 1);
        self.snd:set_pitch(sources.prop_ext, 1);
        self.snd:set_pitch(sources.prop_int, 1);
    
        self.snd:set_gain(sources.rumble_ext, 1);
        self.snd:set_gain(sources.rumble_int, 1);
        self.snd:set_gain(sources.eng_ext, 1);
        self.snd:set_gain(sources.eng_int, 1);
        self.snd:set_gain(sources.prop_ext, 1);
        self.snd:set_gain(sources.prop_int, 1);
end

function ot.aircraft_script:update_frame(dt)
	local propeller_rpm = self.jsbsim:get_property('propulsion/engine[0]/propeller-rpm');
	local wheel_speed = self.jsbsim:get_property('gear/unit[0]/wheel-speed-fps');

        self.geom:rotate_joint(bones.propeller, dt * (2 * PI) * (propeller_rpm), {y = 1});
	self.geom:rotate_joint(bones.wheel_front, dt * PI * (wheel_speed / 5), {x = -1});
	self.geom:rotate_joint(bones.wheel_right, dt * PI * (wheel_speed / 5), {x = -1});
	self.geom:rotate_joint(bones.wheel_left, dt * PI * (wheel_speed / 5), {x = -1});
	self.geom:rotate_joint_orig(bones.rudder, self.jsbsim:get_property('fcs/rudder-pos-rad'), {z = -1});
	self.geom:rotate_joint_orig(bones.flap_left, self.jsbsim:get_property('fcs/flap-pos-rad'), {x = 1,y = 0,z = 0});
        self.geom:rotate_joint_orig(bones.flap_right, self.jsbsim:get_property('fcs/flap-pos-rad'), {x = 1,y = 0,z = 0});

	self.geom:set_mesh_visible_id(meshes.prop_blur, propeller_rpm > 200.0);
	self.geom:set_mesh_visible_id(meshes.blade_one, propeller_rpm < 300.0);
	self.geom:set_mesh_visible_id(meshes.blade_two, propeller_rpm < 300.0);
	self.geom:set_mesh_visible_id(meshes.blade_three, propeller_rpm < 300.0);
	
	if propeller_rpm > 0 then
		if self:get_camera_mode() == 0 then 	
			self.snd:stop(sources.eng_ext); 
                        self.snd:stop(sources.prop_ext);
                        self.snd:stop(sources.rumble_ext);
            
			self.snd:set_pitch(sources.eng_int, clamp(1 + propeller_rpm/4000, 1, 2));
			self.snd:set_gain(sources.eng_int, clamp(propeller_rpm/5000, 0, 0.5));
			
			if self.snd:is_playing(sources.eng_int) == false then 
				self.snd:play_loop(sources.eng_int, sounds.eng_int);
			end
            
			self.snd:set_gain(sources.prop_int, clamp(propeller_rpm/7000, 0, 0.5));
			
			if self.snd:is_playing(sources.prop_int) == false then 
				self.snd:play_loop(sources.prop_int, sounds.prop_int);
                        end
			
			self.snd:set_gain (sources.rumble_int, clamp(propeller_rpm/6000, 0, 0.5));
			
                        if self.snd:is_playing(sources.rumble_int) == false then
				self.snd:play_loop(sources.rumble_int, sounds.rumble);
                        end
		else 
			self.snd:stop(sources.eng_int);
			self.snd:stop(sources.prop_int);
			self.snd:stop(sources.rumble_int);
            
			self.snd:set_pitch(sources.eng_ext, clamp(1 + propeller_rpm/1000, 1, 3));
			self.snd:set_gain (sources.eng_ext, clamp(propeller_rpm/450, 0.05, 2));
			
                        if self.snd:is_playing(sources.eng_ext) == false then 
				self.snd:play_loop(sources.eng_ext, sounds.eng_ext);
                        end
            
			self.snd:set_gain (sources.prop_ext, clamp(propeller_rpm/900, 0, 2));
            
                        if self.snd:is_playing(sources.prop_ext) == false then 
				self.snd:play_loop(sources.prop_ext, sounds.prop_ext);
			end
			
			self.snd:set_gain (sources.rumble_ext, clamp(propeller_rpm/1200, 0, 2));
			
			if self.snd:is_playing(sources.rumble_ext) == false then
				self.snd:play_loop(sources.rumble_ext, sounds.rumble);
			end
		end
	else 
		self.snd:stop(sources.eng_ext);
		self.snd:stop(sources.eng_int);
                self.snd:stop(sources.prop_ext);
		self.snd:stop(sources.prop_int);
		self.snd:stop(sources.rumble_ext);
		self.snd:stop(sources.rumble_int);
        end
    
        if self.started == false and propeller_rpm < 5 then
                self.jsbsim:set_property('fcs/center-brake-cmd-norm', 1);
                self.jsbsim:set_property('fcs/left-brake-cmd-norm', 1);
                self.jsbsim:set_property('fcs/right-brake-cmd-norm', 1);
        elseif self.braking < 0.1 then
                self.jsbsim:set_property('fcs/center-brake-cmd-norm', 0);
                self.jsbsim:set_property('fcs/left-brake-cmd-norm', 0);
                self.jsbsim:set_property('fcs/right-brake-cmd-norm', 0);
        end
end