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.