Custom Blue Rings Demo
Back
The Goal
Recall how "blue rings" work in the xar package of Fractal Block World.
Every chunk in the chunk tree is one of 3 types:
A blue UP, a blue DOWN, or a blue TERMINAL.
Once you touch a blue ring, you travel:
If you are in a blue UP chunk, you go up one chunk
in the chunk tree (towards the root), and the process continues.
If you are in a blue DOWN chunk, you go down one chunk
(to a child specified by the current chunk),
and the process continues.
This process repeats until you reach a blue TERMINAL chunk,
at which point you stop.
We will not dwell on the "traveling up" portion of the
teleportation, because that is effectively covered
in the Custom Pink Rings tutorial.
The point of this guide is to explain how to make your own
blue rings, without relying on any special hardcoded
functionality of the engine.
We will make "striped" blue rings.
With this package, you can use the striped blue rings by
looking at them and pressing "F" (assuming that is how
your keybindings are set up).
Worldgen Code
The only interesting block script
in this package is
WorldNodes/Nodes/Cubes/block_example1.lua
and it reads as follows:
function p.__get_is_solid() return true end
function p.__get_tex() return "block_black_blue_border" end
function p.__main()
gen_set_default_block("e")
for x = 0,15 do
for y = 0,15 do
if( gen_randf() < 0.1 ) then
gen_set_pos(x,y,0, "block_example1")
end
end end
std.create_edges("block_black")
--Creating "blue teleportation source".
gen_add_bent(7,7,5, "bent_ring_blue_striped")
--Note: Never put the blue rings in the
--same position as the terminal position.
--This would cause an infintie loop.
if( gen_randf() < 0.02 ) then
--Making the chunk have blue type "terminal".
blue_striped_tele.set_blue_striped_type_terminal(7,7,9)
else
--In WorldNodes/Helpers:
local found_down_pos = false
for try = 1,100 do
local x = gen_randi(1,14)
local y = gen_randi(1,14)
local bt = gen_get_pos(x,y,0)
if( bt == "block_example1" ) then
found_down_pos = true
blue_striped_tele.set_blue_striped_type_down(x,y,0)
break
end
end
if not found_down_pos then
--Should probably never happen.
blue_striped_tele.set_blue_striped_type_terminal(7,7,9)
end
end
end
So the block will randomly be either blue type DOWN or TERMINAL.
Note that the block script above relies on the helper file
WorldNodes/Helpers/blue_striped_tele.lua
which reads as follows:
--File: blue_striped_tele.lua
-------------------------------------------------------------------------------
-- Variables names that must by synced with other files
-------------------------------------------------------------------------------
--The person calling this script should never use these strings.
--Dynamic block variable names.
--These must by synced with "custom_blue_rings/Game/game_tele_blue_striped.lua".
local dyn_block_var_blue_type = "dyn_blue_striped_tele_type"
local dyn_block_var_blue_pos_x = "dyn_blue_striped_tele_pos_x"
local dyn_block_var_blue_pos_y = "dyn_blue_striped_tele_pos_y"
local dyn_block_var_blue_pos_z = "dyn_blue_striped_tele_pos_z"
-------------------------------------------------------------------------------
-- Setting the "blue teleportation type" of a chunk
-------------------------------------------------------------------------------
--This is the only part of this script which can be called from the outside.
--These can be called within the __main functions of block scripts.
--Unless otherwise specified, a chunk is has "blue type up".
function p.set_blue_striped_type_up()
chunk_dyn_set_s(dyn_block_var_blue_type, "up")
end
function p.set_blue_striped_type_down(x,y,z)
chunk_dyn_set_s(dyn_block_var_blue_type, "down")
chunk_dyn_set_i(dyn_block_var_blue_pos_x, x)
chunk_dyn_set_i(dyn_block_var_blue_pos_y, y)
chunk_dyn_set_i(dyn_block_var_blue_pos_z, z)
end
function p.set_blue_striped_type_terminal(x,y,z)
chunk_dyn_set_s(dyn_block_var_blue_type, "terminal")
chunk_dyn_set_i(dyn_block_var_blue_pos_x, x)
chunk_dyn_set_i(dyn_block_var_blue_pos_y, y)
chunk_dyn_set_i(dyn_block_var_blue_pos_z, z)
end
That is, what these helper functions do is set
"dynamic" block variables of the chunk being generated.
A dynamic block variable is just like a regular
block variable, except it does not need to be
declared during the initialization of the block script.
However, for the current version of the engine, it
is only possible to set dynamic block variables
during chunk generation.
That is it for chunk generation code!
Blue Striped Rings Basic Entity
In this package, the player can use blue striped rings.
This is what the basic entity script for blue striped rings
looks like:
--File: bent_ring_blue_striped.lua
function p.__get_mesh() return "" end
function p.payload(level, bp)
local chunk_id = ga_bp_to_parent_chunk_id(level, bp)
game_tele_blue_striped.start_tele(chunk_id)
ga_play_sound("blue_ring")
end
function p.__on_touch(level, bp)
p.payload(level, bp)
end
function p.__get_can_use(level, bp)
return true
end
function p.__get_use_msg(level, bp)
return "Striped blue ring device"
end
function p.__on_use(level, bp)
p.payload(level, bp)
end
function p.__render(level, bp)
local cur_time = ga_get_game_time()
local speed_mod = 1.0
--
local angle1 = cur_time * 50.0 * speed_mod
local axis1 = std.vec(1.0, 0.0, 0.0)
ga_render_matrix_rotated(angle1, axis1)
ga_render_mesh("ring_blue_striped_large")
--
local angle2 = cur_time * 70.0 * speed_mod
local axis2 = std.vec(0.0, 1.0, 0.0)
ga_render_matrix_rotated(angle2, axis2)
ga_render_mesh("ring_blue_striped_med")
--
local angle3 = cur_time * 90.0 * speed_mod
local axis3 = std.vec(1.0, 0.0, 0.0)
ga_render_matrix_rotated(angle3, axis3)
ga_render_mesh("ring_blue_striped_small")
end
The interesting part of the script above
is when it calls the function
game_tele_blue_striped.start_tele
,
which we will describe in the next section.
Note that it would be wrong to start the
teleportation at the player's location.
We must instead start the teleportation
at the location of the blue striped rings.
Teleportation Script Part 1
The key part of this package is the script
Game/game_tele_blue_striped.lua
which we will describe in pieces.
This is the start of the script:
--File: game_tele_blue_striped.lua
-------------------------------------------------------------------------------
-- Variables names that must by synced with other files
-------------------------------------------------------------------------------
--Dynamic block variable names.
--These must by synced with worldgen code which
--"sets the blue type and blue pos" of a chunk
--during chunk generation.
--That is, it must sync with the file
--"custom_blue_rings/WorldNodes/Helpers/blue_striped_tele.lua".
local dyn_block_var_blue_type = "dyn_blue_striped_tele_type"
local dyn_block_var_blue_pos_x = "dyn_blue_striped_tele_pos_x"
local dyn_block_var_blue_pos_y = "dyn_blue_striped_tele_pos_y"
local dyn_block_var_blue_pos_z = "dyn_blue_striped_tele_pos_z"
--Must sync with "win_base_tele_blue.lua".
local var_always_render = "dyn.blue_striped.always_render"
-------------------------------------------------------------------------------
-- Local constants and variables
-------------------------------------------------------------------------------
--Do not use from outside this script.
--The name of this script.
local this_mod = "game_tele_blue_striped"
--Access the var_stage via helper functions.
local var_stage = "dyn.custom_blue.blue_tele_stage"
local var_start_chunk_id = "dyn.custom_blue.blue_tele_start_chunk_id"
local var_num_iterations = "dyn.custom_blue.blue_tele_num_iterations"
local var_render_countdown = "dyn.custom_blue.blue_tele_render_countdown"
--Do NOT call from outside this script.
--Called just before blue teleportation.
function p.init_dyn_vars(start_chunk_id)
ga_dyn_create_i(var_stage)
ga_dyn_create_i(var_start_chunk_id)
ga_dyn_create_i(var_num_iterations)
ga_dyn_create_i(var_render_countdown)
ga_dyn_create_b(var_always_render)
--
ga_dyn_set_i(var_stage, 0)
ga_dyn_set_i(var_start_chunk_id, start_chunk_id)
ga_dyn_set_i(var_num_iterations, 0)
ga_dyn_set_i(var_render_countdown, 0)
ga_dyn_set_b(var_always_render, false)
end
--Do NOT call from outside this script.
--Called just after blue teleportation.
function p.remove_dyn_vars()
ga_dyn_remove(var_stage)
ga_dyn_remove(var_start_chunk_id)
ga_dyn_remove(var_num_iterations)
ga_dyn_remove(var_render_countdown)
end
-------------------------------------------------------------------------------
-- Blue teleportation API
-------------------------------------------------------------------------------
--This is the only part of this script which can be called from the outside.
--Starts blue teleporting the player
--from the given chunk.
function p.start_tele(chunk_id)
ga_print(this_mod .. ".start_tele begin")
p.init_dyn_vars(chunk_id)
p.set_blue_tele_progress_stage(1)
local callback_name = this_mod .. ".update"
local win_name = "win_tele_blue_striped"
ga_explore_while(callback_name, win_name)
ga_print(this_mod .. ".start_tele end")
end
--This can be called from the outside.
--This will return either "up", "down", or "terminal".
function p.get_chunk_blue_type(chunk_id)
return p.get_chunk_blue_type_new_version(chunk_id)
end
--Returns the "blue pos" local block position of the chunk.
function p.get_chunk_blue_pos(chunk_id)
return p.get_chunk_blue_pos_new_version(chunk_id)
end
--This can be called from the outside.
--Returns -1 if the dest chunk does not exist.
--We go up until we find a "blue down" chunk.
function p.get_blue_dest_top_chunk_id(source_chunk_id)
local cur_chunk_id = source_chunk_id
while( cur_chunk_id >= 0 ) do
local blue_type = p.get_chunk_blue_type(cur_chunk_id)
if( blue_type == "down" or
blue_type == "terminal" )
then
return cur_chunk_id
end
cur_chunk_id = ga_chunk_id_to_parent_chunk_id(cur_chunk_id)
end
return -1
end
function p.get_blue_dest_top_level(source_chunk_id)
local dest_chunk_id = p.get_blue_dest_top_chunk_id(source_chunk_id)
return ga_chunk_id_to_level(dest_chunk_id)
end
-------------------------------------------------------------------------------
-- Some more helper functions
-------------------------------------------------------------------------------
--init_dyn_vars does NOT need to be called before this.
--Returns the following:
-- 0 -> We are not blue teleporting.
-- 1 -> We need to first teleport up the the first "down" or "terminal" chunk.
-- 2 -> We need to keep drilling (one chunk per frame) until we reach a "terminal" chunk.
function p.get_blue_tele_progress_stage()
if not ga_dyn_exists(var_stage) then return 0 end
return ga_dyn_get_i(var_stage)
end
function p.set_blue_tele_progress_stage(stage)
if stage == 0 then ga_dyn_remove(var_stage) return end
ga_dyn_create_i(var_stage)
ga_dyn_set_i(var_stage, stage)
end
There is a very weird part of the
start_tele
function we must address: the call to the Game API function
ga_explore_while
.
Teleportation Script Part 2: The ga_explore_while function
This is the main technical part of this guide.
With blue teleportation, the final chunk where we will end up might
not be in the active chunk tree.
We might have to "drill down" to create a path of chunks
ending with the final chunk.
We need the engine to create new chunks,
and we need the game Lua code to guide the drilling.
The Lua is also needed to tell when we are done.
There is where the ga_explore_while
function comes in.
Calling this function causes the game to enter a "while loop".
The engine alternate between exploring the chunk tree
(creating new chunks) and calling a Lua function specified
by ga_explore_while
.
Actually, the ga_explore_while
takes two arguments.
In our case, note that the call looks like the following:
local callback_name = this_mod .. ".update"
local win_name = "win_tele_blue_striped"
ga_explore_while(callback_name, win_name)
The second argument passed to ga_explore_while
is the name of a window script.
Its render function and input processing function will
be called every frame until the while loop is over.
It is ok if this second argument is the empty string.
We will show what the window could look like later.
The first argument passed to ga_explore_while
is the name of a function, of the form
"script_name.func
".
That function is called every frame until it returns
false (hence the words "while loop").
For our package, the while loop Lua function
should tell the engine where to shrink.
Note that the engine can only shrink one level per frame
in this algorithm.
This is a fundamental limitation, because the Lua needs
the engine to shrink, and the engine needs the Lua to
guide it and tell if the process is over.
function p.update()
local stage = p.get_blue_tele_progress_stage()
ga_print(this_mod .. ".update: stage = " .. tostring(stage))
if( stage == 0 ) then return false end
--Not done yet.
if( stage == 1 ) then
p.set_blue_tele_update_stage_1()
return true --Not done yet.
end
if( stage == 2 ) then
return p.set_blue_tele_update_stage_2()
end
--Should never reach here!
return false
end
While the engine is performing
the "explore while loop", the only Lua code that the
engine calls is specified by the callback functions
that were passed to ga_explore_while
.
The blue teleportation algorithm is always "in a stage".
Stage 0 means we are not teleporting.
Stage 1 means we need to teleport up until
we find a blue type down or terminal chunk.
Stage 2 means we need to continue "drilling"
(creating chunks and shrinking).
If we are in stage 0 in the update function,
it will return false, thus ending the
"explore while loop".
If we are in stage 1, the update function
will teleport the player to the finest ancestor
of the current chunk which is of blue type
either DOWN or TERMINAL.
Here is what the update function calls
if we are in stage 1:
--Teleporting up (towards the root) to the first chunk
--that is of type "down" or "terminal".
function p.set_blue_tele_update_stage_1()
ga_print("set_blue_tele_update_stage_1 begin")
if( p.get_blue_tele_progress_stage() ~= 1 ) then --Safety.
ga_print("*** Error: set_blue_tele_update_stage_1 not called in stage 1")
ga_exit()
end
local frame = this_mod .. ".set_blue_tele_update_stage_1"
ga_debug_push(frame)
--
local source_chunk_id = ga_dyn_get_i(var_start_chunk_id)
--
local top_chunk_id = p.get_blue_dest_top_chunk_id(source_chunk_id)
if( top_chunk_id < 0 ) then
ga_print("*** Error: failure in blue tele algorithm: could not find \"down\" chunk")
ga_exit()
end
ga_print("top_chunk_id = " .. tostring(top_chunk_id))
local top_chunk_path = ga_chunk_id_to_path(top_chunk_id)
--
--The target viewer offset does not matter in spirit mode:
local top_chunk_offset = std.vec(1.5, 1.5, 1.5)
--In spirit mode, the player does not have a body
--(but they are still "in" a chunk).
--Exploration is faster in spirit mode because we do not
--precompute certain chunk information during chunk generation.
ga_move_set_body_spirit()
ga_print(" top_chunk_path = " .. top_chunk_path)
ga_tele(top_chunk_path, top_chunk_offset)
p.set_blue_tele_progress_stage(2)
ga_debug_pop(frame)
ga_print("set_blue_tele_update_stage_1 end")
end
There is something very important in the function above:
we set the player's body to have mode
spirit.
This causes the engine to not create as many unnecessary chunks
during the teleportation.
(Specifically, the player must be in a chunk for at least 2
consecutive frames before the engine creates chunks around
that chunk).
Also, in spirit mode the player is "in a chunk"
but does not have a body, which is ideal for
teleportation.
If we are in stage 2 during the update function,
we need to do something interesting:
we call
p.set_blue_tele_update_stage_2
This will do the shrinking and possibly
end the algorithm.
Here is what the function looks like
and related functions:
function p.maybe_skip_render_frame()
local always_render = ga_dyn_get_b(var_always_render)
if( always_render ) then return end
--For speed we could not render at all anymore until
--the teleportation is done.
--However we can be sneaky and only render
--the first out of every 20 frames.
local render_countdown = ga_dyn_get_i(var_render_countdown)
ga_print("render countdown = " .. tostring(render_countdown))
if( render_countdown > 0 ) then
--Do not render the next frame.
ga_render_skip_next_frame()
ga_dyn_set_i(var_render_countdown, render_countdown-1)
else
--We WILL render the next frame.
ga_dyn_set_i(var_render_countdown, 20)
end
end
--Returns false iff done (like a while loop).
function p.set_blue_tele_update_stage_2(value)
--Safety.
ga_dyn_set_i_by_delta(var_num_iterations, 1)
local which_try = ga_dyn_get_i(var_num_iterations)
if which_try > 1000 then
p.set_blue_tele_update_stage_failed()
end
p.maybe_skip_render_frame()
local viewer_chunk_id = ga_get_viewer_chunk_id()
local blue_type = p.get_chunk_blue_type(viewer_chunk_id)
local blue_pos = p.get_chunk_blue_pos_new_version(viewer_chunk_id)
local offset = std.block_center(blue_pos)
if( blue_type == "up" ) then
ga_print("*** Error: chunk of blue type \"up\" found during a blue descend. ")
ga_exit()
return false
end
if( blue_type == "terminal" ) then
ga_tele_same_level(offset)
p.set_blue_tele_update_stage_finish()
return false --Done.
end
if( blue_type == "down" ) then
--Moving the viewer to a child of the viewer's current chunk.
ga_shrink2(offset)
return true --Not done.
end
end
There is something technical here.
Note that this algorithm can only
perform one shrink per frame.
This makes the teleportation slow if we render each frame.
Luckily we can call ga_render_skip_next_frame
to cause the engine to not render ANYTHING next frame.
However if we call this skip function every frame,
it would be a problem if the algorithm never ends,
because the player would have no way to navigate
through the program.
So we actually render 1 in every 20 frames.
Skipping rendering frames is very practical.
On my machine it speeds up the algorithm by a factor of about 3.
On the other hand, rendering 1 out of every 20 frames
is still pretty crappy if the user wants to go to the
main menu or open the console.
This is where the window we passed to
ga_explore_while
comes in:
we can have that window STOP skipping rendering
if the window detects certain input from the user.
Here is what this
Windows/win_tele_blue_striped.lua
window script looks like:
--File: win_tele_blue_striped.lua
--This window is designed to be rendered
--and do input processing while the
--game_base_tele_blue.lua script is teleporting
--the player. What happens is that that script
--calls the function
--ga_explore_while(callback_name, win_name)
--using "win_tele_blue+striped" (the name of this module)
--as the window name.
--So, the engine will use this window until
--the callback_name function returns false.
function p.__render(wid)
local col_black = std.vec(0.0, 0.0, 0.0)
local col_red = std.vec(1.0, 0.0, 0.0)
local back_alpha = 1.0
ga_win_set_background(wid, col_black, back_alpha)
ga_win_set_char_size(wid, 0.02, 0.04)
ga_win_set_front_color(wid, col_red)
ga_win_txt_center(wid, 0.7, "DO NOT SAVE OR EXIT THE PROGRAM")
ga_win_set_front_color_default(wid)
ga_win_set_char_size(wid, 0.03, 0.06)
ga_win_txt_center(wid, 0.5, "STRIPED BLUE RING TELEPORTATION")
local viewer_level = ga_get_viewer_level()
ga_win_set_char_size(wid, 0.02, 0.04)
ga_win_txt_center(wid, 0.4, "LEVEL = " .. tostring(viewer_level))
end
--We could be clever and have it so if we press the
--escape key, they we no longer call ga_render_skip_next_frame()
--every frame in "game_base_tele_blue.lua".
--We cannot execute normal game binds while we are in the
--"explore while" loop.
--Thus we must be explicit that the ESC key opens the main menu
--and the GRAVE key opens the console.
function p.__process_input(wid)
--Because this __process_input is being called,
--the main menu and console are both closed.
--Thus we can safely have the game_tele_blue_striped script
--skip most render frames while the teleportation is going on.
ga_dyn_set_b("dyn.blue_striped.always_render", false)
local opening_main_menu = false
if ga_win_key_pressed(wid, "ESC") then
opening_main_menu = true
ga_open_title_menu()
end
local opening_console = false
if ga_win_key_pressed(wid, "GRAVE") then
opening_console = true
ga_open_console()
end
if( opening_main_menu or
opening_console )
then
--This way the script game_tele_blue_striped will not
--keep calling ga_render_skip_next_frame() to
ga_dyn_set_b("dyn.blue_striped.always_render", true)
end
end
Teleportation Script Part 3: How It Ends
There are two ways the algorithm can end:
either the program exits because it has
gone on too long, or we place the player in a
chunk of type blue TERMINAL.
Here is the code:
--Called by the p.set_blue_tele_update_stage_2 function.
function p.set_blue_tele_update_stage_finish()
--This will restore the player's body to the mode it
--was just before changing to spirit mode.
ga_move_set_body_spirit_off()
--Cleaning up.
p.remove_dyn_vars()
p.set_blue_tele_progress_stage(0)
end
--Called if the while loop is taking to long.
function p.set_blue_tele_update_stage_failed()
ga_print("*** Error: Too many shrinks in search of blue rings.")
local num_tries = ga_dyn_get_i(var_num_iterations)
ga_print(" num_tries = " .. tostring(num_tries))
ga_exit()
end
Note that before the finish function is called,
the function ga_tele_same_level(offset)
is called
to place the player in the correct position in the final chunk.
There is also something interesting in the function
set_blue_tele_update_stage_finish
:
the call to the function
ga_move_set_body_spirit_off
.
This restores the player's body mode and dimensions
to what they were just before we called
ga_move_set_body_spirit
(so we do not need to remember what the
original body mode and dimensions were).
That is it!
Download
Here is the package we created.