Teleport pad
CompilesStep 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.
How a pad is built
Section titled “How a pad is built”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).
The script
Section titled “The script”// 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");}- 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.
- Statelines 20-24
When the pad last fired, and what the current overlap found (a player or a ball), waiting to be moved.
- 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. - 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.
- Act after movementlines 54-81
late_tickruns 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. - Only loose ballslines 83-95
A ball has a
ball_transform. If itsthrowable_basehas aholder, someone’s carrying it, so leave it. - 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. - Play a burstlines 115-125
Each pad has a
burst_fxchild with an effect waiting to be told toplay. - 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.
- Where to landlines 142-157
The partner’s
arrivalchild if it has one, otherwise the partner itself. If the name doesn’t exist, it logs a warning instead of failing silently.
Ways to connect pads
Section titled “Ways to connect pads”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.
Placing a pad
Section titled “Placing a pad”-
Place the pad prefab in the level inside a group. A pad at the very top of the level can’t search upwards.
-
Give the placement a unique name, like
tp_plaza. That’s the name other pads use. -
On its
trigger→teleport_pad, setdestination_entity_nameto the partner’s name. -
Move its
arrivalto where people sent here should land, outside the trigger box.
Where each rule came from
Section titled “Where each rule came from”Every rule in this script was a bug first:
- Moving the player inside
trigger_overlappedworked only for players standing still → A move doesn’t stick. - Guarding the move with
is_serverleft online players where they were → A player move only happens on the server. - Spawning the burst from the effect’s own asset crashed the game → Spawning an effect crashes.
- The burst replayed whenever the pad came into view → An effect replays.
- Searching the world for
arrivalfound every pad’s → A placed prefab can’t find its partner.