Tutorial ‐ script minigame (lua) - Outerra/anteworld GitHub Wiki
This is tutorial on creating minigame in Lua using script_module api.
Include our Lua math library, containing wide range of mathematical formulas
local vec_math = require("lib/lua/vec_math")
Declare variables.
-- Game settings
local INVADERS_SPEED = 1
local MAX_HEARTS = 5
local MAX_PROJECTILES = 4
local PROJECTILE_LOAD_TIME = 1
local TIME_NEAR_END_CURVE = 43
-- Paths to objdefs
local NORMAL_INVADER_PATH = "invader_ships/invader_ship_01"
local SPECIAL_INVADER_PATH = "invader_ships/invader_ship_02"
local CANNON_PATH = "cannon/cannon"
-- Fonts
local font_xolonium_50_bold
local font
-- Images
local projectile_img
local heart_img
-- Flags for object loading status
local invader_01_loaded
local invader_02_loaded
local cannon_loaded
-- Screen-related variables
local screen_size
local screen_center
-- Time-related variables
local timer
local last_spawn_time
-- Invader and level management
local invaders_in_line
local spawning
local spawned_lines
local lvl_stage
local invader_array
local invader_lines_in_group
local stronger_invaders_to_spawn
-- Cannon and projectile management
local cannon
local projectiles_loaded
local loaded_time
local cannnon_got
local cannon_geom
local barrel_tip_id
local cannon_pitch_id
local cannon_entered
-- Game state variables
local hearts_remaining
local score
local instances_count
local invaders_destroyed
local game_over
-- Location and time related variables
local is_loc_ready
local time_set
-- End position trigger
local trigger_sensor
-- Miscellaneous flags
local exit_game
Create helper function to check whether a specific value exists in a Lua table.
function table_contains(tbl, val)
-- Iterate over all elements in the table
for _, v in ipairs(tbl) do
-- If the element with required value is found, return true
if v == val then
return true
end
end
-- If no match was found, return false after completing the loop
return false
end
Create helper function to check if all invaders in line were destroyed .
function invaders_in_line_destroyed(tbl)
for _, v in ipairs(tbl) do
if not v.removed then
return false
end
end
return true
end
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(api)
-- draw_line() function draws 2D line (width and smooth can be set with function set_line_params() in canvas interface)
api.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})
api.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})
api.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})
api.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})
end
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
- 1.parameter - approximate crater radius
function create_explosion(api, pos, obj_hit)
-- Set emit radius for explosions
local emit_radius = 2
-- Use create_solid_particles() function from explosions interface, to create explosion of solid particles
api.explosions:create_solid_particles(pos, {x = 0, y = 0, z = 1}, emit_radius, 0.04 * math.max(1.0, math.log(emit_radius) / math.log(2)), 10, 1)
-- Make crater on earth, if earth was hit by projectile
if not obj_hit then
api.explosions:make_crater(pos, emit_radius*6)
end
end
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.
local 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
local quat_conjugated = vec_math.quat.conjugate(q)
-- Multiply the quaternion q by the vector quaternion, this combines the rotation quaternion with the vector
local qv = vec_math.quat.mul(q, vec_quat)
-- Multiply the result by the conjugate of q, this final multiplication applies the rotation to the vector
local rotated_qv = vec_math.quat.mul(qv, quat_conjugated)
-- Return the rotated vector in quaternion form
return rotated_qv
end
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(api, invader_path, SPECIAL_INVADER_PATH, num_of_stronger_invaders)
-- Set default values for optional parameters
SPECIAL_INVADER_PATH = SPECIAL_INVADER_PATH or NORMAL_INVADER_PATH
num_of_stronger_invaders = num_of_stronger_invaders or 0
-- If the invader model is not loaded, exit the function early
if not invader_01_loaded or not invader_02_loaded then
return {}
end
-- Array to store normal invader instances
local invaders = {}
-- Array to store stronger invader instances
local 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 then
-- Calculate the spacing between stronger invaders in the line
local 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 i = 0, num_of_stronger_invaders - 1 do
-- Calculate the position of each stronger invader in the line
table.insert(stronger_invaders, (i * spacing) + math.floor(spacing / 2))
end
end
-- Define the start position and orientation for the invader
local start_loc = {x = -1942405.2868467504, y = 5212454.450893189, z = 3122019.4702613624}
local 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 == 0 then
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}
elseif #invader_array == 1 then
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}
elseif #invader_array == 2 then
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}
end
-- Define a local offset vector (distance between invaders)
local local_offset = {x = -25, y = 0, z = 0}
-- Rotate the local offset vector by the invader rotation quaternion
local world_movement = rotate_vector_by_quaternion(local_offset, start_rot)
-- Loop to create and position invader instances
for i = 0, invaders_in_line - 1 do
-- Calculate the new position for the invader by applying the offset
local 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
local invader = {
instance = nil, -- 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 its properties
if table_contains(stronger_invaders, i) then
-- Create special invader
-- 1. param - full model path under packages dir
-- 2. param - world position of the pivot point
-- 3. param - orientation of the model
-- 4. param - if static object should be placed permanently into the world (if instance remains after game has been restarted)
invader.instance = api: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 default properties
invader.instance = api:create_instance(invader_path, start_pos, start_rot, 4)
end
-- If the instance creation failed, return an empty table
if not invader.instance then
return {}
end
-- Add the invader to the array of invaders
table.insert(invaders, invader)
end
-- Return object, containing the array of invaders, spawn time and state
return {invaders = invaders, spawn_time = timer}
end
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
local base_count = math.floor(total_stronger_invaders / total_lines)
-- Calculate any leftover stronger invaders that couldn't be evenly divided among the lines.
local 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
if line_index < extra_invaders then
return base_count + 1
else
return base_count
end
end
Create function to convert quaternion to its corresponding axis of rotation
-- Convert quaternion to its corresponding axis of rotation
function quaternion_to_axis(q)
local Θ = math.acos(q.w) * 2
local sinΘ = math.sin(Θ / 2)
local ax = q.x / sinΘ
local ay = q.y / sinΘ
local az = q.z / sinΘ
return {x = ax, y = ay, z = az}
end
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
game_over = true
-- Open a window, loading the specified HTML
api:open_window("www/GameOverScreen.html?width=" .. screen_size.x .. "&height=" .. screen_size.y)
end
Create function, which is used to reload the minigame
function reload_game()
-- Remove remaining invaders from the scene
for i = 1, #invader_array do
for j = 1, #invader_array[i].invaders do
local invader = invader_array[i].invaders[j]
if invader.instance and not invader.removed then
invader.instance:remove_from_scene()
end
end
end
-- 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
trigger_sensor = {}
invader_array = {}
end
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 ot.script_module:on_main_menu(is_paused)
-- pause the game when main menu is active
self:pause(is_paused)
end
Implement event, that is used for communicating between html window and script
This function is called from the html script
function ot.script_module: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 and b == 0 then
-- Reload game was chosen
reload_game()
end
if a == 0 and b == 1 then
-- Exit game was chosen
exit_game = value
end
return 0
end
Event for communicating between html window and script
This function is called from the html script
function ot.script_module: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 and param_b == 0 then
-- Sends value to HTML script
-- Send number of destroyed invaders (to show on game over screen)
return {_ret = 0, value = invaders_destroyed}
end
if param_a == 0 and param_b == 1 then
-- Send score (to show on game over screen)
return {_ret = 0, value = score}
end
end
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 ot.script_module:on_mouse_button(mouse_button, state, modifiers)
-- If button is released, do nothing
if not state then
return false
end
-- When left mouse button is pressed
if mouse_button == 0 then
-- Check if there are projectiles loaded in the cannon
if projectiles_loaded >= 1 then
-- Get camera direction
local camera_direction = self:get_camera_dir()
-- Multiply to set speed
camera_direction.x = camera_direction.x * 400
camera_direction.y = camera_direction.y * 400
camera_direction.z = camera_direction.z * 400
if cannon then
-- Get barrel tip bone ECEF position
local barrel_tip_pos_ECEF = cannon_geom:get_joint_ecef_pos(barrel_tip_id)
-- Launch a ballistic tracer/projectile, using launch_tracer() function from explosions interface
self.explosions:launch_tracer(barrel_tip_pos_ECEF, camera_direction, 100, {x = 0.5, y = 0.5, z = 0.5, w = 1})
end
-- When all projectiles were loaded, while shooting, set the "loaded_time", so that following projectile starts loading
if projectiles_loaded == MAX_PROJECTILES then
loaded_time = timer
end
-- Decrease loaded projectiles count
projectiles_loaded = projectiles_loaded - 1
end
end
return true
end
Implement event, that is called, when an object is preloaded (using preload_object function)
function ot.script_module:on_preload_object_done(model_path)
-- Set flags when objects are preloaded
if model_path == NORMAL_INVADER_PATH then
invader_01_loaded = true
elseif model_path == SPECIAL_INVADER_PATH then
invader_02_loaded = true
elseif model_path == CANNON_PATH then
cannon_loaded = true
end
end
Implement event, which is triggered when changed script is reloaded
function ot.script_module:on_reload()
-- Call function to reset game state
reload_game()
return true
end
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 ot.script_module:on_initialize()
-- Get explosions interface
self.explosions = query_interface("lua.ot.explosions.get")
-- Get canvas interface
self.canvas = query_interface("lua.ot.canvas.create", true, true)
-- Get screen size from world interface
screen_size = self:screen_size()
-- Calculate screen center
screen_center = {x = screen_size.x / 2, y = screen_size.y / 2}
-- Set initial values
hearts_remaining = MAX_HEARTS
projectiles_loaded = MAX_PROJECTILES
cannon = nil
cannon_geom = nil
game_over = false
spawning = true
cannon_got = false
cannon_entered = false
time_set = false
invader_01_loaded = false
invader_02_loaded = false
cannon_loaded = false
is_loc_ready = false
time_set = 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 = {}
-- Set smooth and width parameters of canvas lines
self.canvas:set_line_params(3, 1)
-- Load fonts from fnt file
-- param - path to fnt file
font_xolonium_50_bold = self.canvas:load_font("ui/xolonium_50_bold.fnt")
font = self.canvas:load_font("ui/hud.fnt")
-- Load images
-- param - path to imgset file
projectile_img = self.canvas:load_image("projectile.imgset/projectile")
heart_img = self.canvas:load_image("heart-icon.imgset/heart")
-- Use preload_object() function from script_module api, to preload object
-- event on_preload_object_done() from script_module api is called when object is preloaded
self:preload_object(NORMAL_INVADER_PATH)
self:preload_object(SPECIAL_INVADER_PATH)
self:preload_object(CANNON_PATH)
-- Jump to a location that was saved in the game campos
self:load_location("minigame_location", false)
-- Create sensor of sphere shape
-- This sensor is used as trigger objects on the end position (checks if an invader reached the end position)
self:create_sensor({x = -1941638.9198791014, y = 5211794.163754703, z = 3123587.0791307855}, {x = 0, y = 0, z = 0, w = 1}, 100)
-- Function needs to return a boolean
return true
end
before_simulation_step is called 60 times per second, similiar to simulation_step from vehicle_script api
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 ot.script_module:before_simulation_step(dtns, ns_sim)
-- Check if location is ready
-- returns - true if last loaded location is ready
is_loc_ready = self:is_location_ready()
-- Return if the location is not loaded
if not is_loc_ready then
return
end
-- Set time, if not yet set (to not start the minigame at night...)
if not time_set then
-- Set the time of the day
self:set_time(1, 36000, false)
time_set = true
end
if cannon_loaded and not cannon_entered then
if cannon == nil then
-- Spawn cannon
local cannon_start_pos = {x = -1941613.3096685943, y = 5212175.574482916, z = 3123162.568296084}
local cannon_start_rot = {x = -0.49617794156074524, y = -0.09487587213516235, z = 0.14400778710842133, w = 0.8509216904640198}
cannon = self:create_static_instance(CANNON_PATH, cannon_start_pos, cannon_start_rot, 2)
else
-- Get cannon geometry
cannon_geom = cannon:get_geomob()
-- Get cannon bones
if cannon_geom ~= nil then
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
end
end
end
-- Do not continue until cannon is loaded
if not cannon then
return
end
-- Do not continue on game over
if game_over then
return
-- When all lives are lost, set game over
elseif hearts_remaining <= 0 then
game_over = true
on_game_over(self)
return
end
-- Calculate dt
local dt = dtns / 1000000000
-- Increment the timer by the delta time
timer = timer + dt
-- Load projectile after some time
if timer - loaded_time > PROJECTILE_LOAD_TIME and projectiles_loaded < MAX_PROJECTILES then
loaded_time = timer
projectiles_loaded = projectiles_loaded + 1
end
-- 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 the current frame
local trigger_sensor = self:get_tiggered_sensors()
-- If an invader triggered the sensor, loop through the invaders to find the triggered one, based on its id
if #trigger_sensor > 0 then
for t = #trigger_sensor, 1, -1 do
-- Get the id of the invader that triggered the sensor
local triggered_id = trigger_sensor[t].trigger_entity_id
-- Get the invader object from the triggered id
local triggered_invader_obj = self:get_object(triggered_id)
-- Check if object was successfully received
if triggered_invader_obj ~= nil then
local break_outer_loop = false
-- Loop through the invaders
for i = #invader_array, 1, -1 do
if break_outer_loop == true then
break
end
local invaders_line = invader_array[i].invaders
for j = #invaders_line, 1, -1 do
local invader = invaders_line[j]
-- Skip the invaders that are removed
if invader.removed then
goto continue_invader
end
-- Check if the invader is the one that triggered the sensor, by comparing the id
if triggered_invader_obj:id() == invader.instance:id() then
-- Set the "removed" flag
invader.removed = true
-- Remove the invader
self:remove_object(triggered_id)
local current_line_invaders = invader_array[i].invaders
-- Check if all invaders are removed
if invaders_in_line_destroyed(current_line_invaders) then
-- If all invaders in the current line have been removed, remove the line from invader_array
table.remove(invader_array, i)
end
-- Decrease hp
hearts_remaining = hearts_remaining - 1
-- If the triggered invader was found, break out of the outer loop
break_outer_loop = true
end
::continue_invader::
end
end
end
end
end
-- Spawn multiple lines of invaders each second
if lvl_stage <= 2 or (spawning and timer - last_spawn_time > 1) then
-- Initialize invader line object
local invader_line = {}
-- Calculate number of stronger invaders in group
local stronger_invaders_in_group = math.floor((lvl_stage - 2) * 1.2)
-- Calculate number of stronger invaders per line
local stronger_invaders_per_line = calculate_stronger_invaders_per_line(spawned_lines, invader_lines_in_group, stronger_invaders_in_group)
-- Calculate total number of invaders
local total_invaders = invader_lines_in_group * invaders_in_line
-- If the number of stronger invaders in group is bigger than normal invaders, add an additional invader line
if stronger_invaders_in_group > total_invaders / 2 then
invader_lines_in_group = invader_lines_in_group + 1
end
-- Start spawning stronger invaders on the 3rd wave
if lvl_stage >= 3 then
stronger_invaders_per_line = stronger_invaders_per_line
else
stronger_invaders_per_line = 0
end
-- Spawn lines of invaders
invader_line = spawn_invader_line(self, NORMAL_INVADER_PATH, SPECIAL_INVADER_PATH, stronger_invaders_per_line)
-- Adjust the spawn time for early waves
if lvl_stage == 1 then
invader_line.spawn_time = invader_line.spawn_time - 26 * (1 / INVADERS_SPEED)
elseif lvl_stage == 2 then
invader_lines_in_group = 2
if spawned_lines == 0 then
invader_line.spawn_time = invader_line.spawn_time - 13 * (1 / INVADERS_SPEED)
elseif spawned_lines == 1 then
invader_line.spawn_time = invader_line.spawn_time - 11.5 * (1 / INVADERS_SPEED)
end
end
-- Return if invader_line is empty
if next(invader_line) == nil then
return
end
-- Push invader line into invader array
table.insert(invader_array, invader_line)
last_spawn_time = timer
spawned_lines = spawned_lines + 1
-- Stop spawning when the desired number of invader lines has been spawned
if spawned_lines == invader_lines_in_group then
if lvl_stage >= 3 then
spawning = false
end
spawned_lines = 0
lvl_stage = lvl_stage + 1
end
-- Start spawning additional group of invaders after a certain time
elseif not spawning and timer - last_spawn_time > 12 then
spawning = true
end
-- Get projectiles that hit something
local landed_projectile = self:get_landed_projectiles()
-- If projectile hit something
if #landed_projectile > 0 then
for _, projectile in ipairs(landed_projectile) do
local hit_pos = projectile.pos
local hit_id = projectile.hitid
-- Check if the hit object was not earth
if hit_id ~= 0xffffffff then
local break_outer_loop = false
for i = 1, #invader_array do
if break_outer_loop == true then
break
end
for j = 1, #invader_array[i].invaders do
local invader = invader_array[i].invaders[j]
-- Check if the hit ID matches the invader's ID
if hit_id == invader.instance:id() then
local invader_pos = invader.instance:get_pos()
local 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
create_explosion(self, hit_pos_ecef, true)
invader.lives = invader.lives - 1
-- If invader has no lives remaining
if invader.lives <= 0 then
invaders_destroyed = invaders_destroyed + 1
-- Add score based on the invader type
if invader.special then
score = score + 5
else
score = score + 1
end
invader.removed = true
invader.instance:remove_from_scene()
local current_line_invaders = invader_array[i].invaders
-- Check if all invaders in line are are removed
if invaders_in_line_destroyed(current_line_invaders) then
-- If all invaders in the current line have been removed, remove the line from invader_array
table.remove(invader_array, i)
end
end
break_outer_loop = true
end
end
end
else
-- Create explosion for non-invader hits
create_explosion(self, hit_pos, false)
end
end
end
-- Check if invader model was loaded
if not invader_01_loaded then
return
end
-- Check if there are any invaders in the array
if #invader_array > 0 then
for i = #invader_array, 1, -1 do
local line_timer = timer - invader_array[i].spawn_time
local invaders_line = invader_array[i].invaders
for index = #invaders_line, 1, -1 do
local invader = invaders_line[index]
if invader.removed then
goto continue_invader_2
end
local invader_pos = invader.instance:get_pos()
local invader_rot = invader.instance:get_rot()
local distance_traveled = line_timer * INVADERS_SPEED
local local_movement = {x = 0, y = INVADERS_SPEED * dt * 30, z = 0}
-- Define the axis of rotation and angle
local axis = {x = 0, y = 0, z = -1}
local angle = 0
-- Adjust movement and rotation at different points in the path
if distance_traveled > 1 and distance_traveled < 8.7 then
angle = 0.0015 * INVADERS_SPEED
local_movement = {x = 0, y = (0.4 + index / 22) * INVADERS_SPEED, z = 0}
elseif distance_traveled > 45.5 and distance_traveled < 70 then
local_movement = {x = 0, y = (0.4 + index / 40) * INVADERS_SPEED, z = 0}
angle = 0.00082 * INVADERS_SPEED
end
-- Rotate invader and apply movement
local angle2 = angle / 2
local sin_ang = math.sin(angle2)
local rot = {x = 0, y = 0 , z = 0, w = 0}
rot.x = axis.x * sin_ang
rot.y = axis.y * sin_ang
rot.z = axis.z * sin_ang
rot.w = math.cos(angle2)
local final_rot = vec_math.quat.mul(invader_rot, rot)
invader.instance:set_rot(final_rot)
local world_movement = rotate_vector_by_quaternion(local_movement, invader_rot)
local new_position = {
x = invader_pos.x + world_movement.x * dt / (1 / 60),
y = invader_pos.y + world_movement.y * dt / (1 / 60),
z = invader_pos.z + world_movement.z * dt / (1 / 60)
}
invader.instance:set_pos(new_position)
::continue_invader_2::
end
end
end
-- Update remaining invaders count
instances_count = 0
for i = 1, #invader_array do
for _, invader in ipairs(invader_array[i].invaders) do
if not invader.removed then
instances_count = instances_count + 1
end
end
end
end
visual_update is called each frame, similiar to update_frame from vehicle_script api
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
- 5.parameter - color
function ot.script_module:visual_update(dtrender, dtsim, dtinterpolate)
if not cannon then
-- Fill the whole screen with black rectangle, until the cannon is loaded
canvas:fill_rect(0, 0, screen_size.x, screen_size.y, {r = 0, g = 0, b = 0, a = 255})
-- Draw loading text on screen, until the cannon is loaded
canvas:draw_text(font, screen_center.x - 70, screen_center.y - 7, "Loading minigame... ", {r = 255, g = 255, b = 255, a = 255})
return
end
-- Return if the location is not loaded
if not is_loc_ready then
return
end
-- Draw crosshair
draw_crosshair(self)
-- Draw background behind the score
self.canvas:fill_rect(20, screen_size.y - 20, 210 + 40 * string.len(tostring(score)), -50, {r = 15, g = 0, b = 60, a = 255})
-- Draw score on the screen
self.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
self.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 p = 0, MAX_PROJECTILES - 1 do
self.canvas:draw_image_wh(projectile_img, 30, 20 + (40 * p), 110, 30, {r = 255, g = p < projectiles_loaded and 255 or 0, b = p < projectiles_loaded and 255 or 0, a = 255})
end
-- Draw lives on the screen
for h = 0, MAX_HEARTS - 1 do
self.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 and 255 or 100})
end
end
on_final_camera is invoked after the final camera position in the frame is known
function ot.script_module:on_final_camera(pos, rot)
-- Return if the location is not loaded
if not is_loc_ready then
return
end
-- Check if cannon and it's geometry were successfully loaded
if cannon == nil or cannon_geom == nil then
return
end
-- Get cannon inverse rotation
local cannon_rot_inv = vec_math.quat.inverse(cannon_geom:get_rot())
-- Multiply the cannon inverse rotation with the camera rotation
rot = vec_math.quat.mul(cannon_rot_inv, rot)
-- Get axis
local axis = quaternion_to_axis(rot)
-- Calculate angle
local 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})
end