Custom Teleporter Demo

Back


The Goal

We will make a package where you can teleport from and "A" teleporter to a "B" teleporter. You can also fill the "B" teleporter chunk with solid blocks, making it impossible to travel from A to B. So, if you try to travel from A to B, the following will happen:
  1. The engine will bring you from A to B.
  2. Your Lua code will detect there is a problem so you will request the engine to teleport you back to A.
  3. The engine will do this.
This is similar to teleportation via Blue Rings, but there is a key difference: here the engine can do all the actual teleporting, you just have to write Lua code to determine if the destination is ok once the engine brings you there. With blue rings, your Lua code needs to run each frame to determine where to shrink to next.

The Teleportation Manifesto

This demo is based on the Teleportation Manifesto which can be found here. That document describes the thought process behind doing this kind of teleportation. You should read that document. We will use a simplified version of the algorithm.

Filling Up A Chunk With Solid Blocks

To warm up, here is a basic entity which, when you use it, fills up the chunk it is in with solid blocks:
--File: bent_fill_with_blocks.lua

function p.__get_mesh() return "bent_fill_with_blocks" end

function p.payload(level, bp)
    ga_play_sound("explosion")
    local chunk_id = ga_bp_to_parent_chunk_id(level, bp)
    for x = 0,15 do
    for y = 0,15 do
    for z = 0,15 do
        local lbp = std.bp(x,y,z)
        local bp2 = ga_chunk_id_and_lbp_to_bp(chunk_id, lbp)
        ga_block_change_perm(level, bp2, "block_s")
    end end end
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 "Use to fill chunk with blocks (stand back!)"
end

function p.__on_use(level, bp)
    p.payload(level, bp)
end

Teleporter: "A" End

Here is the "A end" basic entity. You can left click to enable and disable it. You right click on it to actually teleport to your B end. The path of your A and B end are stored in global variables.
--File: bent_tele_end_a.lua

--We have a custom render function.
function p.__get_mesh() return "" end

--Slow.
function p.enabled(level, bp)
    local chunk_id = ga_bp_to_parent_chunk_id(level, bp)
    local this_path = ga_chunk_id_to_path(chunk_id)
    local a_path = ga_get_s("var.tele.end_a.path")
    return (this_path == a_path)
end

function p.__get_can_use(level, bp)
    return true
end

function p.__get_use_msg(level, bp)
    return "Teleporter end: A"
end

--Left clicking.
function p.__on_use(level, bp)
    if p.enabled(level, bp) then
        --Turn it off.
        ga_set_s("var.tele.end_a.path", "")
    else
        --Turn it on.
        local chunk_id = ga_bp_to_parent_chunk_id(level, bp)
        local this_path = ga_chunk_id_to_path(chunk_id)
        ga_set_s("var.tele.end_a.path", this_path)
    end
end

--Right clicking.
function p.__on_use2(level, bp)
    if( not p.enabled(level, bp) ) then
        game_msg.add("You need to enable this end")
        ga_play_sound("error")
        return
    end
    -- Teleporting from a to b
    -- (this may take many frames).
    game_tele_a_to_b.start_tele_from_a_to_b(chunk_id, lbp)
end

function p.__render(level, bp)
    local mesh_name = "bent_tele_end_a"
    if( p.enabled(level, bp) ) then
        mesh_name = "bent_tele_end_a_green"
    end
    --
    --Creating the classic "pulsating" look.
    local cur_time = ga_get_game_time()
    scale = 1.0 + math.cos(6.0 * cur_time) * 0.1
    ga_render_matrix_scaled(scale, scale, scale);
    ga_render_mesh(mesh_name)
end

Teleporter: "B" End

The B end is similar. You can enable and disable it. However you cannot teleport from it. Here is the code:
--File: bent_tele_end_a.lua

--We have a custom render function.
function p.__get_mesh() return "" end

--Slow.
function p.enabled(level, bp)
    local chunk_id = ga_bp_to_parent_chunk_id(level, bp)
    local this_path = ga_chunk_id_to_path(chunk_id)
    local b_path = ga_get_s("var.tele.end_b.path")
    return (this_path == b_path)
end

function p.__get_can_use(level, bp)
    return true
end

function p.__get_use_msg(level, bp)
    return "Teleporter end: B"
end

--Left clicking.
function p.__on_use(level, bp)
    if p.enabled(level, bp) then
        --Turn it off.
        ga_set_s("var.tele.end_b.path", "")
    else
        --Turn it on.
        local chunk_id = ga_bp_to_parent_chunk_id(level, bp)
        local this_path = ga_chunk_id_to_path(chunk_id)
        ga_set_s("var.tele.end_b.path", this_path)
    end
end

function p.__render(level, bp)
    local mesh_name = "bent_tele_end_b"
    if( p.enabled(level, bp) ) then
        mesh_name = "bent_tele_end_b_green"
    end
    --
    --Creating the classic "pulsating" look.
    local cur_time = ga_get_game_time()
    scale = 1.0 + math.cos(6.0 * cur_time) * 0.1
    ga_render_matrix_scaled(scale, scale, scale);
    ga_render_mesh(mesh_name)
end

Actually Teleporting Part 1

Recall that when we right click on the "A end" teleporter, it actually causes us to teleport by the function

game_tele_a_to_b.start_tele_from_a_to_b.

Here is that function, and a related one:
--File: game_tele_a_to_b.lua

--This is the only function which should be called
--from the outside.
function p.start_tele_from_a_to_b(chunk_id, lbp)
    --If we wanted, we could save the block path
    --of the given chunk (using chunk_id).
    --We could also save the lbp and use this information
    --in case we need to teleport back from b to a.
    --However, by the way this demo is set up,
    --return path is already stored in
    --"var.tele.end.a.path".

    --Getting the target location.
    local dest_path   = ga_get_s("var.tele.end_b.path")
    local dest_lbp    = std.bp(7,7,7) --Could set somehow.
    local dest_offset = std.block_center(dest_lbp)
    
    if( dest_path == "" ) then
        game_msg.add("You do not have a teleporter \"end b\"")
        ga_play_sound("error")
        return
    end

    --Setting the player's body to "spirit mode".
    --We must turn this off at the end.
    ga_move_set_body_spirit()

    --Requesting that the engine do the teleportation.
    ga_tele(dest_path, dest_offset)

    --Setting up the "explore while loop".
    local lua_func = "game_tele_a_to_b.tele_from_a_while_loop"
    local lua_win = "" --Not using a window (the teleportation is fast).
    ga_explore_while(lua_func, lua_win)

    ga_play_sound("waypoint_travel")
end
Let us break the function above down. First we get the block path of the "end b" teleporter, which is stored in a global variable as a string. Then we set the player's body mode to "spirit". Next, we call the function ga_tele, which makes a request to teleport the player. This will cause the engine to lock the game while the engine moves the player to the destination. This might involve creating many chunks. Finally, we call the function ga_explore_while, which tells the engine to repeatedly call the function

game_tele_a_to_b.tele_from_a_while_loop

until it returns false. Here is that callback function:
--Still in the file game_tele_a_to_b.lua

function p.tele_from_a_while_loop()
    --Safety check.
    local viewer_chunk_id = ga_get_viewer_chunk_id()
    local viewer_path = ga_chunk_id_to_path(viewer_chunk_id)
    local b_path = ga_get_s("var.tele.end_b.path")
    if( viewer_path ~= b_path ) then
        --This should never happen.
        ga_print("*** Error: game_tele_a_to_b failed: not in path b chunk")
        ga_exit()
    end
    --Ok, we are in the respawn point chunk.
    local ok = false
    local dest_lbp = std.bp(-1,-1,-1)
    local data = ga_search_for_bent_in_chunk(
        viewer_chunk_id, "bent_tele_end_b")
    if( data.is_valid ) then
        dest_lbp = data.value
        local bt = ga_chunk_id_and_lbp_to_bt(viewer_chunk_id, dest_lbp)
        if( not ga_bt_get_physically_solid(bt) ) then
            ok = true
        end
    end
    if( ok ) then
        --Ending the teleportation.
        local new_center = std.block_center(dest_lbp)
        ga_tele_same_level(new_center)
        ga_move_set_body_spirit_off()
        return false --Ending the while loop.
    else
        --Cannot teleport to this chunk.
        --Need to teleport back to "path a".
        p.tele_from_a_to_b_cancel()

        --Now we are going to return true.
        --The following might seem strange.
        --What happened is the tele_from_a_to_b_cancel
        --function just set another "explore while" loop,
        --and we do not want to return from that just yet!
        --That is, there is at most one explore while loop.
        --Indeed these "while loop" functions, like the
        --function tele_from_a_while_loop we are in
        --now, actually returns whether we should continue
        --the "current explore while loop", regardless of what
        --the callback function is.
        return true --Continue with "tele_back_to_a_while_loop".
    end
end
In the above function, we check that the target position of the player in the b chunk is not physically solid. If it is not solid, we end the teleportation process by setting the player's body mode to what it was before (by calling ga_move_set_body_spirit_off).

On the other hand, if the target position is solid, we must teleport the player back to "A end". To do this, note that we are still in spirit mode, so we should keep it that way. Next, we need to request that the engine perform another teleportation. Finally, we need to set up another explore while loop. This is all done in the function tele_from_a_to_b_cancel. We will show the code for this in the next section.

Actually Teleporting Part 2

Here is the relevant code for teleporting back to the "A end" in the case that there is a problem with the "B end":
-- We tried to go from a to b,
-- but there was a problem with the b chunk.
-- Now, we need to go back to a
-- The player should already be in spirit mode.
function p.tele_from_a_to_b_cancel()
    ga_print("*** Warning: Canceling from a to b teleportation")
    game_msg.add("Teleportation failed")
    ga_play_sound("error")

    --Getting the "path b".
    local dest_path  = ga_get_s("var.tele.end_a.path")
    local dest_lbp   = std.bp(7,7,7) --Could set somehow.
    local dest_offset = std.block_center(dest_lbp)

    --Remember, the player is still in spirit mode.

    --Requesting that the engine do the teleportation.
    ga_tele(dest_path, dest_offset)

    -- Setting up the "explore while loop".
    local lua_func = "game_tele_a_to_b.tele_back_to_a_while_loop"
    local lua_win = "" --Not using a window (the teleportation is fast).
    ga_explore_while(lua_func, lua_win)
end

function p.tele_back_to_a_while_loop()
    --Assume the teleportation worked.
    ga_move_set_body_spirit_off()
    return false --End the while loop.
end
That is it!

Bonus: "ga_set_tele_back" And "ga_tele_back"

Even though we accomplished our task, there are two more Game API functions which could make our lives easier.

The first function is ga_set_tele_back. If ga_set_tele_back is called at all, it must be called immediately AFTER ga_tele (NOT BEFORE). What this does is set the "tele back location".

If the engine performs a ga_tele teleportation but then we decide that the destination chunk is unsafe, we can call ga_tele_back and it will teleport us back to the position that we passed to ga_set_tele_back.

If we do not call ga_set_tele_back ahead of time but we DO end up calling ga_tele_back later, then the engine will bring us back to the position of the player when ga_tele was called.

This is what the code would look like to request teleportation by the engine:
ga_tele(dest_path, dest_offset)
ga_set_tele_back(source_path, source_offset) --Optional.
Then once the teleportation is finished, if we do not like the destination and want to teleport back, all we need is the following very simple line:
ga_tele_back()
Of course, you can write your own version of the ga_set_tele_back and ga_tele_back functions. You would just store the return position in some global variables (or dynamic variables).

Download

Here is the package we created.