Tutorial ‐ script minigame (js) - Outerra/anteworld GitHub Wiki
This is tutorial on creating minigame in JavaScript using script_module api.
Include our JavaScript math library, containing wide range of mathematical formulas
$include("/lib/vecmath.js");
Declare and initialize constants, paths, and variables.
// Game settings (Parameters and Flags)
const INVADERS_SPEED = 1;
const MAX_HEARTS = 5;
const MAX_PROJECTILES = 4;
const PROJECTILE_LOAD_TIME = 1;
const TIME_NEAR_END_CURVE = 43;
// Paths to objdefs
const NORMAL_INVADER_PATH = "invader_ships/invader_ship_01";
const SPECIAL_INVADER_PATH = "invader_ships/invader_ship_02";
const CANNON_PATH = "cannon/cannon";
// Fonts
let font_xolonium_50_bold;
let font;
// Images
let projectile_img;
let heart_img;
// Flags for object loading status
let invader_01_loaded;
let invader_02_loaded;
let cannon_loaded;
// Screen-related variables
let screen_size;
let screen_center;
// Time-related variables
let timer;
let last_spawn_time;
// Invader and level management
let invaders_in_line;
let spawning;
let spawned_lines;
let lvl_stage;
let invader_lines_in_group;
let stronger_invaders_to_spawn;
let invader_array;
// Cannon and projectile management
let cannon;
let projectiles_loaded;
let loaded_time;
let cannon_geom;
let barrel_tip_id;
let cannon_pitch_id;
let cannon_entered;
// Game state variables
let hearts_remaining;
let score;
let instances_count;
let invaders_destroyed;
let game_over;
// Location and day time
let is_loc_ready;
let time_set;
// End position trigger
let trigger_sensor;
// Miscellaneous flags
let exit_game;
Create function to draw crosshair on screen.
Method draw_line draws 2D line
- 1. parameter - start position of the line on x axis of the screen
- 2. parameter - start position of the line on y axis of the screen
- 3. parameter - end position of the line on x axis of the screen
- 4. parameter - end position of the line on y axis of the screen
- 5. parameter - color of the line (in range 0-255)
Note: width and smooth can be set with function set_line_params in canvas interface
function draw_crosshair()
{
this.canvas.draw_line(screen_center.x + 10, screen_center.y, (screen_center.x + 30), (screen_center.y), {r: 255, g: 255, b: 255, a: 255});
this.canvas.draw_line(screen_center.x - 10, screen_center.y, (screen_center.x - 30), (screen_center.y), {r: 255, g: 255, b: 255, a: 255});
this.canvas.draw_line(screen_center.x, screen_center.y + 10, (screen_center.x), (screen_center.y + 30), {r: 255, g: 255, b: 255, a: 255});
this.canvas.draw_line(screen_center.x, screen_center.y - 10, (screen_center.x), (screen_center.y - 30), {r: 255, g: 255, b: 255, a: 255});
}
Create function for creating an explosion at a specified location
Use create_solid_particles method from explosions interface, to create explosion of solid particles
- 1. parameter - ECEF world position
- 2. parameter - smoke ejection direction (normalized pos for the upward dir)
- 3. parameter - radius of the emitter area
- 4. parameter - max particle radius
- 5. parameter - ejection speed
- 6. parameter - spread direction dissipation, tangent of the half-angle (default: 0.4)
- 7. parameter - highlight 0:solid, 1+:water spray (default: 0.0)
- 8. parameter - age in seconds at the creation time (default: 0)
- 9. parameter - base particle color (default: {r:0.03, g:0.02, b:0.01})
- 10. parameter - highlight color in rgb (can be >1 for bloom), a: highlight inverse size coefficient (default: {r:40, g:6, b:0, a:10))
Use make_crater method from explosions interface, to create crater on earth surface
- 1. parameter - ECEF world position
- 2. parameter - approximate crater radius
function create_explosion(pos, obj_hit)
{
// Set emit radius for explosions
let emit_radius = 2;
// Create explosion
this.explosions.create_solid_particles(pos, {x: 0, y: 0, z: 1}, emit_radius, 0.04 * Math.max(1.0, Math.log2(emit_radius)), 10, 1);
// Make crater on earth, if earth was hit by projectile
if(!obj_hit)
{
this.explosions.make_crater(pos, emit_radius*6);
}
}
Create function to rotate a vector by a quaternion
function rotate_vector_by_quaternion(v, q)
{
// Convert the vector into a quaternion with a w component of 0, this is necessary for quaternion-vector multiplication.
let vec_quat = {x:v.x, y:v.y, z:v.z, w:0};
// Compute the conjugate of the quaternion q. The conjugate of a quaternion is used to reverse the rotation
let quat_conjugated = Quaternion.conjugate(q);
// Multiply the quaternion q by the vector quaternion, this combines the rotation quaternion with the vector
let qv = Quaternion.mul(q, vec_quat);
// Multiply the result by the conjugate of q, this final multiplication applies the rotation to the vector
let rotated_qv = Quaternion.mul(qv, quat_conjugated);
// Return the rotated vector in quaternion form
return rotated_qv;
}
Create function to spawn invaders
Use create_instance method to create static invaders
- 1. parameter - full model path under packages dir
- 2. parameter - world position of the pivot point
- 3. parameter - orientation of the model
- 4. parameter - if object is persistent(0), colliding(1) or kinematic(2), where:
- 0 - none
- 1 - object is saved to the cache (creates a static object that needs to be activated). This object remains even after game is restarted
- 2 - object is colliding with other objects (creates a collision object)
- 4 - used for static objects that will be moved frequently in realtime
function spawn_invader_line(invader_path, SPECIAL_INVADER_PATH = NORMAL_INVADER_PATH, num_of_stronger_invaders = 0)
{
// If the invader model is not loaded, exit the function early
if (!invader_01_loaded || !invader_01_loaded)
{
return {};
}
// Array to store normal invader instances
let invaders = [];
// Array to store stronger invader instances
let stronger_invaders = [];
// Calculate where to position stronger invaders in the line
// Check if there are stronger invaders to be included in the line
if (num_of_stronger_invaders > 0)
{
// Calculate the spacing between stronger invaders in the line
let spacing = Math.floor(invaders_in_line / num_of_stronger_invaders);
// Loop through the number of stronger invaders to place them at specific positions in the line
for (let i = 0; i < num_of_stronger_invaders; i++)
{
// Calculate the position of each stronger invader in the line
stronger_invaders.push((i * spacing) + Math.floor(spacing / 2));
}
}
// Define the start position and orientation for the invader
let start_loc = {x:-1942405.2868467504,y:5212454.450893189,z:3122019.4702613624};
let start_rot = {x:0.22153694927692413,y:0.4521426856517792,z:0.6030480861663818,w:-0.618725597858429};
// Adjust the starting position of the first 3 waves, so that they are closer on the game start
if(invader_array.length == 0 )
{
start_loc = {x:-1941927.023828117,y:5212336.678825753,z:3122498.8268912034};
start_rot = {x:0.04354477673768997,y:0.5016129612922668,z:0.7856196165084839,w:-0.3595695197582245};
}
else if(invader_array.length == 1)
{
start_loc = {x:-1942146.710236884,y:5212422.649997227,z:3122225.6552367667};
start_rot = {x:0.04354477673768997,y:0.5016129612922668,z:0.7856196165084839,w:-0.3595695197582245};
}
else if(invader_array.length == 2)
{
start_loc = {x:-1942171.8067069917,y:5212432.471147353,z:3122194.4487372655};
start_rot = {x:0.04354477673768997,y:0.5016129612922668,z:0.7856196165084839,w:-0.3595695197582245};
}
// Define a local offset vector (distance between invaders)
let local_offset = {x:-25, y:0, z:0};
// Rotate the local offset vector by the invader rotation quaternion
let world_movement = this.rotate_vector_by_quaternion(local_offset, start_rot);
// Loop to create and position invader instances
for (let i = 0; i < invaders_in_line; i++)
{
// Calculate the new position for the invader by applying the offset
let start_pos = {
x: start_loc.x + i * world_movement.x,
y: start_loc.y + i * world_movement.y,
z: start_loc.z + i * world_movement.z
};
// Initialize a new invader object
let invader = {
instance: null, // Placeholder for the invader's instance
lives : 1, // Default health
special : false, // Whether this invader is special (stronger)
removed : false // Whether this invader has been removed from the scene
};
// If the current index corresponds to a stronger invader, spawn stronger invader and set it's properties
if (stronger_invaders.includes(i))
{
// Spawn special invader
invader.instance = this.create_instance(SPECIAL_INVADER_PATH, start_pos, start_rot, 4);
// Define parameters for special invader
invader.lives = 2;
invader.special = true;
}
else
{
// Spawn normal invader with defaul properties
invader.instance = this.create_instance(invader_path, start_pos, start_rot, 4);
}
// If the instance creation failed, return an empty object
if(!invader.instance)
{
return {};
}
// Add the invader to the array of invaders
invaders.push(invader);
}
// Return object, containing the array of invaders, spawn time and state
return { invaders: invaders, spawn_time: timer};
}
Create function to calculate the number of stronger invaders to be placed in a specific line based on the total number of stronger invaders and the total number of lines.
function calculate_stronger_invaders_per_line(line_index, total_lines, total_stronger_invaders)
{
// Calculate the base number of stronger invaders that each line should receive
let base_count = Math.floor(total_stronger_invaders / total_lines);
// Calculate any leftover stronger invaders that couldn't be evenly divided among the lines.
let extra_invaders = total_stronger_invaders % total_lines;
// If the current line index is less than the number of extra invaders,
// this line gets one extra stronger invader. Otherwise, it just gets the base count
return line_index < extra_invaders ? base_count + 1 : base_count;
}
Create function to convert quaternion to its corresponding axis of rotation
function quaternion_to_axis(q)
{
let Θ = Math.acos(q.w) * 2;
let sinΘ = Math.sin(Θ / 2);
let ax = q.x / sinΘ;
let ay = q.y / sinΘ;
let az = q.z / sinΘ;
return { x: ax, y: ay, z: az };
}
Create function, that get's called on game over
Use method open_window, to open a browser window, loading specified html
- parameter - relative path, with optional query part (e.g. url?param1¶m2 ...)
Recognized tokens for relative path: name - window name width - initial window width height - initial window height x - initial window x position y - initial window y position transp - true or 1 if the window should support transparency
function on_game_over()
{
game_over = true;
this.open_window("www/GameOverScreen.html?width=" + screen_size.x + "&height=" + screen_size.y);
}
Create helper function, that removes remaining spawned invaders from the scene
function remove_spawned_invaders()
{
// Remove invaders
for (let i = 0; i < invader_array.length; i++)
{
for (let j = 0; j < invader_array[i].invaders.length; j++)
{
let invader = invader_array[i].invaders[j];
if (invader.instance && invader.removed == false)
{
invader.instance.remove_from_scene();
}
}
}
}
Create function, which is used to reload the minigame, when "Reload" is pressed in "Game over" HTML menu
function reload_game()
{
// Remove spawned invaders from scene
remove_spawned_invaders()
// Set values to initial state
timer = 0;
last_spawn_time = 0;
score = 0;
spawning = true;
invaders_in_line = 6;
lvl_stage = 1;
spawned_lines = 0;
projectiles_loaded = MAX_PROJECTILES;
hearts_remaining = MAX_HEARTS;
loaded_time = 0;
instances_count = 0;
invader_lines_in_group = 1;
stronger_invaders_to_spawn = 0;
invaders_destroyed = 0;
game_over = false;
time_set = false;
is_loc_ready = false;
exit_game = false;
trigger_sensor = []
invader_array = [];
}
Implement event, that gets triggered, when Esc menu is opened/closed
Use method pause for pausing the game mod
- parameter - true, if mod should be paused
function on_main_menu(is_paused)
{
this.pause(is_paused);
}
Implement event, that is used for communicating between html window and script
This function is called from the html script
function on_set_value_num(a, b, value)
{
// If the function is called with certain combination of parameters 'a' and 'b', do corresponding action
if (a == 0 && b == 0)
{
//Reload game was selected
reload_game();
}
if (a == 0 && b == 1)
{
//Exit game was selected
exit_game = value;
}
}
Event for communicating between html window and script
This function is called from the html script
function on_get_value_num(param_a, param_b, value)
{
// If the function is called with certain combination of parameters 'a' and 'b', do corresponding action
if (param_a == 0 && param_b == 0)
{
// Sends value to html script
return {value: invaders_destroyed};
}
if (param_a == 0 && param_b == 1)
{
return {value: score};
}
}
Add event, which gets called on mouse button press.
Use method launch_tracer from explosions interface, to launch a ballistic tracer/projectile
- 1. parameter - launch position
- 2. parameter - launch speed vector
- 3. parameter - tracer size
- 4. parameter - tracer color
- 5. parameter - fadeout emission reduction parameter for each older point on the trail (default: 0.5)
- 6. parameter - length of the trail in seconds (default: 0.2)
- 7. parameter - time [s] of tracer existence: <=0 means until hitting the ground (default: 0.0)
- 8. parameter - age of the tracer. Affects trail length, fall speed (default: 0)
- 9. parameter - tracer id to reuse (default: 0xffffffffUL)
- 10. parameter - object id to be dragged by tracer (default: ())
- 11. parameter - custom value (default: 0) returns - trace id
function on_mouse_button( mouse_button, state, modifiers )
{
// If button is released, do nothing
if(!state)
{
return;
}
// When left mouse button is pressed
if(mouse_button == 0)
{
// Check if there are projectiles loaded in the cannon
if( projectiles_loaded >= 1)
{
// Get camera direction
let camera_direction = this.get_camera_dir();
// Multiply to set speed
camera_direction.x *=400;
camera_direction.y *=400;
camera_direction.z *=400;
if(cannon)
{
// Get barrel tip bone ECEF position
let barrel_tip_pos_ECEF = cannon_geom.get_joint_ecef_pos(barrel_tip_id);
// Launch projectile
this.explosions.launch_tracer( barrel_tip_pos_ECEF, camera_direction, 100, {x:0.5,y:0.5,z:0.5, w:1})
}
// When all projectiles were loaded, while shooting, set the "loaded_time", so that following projectile starts loading
if(projectiles_loaded == MAX_PROJECTILES)
{
loaded_time = timer;
}
// Decrease loaded projectiles count
projectiles_loaded --;
}
}
}
Implement event, that is called, when an object is preloaded (using preload_object function)
function on_preload_object_done(model_path)
{
// Set flags, when objects are preloaded
if(model_path == NORMAL_INVADER_PATH)
{
invader_01_loaded = true;
}
else if(model_path == SPECIAL_INVADER_PATH)
{
invader_02_loaded = true;
}
else if(model_path == CANNON_PATH)
{
cannon_loaded = true;
}
}
Implement event, which is called before script is reloaded
Note: On script reload, the game state is automatically reloaded, but the spawned objects (cannon and invaders) need to be correctly removed from scene before that happens.
function on_before_reload()
{
// The cannon should be exited before it is removed from the scene.
cannon.exit()
// Remove old cannon
cannon.remove_from_scene()
// Remove spawned invaders from scene
remove_spawned_invaders();
//In js script, method "on_before_reload" should return an array (user data), which is then passed to method "on_after_reload" (can be empty array)...
return [];
}
Initialize minigame.
Event on_initialize from script_module api is used to initialize script, same as init_vehicle from vehicle_script.
Bind the functions to ensure that "this" inside the function refers to the script module object. This way, the methods from the C++ interface can be called from inside the function. Without this binding, calling "this_interface_method()" inside these functions wouldn't work.
Use set_line_params method, to set smooth and width parameters of canvas lines
- 1. parameter - width od the canvas lines
- 2. parameter - smooth size of the canvas lines
Use load_font method ,to load fonts from fnt file
- parameter - path to fnt file
Use load_image method, to load images
- parameter - path to imgset file ()
Use preload_object method, to preload object (event on_preload_object_done() from script_module api is called, when object is preloaded)
Use load_location method, to jump to a location that was saved in game campos
- 1. parameter - save name
- 2. param - if date/time is contained in the location file, apply it
- returns - true if the location was found
Use create_sensor method, to create sensor of sphere shape, This sensor is used as trigger objects on end position (checks, if an invader got to end position)
- 1. parameter - position
- 2. parameter - rotation
- 3. parameter - radius
function on_initialize()
{
this.create_explosion = create_explosion;
this.draw_crosshair = draw_crosshair;
this.rotate_vector_by_quaternion = rotate_vector_by_quaternion;
this.spawn_invader_line = spawn_invader_line;
this.calculate_stronger_invaders_per_line = calculate_stronger_invaders_per_line;
this.quaternion_to_axis = quaternion_to_axis;
this.on_game_over = on_game_over;
// Get explosions interface
this.explosions = this.$query_interface('ot::js::explosions.get');
// Get canvas interface
this.canvas = this.$query_interface('ot::js::canvas.create', true, true);
// Get screen size from world interface
screen_size = this.screen_size();
// Calculate screen center from
screen_center = {x: screen_size.x / 2, y: screen_size.y / 2};
// Set initial values
hearts_remaining = MAX_HEARTS;
projectiles_loaded = MAX_PROJECTILES;
cannon = null;
cannon_geom = null;
game_over = false;
spawning = true;
cannon_entered = false;
time_set = false;
invader_01_loaded = false;
invader_02_loaded = false;
cannon_loaded = false;
is_loc_ready = false;
exit_game = false;
timer = 0;
last_spawn_time = 0;
score = 0;
invaders_in_line = 6;
lvl_stage = 1;
spawned_lines = 0;
loaded_time = 0;
instances_count = 0;
invaders_destroyed = 0;
invader_lines_in_group = 1;
stronger_invaders_to_spawn = 0;
invader_array = [];
trigger_sensor = [];
this.canvas.set_line_params(3,1);
// Load fonts from fnt file
font_xolonium_50_bold = this.canvas.load_font( "ui/xolonium_50_bold.fnt" );
font = this.canvas.load_font( "ui/hud.fnt" );
// Load images
projectile_img = this.canvas.load_image("projectile.imgset/projectile");
heart_img = this.canvas.load_image("heart-icon.imgset/heart");
// Preload objects
this.preload_object(NORMAL_INVADER_PATH);
this.preload_object(SPECIAL_INVADER_PATH);
this.preload_object(CANNON_PATH);
// Jump to a location that was saved in game campos
this.load_location("minigame_location", false);
// Create sensor of sphere shape
// This sensor is used as trigger on end position, which checks, if an invader got to end position
this.create_sensor({x:-1941638.9198791014,y:5211794.163754703,z:3123587.0791307855}, {x: 0, y: 0, z: 0, w: 1}, 100);
// Function needs to return bool
return true;
}
before_simulation_step is called before simulation step, 60 times per second
- 1. parameter - dtns - time delta [ns] since simulation step
- 2. parameter - ns_sim - absolute simulation time [ns]
Method set_time is used, to set time of the day.
- 1. parameter - day of the year
- 2. parameter - time of the day in seconds
- 3. parameter - if true set UTC, false set solar time for current location
Method remove_object is used to remove the invader
- parameter - entity id
Method get_tiggered_sensors returns an array of objects, that triggered the sensor in current frame.
Method get_landed_projectiles returns an array of landed projectiles (tracers) in current frame.
Note: When an object is hit, then the position returned, is relative to model space. Otherwise returns world position.
function before_simulation_step(dtns, ns_sim)
{
// Do not continue on game over
if(game_over == true)
{
return;
}
// When all lives are lost, set game over
else if(hearts_remaining <= 0)
{
game_over = true;
this.on_game_over();
return;
}
// Check if location is ready
// returns - true if last loaded location is ready
is_loc_ready = this.is_location_ready();
// Return if the location is not loaded
if(!is_loc_ready)
{
return;
}
// Set time, if not yet set (to not start the minigame at night...)
if(!time_set)
{
// Set the time of the day
this.set_time(1, 36000, false);
time_set = true;
return;
}
// When cannon is loaded, spawn the cannon, get it's geometry and enter it
if(cannon_loaded && cannon_entered === false)
{
// If cannon is not yet spawned
if (cannon == null)
{
// Spawn cannon
let cannon_start_pos = {x:-1941609.2055510941,y:5212173.27245129,z:3123161.9673035163};
let cannon_start_rot = {x:-0.49617794156074524,y:-0.09487587213516235,z:0.14400778710842133,w:0.8509216904640198};
cannon = this.create_static_instance(CANNON_PATH, cannon_start_pos, cannon_start_rot, 2);
}
// When cannon is spawned
else
{
// Get cannon geometry
cannon_geom = cannon.get_geomob();
// Get cannon bones
if(cannon_geom != null)
{
barrel_tip_id = cannon_geom.get_joint("barrel_tip")
cannon_pitch_id = cannon_geom.get_joint("cannon_pitch");
// Enter cannon
cannon.enter();
cannon_entered = true;
}
}
}
// Do not continue until cannon and invaders models are loaded
if(!cannon || !cannon_geom || !cannon_entered || !invader_01_loaded || !invader_02_loaded)
{
return;
}
// Load projectile after some time
if( timer - loaded_time > PROJECTILE_LOAD_TIME && projectiles_loaded < MAX_PROJECTILES )
{
loaded_time = timer;
projectiles_loaded ++;
}
// For performance reasons, create and check triggered sensor after some time...
if(timer > 15)
{
// Check if invaders got to end position (if they triggered the sensor on end position)
// Method "get_tiggered_sensors" returns an array of objects, that triggered the sensor in current frame
trigger_sensor = this.get_tiggered_sensors();
}
// If an invader triggered the sensor, loop through the invaders in reverse order to find the triggered one, based on it's id
if(trigger_sensor.length > 0)
{
for (let t = trigger_sensor.length - 1; t >= 0; t--)
{
// Get the id of the invader, that triggered the sensor
let triggered_id = trigger_sensor[t].trigger_entity_id;
// Get the invader object from the triggered id
let triggered_invader_obj = this.get_object(triggered_id);
// Check if object was successfully received
if (triggered_invader_obj != null)
{
// Loop through the invaders
outer_loop: for (let i = invader_array.length - 1; i >= 0; i--)
{
let invaders_line = invader_array[i].invaders;
for (let j = invaders_line.length - 1; j >= 0; j--)
{
let invader = invaders_line[j];
// Skip the invaders, that are removed
if (invader.removed == true)
{
continue;
}
// Check if the invader is the one, that triggered the sensor, by comparing the id
if (triggered_invader_obj.scene_id() == invader.instance.scene_id())
{
// Set the "removed" flag
invader.removed = true;
// Remove the invader
// param - entity id
this.remove_object(triggered_id);
// If all invaders in the current line have been removed (the entire invader line is empty), remove the line element (invader line) from the invader_array
if (invaders_line.every(inv => inv.removed))
{
invader_array.splice(i, 1);
}
// Decrease hp
hearts_remaining--;
// If the triggered invader was found, break out of the outer loop, to not continue looping through the arrays
break outer_loop;
}
}
}
}
}
}
// Spawn multiple lines of invaders each second
if(lvl_stage <= 2 || spawning == true && timer - last_spawn_time > 1 )
{
// Initialize invader line object
let invader_line = {};
// Calculate number of stronger invaders in group
let stronger_invaders_in_group = Math.floor((lvl_stage-2) * 1.2);
// Calculate number of stronger invaders per line
let stronger_invaders_per_line = this.calculate_stronger_invaders_per_line(spawned_lines, invader_lines_in_group, stronger_invaders_in_group);
// Calculate total number of invaders
let total_invaders = invader_lines_in_group * invaders_in_line;
// If the number of stronger invaders in group is bigger than number of normal invaders, add additional invader line to the group
if(stronger_invaders_in_group > (total_invaders / 2))
{
invader_lines_in_group++;
}
// Start spawning stronger invaders on the 3. wave
stronger_invaders_per_line = lvl_stage >= 3 ? stronger_invaders_per_line : 0;
// Spawn lines of normal and stronger invaders
invader_line = this.spawn_invader_line(NORMAL_INVADER_PATH, SPECIAL_INVADER_PATH, stronger_invaders_per_line);
// This is made to spawn first 2 groups of invaders closer, when the game starts
if(lvl_stage === 1)
{
// As the invaders are spawned closer, the spawn time needs to be adjusted, so that they follow their route correcty (based on their timer and speed)
invader_line.spawn_time -= 22.5 * (1 / INVADERS_SPEED);
}
else if(lvl_stage === 2)
{
// Spawn 2 lines of normal invaders in the group
invader_lines_in_group = 2;
// Adjust the spawn time
if(spawned_lines == 0)
{
invader_line.spawn_time -= 11.5 * (1 / INVADERS_SPEED);
}
else if(spawned_lines == 1)
{
invader_line.spawn_time -= 10 * (1 / INVADERS_SPEED);
}
}
// Return if the invader_line is empty
if(Object.keys(invader_line).length == 0)
{
return;
}
// Push invader line into invader array
invader_array.push(invader_line);
// Store time, when the latest invader line has been spawned
last_spawn_time = timer;
// Increment number of spawned line
spawned_lines++;
// Stop spawning, when desired number of invader lines has been spawned
if(spawned_lines === invader_lines_in_group)
{
// First 2 waves are spawned instantly, therefore they ignore the spawning rules
if(lvl_stage >= 3)
{
spawning = false;
}
// Reset number of already spawned lines
spawned_lines = 0;
// Increase difficulty
lvl_stage++;
}
}
// Start spawning additional group of invaders after certain time
else if(spawning == false && timer - last_spawn_time > 12)
{
spawning = true;
}
// Get projectiles, that hit something
// Method "get_landed_projectiles" returns an array of landed projectiles (tracers) in current frame
let landed_projectile = this.get_landed_projectiles();
// If projectile hit something
if (landed_projectile.length > 0) {
// Loop through each hit
landed_projectile.forEach(projectile => {
// Get hit position
//Note: When an object is hit, then the position returned, is relative to model space. Otherwise returns world position
let hit_pos = projectile.pos;
// Get ID of hit object (earth returns -1)
let hit_id = projectile.hitid;
// Check if the hit object was not earth
if (hit_id != -1)
{
// Loop through each invader line
outer_loop: for (let i = 0; i < invader_array.length; i++)
{
// Loop through each invader in the current line
for (let j = 0; j < invader_array[i].invaders.length; j++)
{
// Check if the hit ID matches the current invader's ID
if (hit_id == invader_array[i].invaders[j].instance.scene_id())
{
// Current invader
let invader = invader_array[i].invaders[j];
// Get invaders ECEF position
let invader_pos = invader.instance.get_pos();
// Calculate ECEF hit position, by adding relative hit position to invaders ECEF position (ECEF position needed for creating explosion...)
// Note: returned relative hit position is oriented in world space, so there is no need to change rotation
let hit_pos_ecef = {x: (invader_pos.x + hit_pos.x ) , y: (invader_pos.y + hit_pos.y ), z: (invader_pos.z + hit_pos.z) }
//Create explosion
this.create_explosion(hit_pos_ecef, true);
// Decrease the number of invader lives
invader.lives -= 1;
// If invader has no lives remaining
if (invader.lives <= 0)
{
// Increase the destroyed invaders count
invaders_destroyed++;
// Add score, based on the invader type
if(invader.special == true)
{
score += 5;
}
else
{
score += 1;
}
// Mark the hit invader as removed
invader.removed = true;
// Remove the hit invader from the scene
invader.instance.remove_from_scene();
// Check if all invaders in the current line are removed (marked as removed), if so, remove the invader line from the array
if (invader_array[i].invaders.every(inv => inv.removed))
{
invader_array.splice(i, 1);
}
}
// Break out of both loops
break outer_loop;
}
}
}
}
else
{
//Create explosion for non-invader hits
this.create_explosion(hit_pos, false);
}
});
}
// Update the remaining invaders count
instances_count = invader_array.reduce((total, line) => {
let active_invaders = line.invaders.filter(invader => !invader.removed);
return total + active_invaders.length;
}, 0);
}
visual_update is called each frame, similiar to update_frame from vehicle_script api
- 1. parameter - dtrender - time delta [s] since the last frame
- 2. parameter - dtsim - simulation time delta [s] since the last frame
- 3. parameter - dtinterpolate - delta time from last simulation step to predicted frame time
Use fill_rect method from canvas interface, to fill rectangle on screen
- 1. parameter - start of the rectangle on x axis
- 2. parameter - start of the rectangle on y axis
- 3. parameter - width of the rectangle
- 4. parameter - height of the rectangle
Use draw_text method from canvas interface, to write text on screen
- 1. parameter - font
- 2. parameter - position on x axis (bottom left corner of the text)
- 3. parameter - position on y axis (bottom left corner of the text)
- 4. parameter - text
- 5. parameter - color
Use draw_image_wh method from canvas interface, to draw image on screen
- 1. parameter - Image ID
- 2. parameter - position on x axis (bottom left corner of the image)
- 3. parameter - position on y axis (bottom left corner of the image)
- 4. parameter - width
- 5. parameter - height
- 6. parameter - color
function visual_update( dtrender, dtsim, dtinterpolate )
{
// Show loading screen until game is loaded
if(!is_loc_ready || !cannon || !cannon_geom || !cannon_entered || !invader_01_loaded || !invader_02_loaded)
{
// Fill the whole screen with black rectangle
this.canvas.fill_rect(0, 0, screen_size.x, screen_size.y, {r: 0,g: 0,b: 0, a:255});
// Draw loading text on screen
this.canvas.draw_text(font, screen_center.x - 70, screen_center.y - 7, "Loading minigame... ", {r: 255,g: 255,b: 255, a:255});
return;
}
// Clamp delta time, to avoid issues caused by rendering lag
let dt_clamped = Math.min(dtrender, 1/10)
// Increment the timer by the clamped dt
timer += dt_clamped;
// Check if there are any invaders in the array
if (invader_array.length > 0 )
{
// Loop through each invader line in reverse order
for (let i = invader_array.length - 1; i >= 0; i--)
{
// Calculate the time since the invader line was spawned
let line_timer = timer - invader_array[i].spawn_time;
// Get the array of invaders in the current line
let invaders_line = invader_array[i].invaders;
// Loop through each invader in the current line in reverse order
for (let index = invaders_line.length - 1; index >= 0; index--)
{
let invader = invaders_line[index];
// If the invader is removed (marked as removed), skip to the next invader
if(invader.removed == true)
{
continue;
}
// Get the current position and rotation of the invader
let invader_pos = invader.instance.get_pos();
let invader_rot = invader.instance.get_rot();
// Define the local movement vector
let distance_traveled = line_timer * INVADERS_SPEED;
let move_step = INVADERS_SPEED * dt_clamped;
let local_movement = {x: 0, y: move_step * 32.5, z: 0};
let move_step_turn = move_step * 65;
// Define the axis of rotation and initialize the angle
let axis = {x:0, y:0, z:-1};
let angle = 0;
// Adjust the angle and the movement speed when at the first curve
if (distance_traveled > 1 && distance_traveled < 8.5)
{
// Calculate the position for the invader by applying the offset based on the index (i) of the invader
// The offset is dependent on the index to ensure that invaders with higher indices move more,
// allowing for correct movement and spacing when turning
angle = 0.0015 * move_step_turn;
local_movement = {x:0, y:(0.4 + index/22) * move_step_turn , z:0};
}
// Adjust the angle and the movement speed when at the end curve
else if (distance_traveled > 39 && distance_traveled < 63)
{
angle = 0.00082 * move_step_turn;
local_movement = {x:0, y:(0.4 + index/40) * move_step_turn , z:0};
}
// Create a quaternion for the rotation based on the axis and angle
let rot = Quaternion.setFromAxisAngle(axis,angle);
// Multiply the current invader rotation by the new rotation quaternion
let final_rot = Quaternion.mul(invader_rot, rot)
// Set the new rotation for the invader
invader.instance.set_rot(final_rot);
// Rotate the local movement vector by the invader rotation quaternion
let world_movement = this.rotate_vector_by_quaternion(local_movement, invader_rot);
// Calculate the new position for the invader by applying the world movement and index coefficient
let new_position = {
x: invader_pos.x + world_movement.x,
y: invader_pos.y + world_movement.y,
z: invader_pos.z + world_movement.z
};
// Move the invader
invader.instance.set_pos(new_position);
};
};
}
// Draw crosshair
this.draw_crosshair();
// Draw background behind the score
this.canvas.fill_rect(20, screen_size.y - 20, 210 + 40 * score.toString().length, -50, {r: 15,g: 0,b: 60, a:255});
// Draw score on the screen
this.canvas.draw_text(font_xolonium_50_bold, 30, screen_size.y - 67, "score: "+ score, {r: 0,g: 150,b: 150, a:255});
// Draw number of remaining invaders on the screen
this.canvas.draw_text(font, screen_size.x - 250, screen_size.y - 67, "Invaders remaining: "+ instances_count, {r: 0,g: 0,b: 0, a:255});
//Draw projectiles on the screen
for( let p = 0; p < MAX_PROJECTILES; p++)
{
this.canvas.draw_image_wh(projectile_img, 30,20 + (40 * p), 110, 30,{r:255, g: p < projectiles_loaded ? 255 : 0, b: p < projectiles_loaded ? 255 : 0, a: 255});
}
//Draw lives on the screen
for( let h = 0; h < MAX_HEARTS; h++)
{
this.canvas.draw_image_wh(heart_img, screen_size.x - 50 - (40 * h) ,20, 40, 40,{r:255, g:255, b:255, a: h < hearts_remaining ? 255 : 100});
}
}
on_final_camera is invoked after the final camera position in the frame is known. Cannon rotation is based on camera rotation, therefore the final camera position needs to be known, before setting cannon rotation.
- 1. parameter - pos - final camera position
- 2. parameter - rot - final camera rotation
function on_final_camera(pos, rot)
{
// Return if the game is not loaded
if(!is_loc_ready || !cannon || !cannon_geom || !cannon_entered || !invader_01_loaded || !invader_02_loaded)
{
return;
}
// Get cannon inverse rotaion
let cannon_rot_inv = Quaternion.inverse(cannon_geom.get_rot());
// Multiply the cannon inverse rotation with the camera rotation
rot = Quaternion.mul(cannon_rot_inv, rot);
// Get axis
let axis = this.quaternion_to_axis(rot);
// Calculate angle
let angle = 2 * Math.acos(rot.w);
// Rotate cannon
cannon_geom.rotate_joint_orig(cannon_pitch_id, angle, {x:axis.x, y:axis.y, z:axis.z});
}