Skip to content

Teleport pad

Compiles

Step on a pad, arrive at another. Players travel, loose balls travel, and a ball someone is holding stays in their hands. It’s a small script, but it touches nearly everything this guide covers: settings, triggers, finding entities across a prefab, moving players the right way, playing effects, and running on every machine.

The pad is a prefab: one little level, placed as many times as you like.

  • teleport_padthe prefab; each placement gets its own name
    • padmodeleffectthe visible pad, idle glow
    • triggerbox_triggerteleport_pad← your scriptscaled to the size of the pad
      • burst_fxeffectplayed on departure
    • arrivalwhere travellers to this pad land
      • burst_fxeffectplayed on arrival
    • glow_lightpoint_light

Each placement overrides two things: destination_entity_name on its teleport_pad (the name of the placement to send people to) and the position of its arrival (where people sent to it land).

teleport_pad.vscript
// teleport_pad.vscript: a pad that moves whoever steps on it to a partner
// pad: players, and balls nobody is holding.
//
// Sits on the same entity as a box_trigger, which reports overlaps to it.
// Switch the pad off without removing it.
let enabled: bool = true;
// The name of the pad to send people to. That pad's `arrival` child says
// where they land; a pad without one is its own landing point.
let destination_entity_name: string = "";
// Turn the arriving player to face the way the destination faces.
let use_destination_rotation: bool = true;
// Seconds before the pad sends anyone again, so whoever lands on a pad is
// not thrown straight back.
let cooldown_seconds: float = 1.5;
let mut last_teleport_ms: int = -1000000;
// What the overlap found this frame, waiting for late_tick to move it.
let mut pending_player: Component = null;
let mut pending_ball: Component = null;
// Runs on every machine: a player's movement is simulated on their own
// game too, so a move made only on the server would be undone a frame later.
event export fn type_created() {
$net.set_replicated(true, false);
}
// Only records what arrived. Moving it here would be overwritten by the
// movement code, which writes the player's position after this runs.
event export fn trigger_overlapped(other, unused) {
if !enabled || !other {
return;
}
if $time.get_time_ms() - last_teleport_ms < $time.seconds_to_ms(cooldown_seconds) {
return;
}
let touched: Entity = $component.get_entity(other);
let player: Component = $entity.get_hierarchy_singleton("catch_player.vscript", touched);
if player {
pending_player = player;
return;
}
let ball: Component = find_loose_ball(touched);
if ball {
pending_ball = ball;
}
}
// After movement has run: now a new position sticks.
event export fn late_tick() {
if !pending_player && !pending_ball {
return;
}
let player: Component = pending_player;
let ball: Component = pending_ball;
pending_player = null;
pending_ball = null;
let destination: Component = find_destination_transform();
if !destination {
return;
}
let position: vec3 = $entity.get_world_position(destination);
let rotation: vec4 = $entity.get_world_rotation(destination);
if ball {
// A ball's own script owns its position, so ask it to move.
$component.queue_event("set_position_and_rotation", ball, [position, rotation], true);
} else {
move_player(player, position, rotation);
}
play_burst($entity.get());
play_burst($component.get_entity(destination));
last_teleport_ms = $time.get_time_ms();
}
// The ball's transform, or null when `touched` is not a ball or someone
// is holding it; a held ball travels with its holder.
fn find_loose_ball(touched: Entity) -> Component {
let ball: Component = $entity.get_hierarchy_singleton("ball_transform.vscript", touched);
if !ball {
return null;
}
let throwable: Component = $support.entity_get_component($component.get_entity(ball), "throwable_base.vscript");
if throwable && throwable.holder {
return null;
}
return ball;
}
fn move_player(player: Component, position: vec3, rotation: vec4) {
let body: Entity = $component.get_entity(player);
let transform: Component = $support.entity_get_component(body, "transform.vscript");
if !transform {
return;
}
transform.position = position;
if use_destination_rotation {
transform.rotation = rotation;
// Tell the movement code too, or the camera snaps back to the old facing.
let movement: Component = $support.entity_get_component(body, "player_movement.vscript");
if movement {
let yaw: float = $math.eulers_from_rotation(rotation).z;
$component.queue_event("force_set_player_yaw", movement, [yaw], false);
}
}
}
// Plays the effect parked on `pad`'s `burst_fx` child, if it has one.
fn play_burst(pad: Entity) {
let fx: Entity = $entity.get_child_named("burst_fx", true, pad);
if !fx {
return;
}
let effect: Component = $support.entity_get_component(fx, "effect.vscript");
if effect {
$component.queue_event("play", effect, null, false);
}
}
// The nearest entity called `name`: this pad's own children first, then
// each ancestor's in turn. Climbing is what lets the pad find its partner
// when both are placed from a prefab.
fn find_named_upwards(name: string) -> Entity {
let mut at: Entity = $entity.get();
while at {
let found: Entity = $entity.get_child_named(name, true, at);
if found {
return found;
}
at = $entity.get_parent(at);
}
return null;
}
// Where travellers land: the partner's `arrival` child, or the partner.
fn find_destination_transform() -> Component {
if destination_entity_name == "" {
return null;
}
let mut target: Entity = find_named_upwards(destination_entity_name);
if !target {
$debug.print_warning("[teleport_pad] no entity named {}", [destination_entity_name], false);
return null;
}
let arrival: Entity = $entity.get_child_named("arrival", true, target);
if arrival {
target = arrival;
}
return $support.entity_get_component(target, "transform.vscript");
}
  1. Settingslines 6-18

    Four constants, so each placed pad can be configured in the editor: on or off, where to send people, whether to turn them, and how long to wait between sends.

  2. Statelines 20-24

    When the pad last fired, and what the current overlap found (a player or a ball), waiting to be moved.

  3. Run everywherelines 26-30

    The component replicates, and nothing below checks is_server. Every machine moves the player itself, which is the only way a move sticks online.

  4. Notice, don't actlines 32-52

    The trigger reports an overlap. After the guards (switched off? nothing there? cooling down?), the pad works out whether a player or a loose ball arrived, and only records it. Moving it here would be overwritten later in the frame.

  5. Act after movementlines 54-81

    late_tick runs after the movement code has written positions, so a new one sticks. Take what was recorded, find the destination, move the ball (by asking it) or the player (by writing), play both bursts, and start the cooldown.

  6. Only loose ballslines 83-95

    A ball has a ball_transform. If its throwable_base has a holder, someone’s carrying it, so leave it.

  7. Move a playerlines 97-113

    Write the transform’s position and rotation, then send the new yaw to player_movement, or the camera snaps back to the old facing.

  8. Play a burstlines 115-125

    Each pad has a burst_fx child with an effect waiting to be told to play.

  9. Find the partnerlines 127-140

    Search this pad’s children, then its parent’s, then its grandparent’s… Prefab entities are children of their placement, so the partner pad is several parents up.

  10. Where to landlines 142-157

    The partner’s arrival child if it has one, otherwise the partner itself. If the name doesn’t exist, it logs a warning instead of failing silently.

Every pad names one destination, so links go one way. That’s enough for every shape:

Shape Set up
Two-way pair plaza → garage, garage → plaza
One-way drop roof → garage; garage sends somewhere else, or nowhere
Loop a → b, b → c, c → a
Hop in place A pad names itself; it sends people to its own arrival

For a hop in place, keep arrival outside the pad’s own trigger, or people land back on it and hop again every 1.5 seconds.

  1. Place the pad prefab in the level inside a group. A pad at the very top of the level can’t search upwards.

  2. Give the placement a unique name, like tp_plaza. That’s the name other pads use.

  3. On its trigger → teleport_pad, set destination_entity_name to the partner’s name.

  4. Move its arrival to where people sent here should land, outside the trigger box.

Every rule in this script was a bug first: