Reference · Updated 2552.08.11.13.40
Teleporters — ASLTeleporter
Server-authoritative teleporters. C++ owns the decision, the move and the replicated state; the Blueprint subclass owns the meshes, the ring FX and the ambient audio.
Server-authoritative teleporters. C++ owns the decision, the move and the replicated state; the Blueprint subclass owns the meshes, the ring FX and the ambient audio.
Class: Plugins/SystemLinkCore/Source/SystemLinkCore/Public/World/SLTeleporter.h Sibling reference: Docs/Doors.md — same authority model, and worth reading first.
#1. Why this class exists
The migrated BP_Teleporter (from the 5.6 project) got the same three things wrong ASLDoor was written for. Moving a pawn is the harder version of each.
1. Overlap with no authority check. It teleported straight out of On Component Begin Overlap. That fires on every machine that simulates the overlap — the server and each client. So a client moved its own pawn locally and the server moved it again; every other client yanked that pawn's proxy across the map and then had it dragged back by replication; and the client, not the server, was deciding where a player ended up. A door opened client-side is a desync. A pawn moved client-side is a desync and a trivial cheat vector.
2. Unreplicated state driving replicated-looking visuals. The Enabled / Target Actor handshake lived in plain Blueprint variables, and the ring visibility and audio pause were driven off them. Each machine had its own copy, the server never agreed with any of them, and a player entering network relevancy saw whatever the default happened to be — forever.
3. A filter that failed silently. TeleportActor gated its whole cosmetic Sequence on a Does Object Implement Interface node whose interface class was left empty by the migration. Always false. The teleport itself still worked because it sat upstream of that branch, so the bug read as "the teleporter works, the effects just aren't hooked up yet" rather than as a broken node.
And one that is specific to this class: the original latched a Target Actor and waited for EndOverlap to release it. EndOverlap is not reliably delivered for an actor destroyed inside the volume, so dying on a pad left the teleporter latched shut for the rest of the match. Readiness here is a timer, not a latch — there is no state a dead player can strand.
The general rule: teleporting is authoritative gameplay, not a cosmetic. Decide on the server, move on
the server, replicate the state, and let each machine animate itself.
#2. How it works
Overlap on A (AUTHORITY ONLY)
│
├─ PassesTriggerFilter(Actor)?
├─ B->CanAcceptArrival(Actor)? ── no ──▶ OnTeleportBlocked (A, server)
│
▼
B->ReceiveTeleport(Actor)
│
├─ 1. B goes NOT-READY ◀── must happen BEFORE the move (see below)
├─ 2. TeleportTo(exit location, exit yaw)
├─ 3. ClientSetRotation → the player faces out of the exit
├─ 4. ExitMomentum → velocity policy + MaxExitSpeed clamp
├─ 5. PC->Client_OnTeleported(B) → overlay + whoosh, that ONE player
└─ 6. TeleportEventCounter++ → arrival cosmetics, EVERY machine
bReady replicates on both pads ──▶ OnTeleporterStateChanged(bIsReady, bImmediate) in Blueprint
#The bounce-back guard
Landing on B puts the pawn inside B's own trigger volume, which fires BeginOverlap synchronously inside TeleportTo. B sets itself not-ready before the move, so that overlap is refused.
⚠ Readiness returning does not re-evaluate whoever is standing there. That is deliberate: re-checking would send the arriving player straight back the instant the cooldown lapsed. You step off and back on to use a teleporter twice.
#Why a counter for the flash, and state for the glow
Two different cosmetic lifetimes, so two different mechanisms:
| Cosmetic | Driven by | Why |
|---|---|---|
| Ring glow, ambient hum, "powered down" look | Replicated bReady + OnRep | It is a state. A late joiner receives the current value and is correct for free. |
| Arrival flash, departure puff, pad sounds | Replicated TeleportEventCounter + OnRep | It is an event. A NetMulticast would simply not reach anyone who was not relevant when it fired — and bReady alone cannot carry it, because a DepartureCooldown of 0 produces no state change to hang it off. |
| Screen overlay, the player's own whoosh | Client_ RPC to one controller | It belongs to exactly one player. Both of the above reach everybody. |
#3. Making a teleporter
Everything structural is created in C++, so a new Blueprint needs very little.
- Create a Blueprint deriving from
SLTeleporter(the C++ class isAbstract, so it must be
subclassed). Name it BP_SL_Teleporter.
- Add your meshes as children of the inherited
Root. Add yourNiagaraComponentfor the rings and an
AudioComponent for the hum, same as the old Blueprint had.
- Do not add a trigger volume. C++ creates
TriggerVolumeand binds it. Resize that one.
- Move the
ExitPointcomponent to where arrivals should land, and point its +X axis the way they
should face. Clear of the frame mesh, but keep it inside TriggerVolume — standing on the pad you arrived on is the normal case and the ArrivalCooldown guard is built for it. (An exit placed outside the volume is handled correctly too, but you lose nothing by keeping it in.)
- Implement
OnTeleporterStateChanged(bIsReady, bImmediate):
OnTeleporterStateChanged (bIsReady, bImmediate)
│
├── Set Visibility (rings) = bIsReady
└── Set Paused (Audio) = NOT bIsReady
That is the direct replacement for the old Reset function and the ring/audio nodes scattered through TeleportActor and the overlap graph — and unlike those, it runs on every machine off replicated state.
⚠ bImmediate is about transition vs. restoration, not about animation. It is true when this is the state you arrived into — a late joiner, or the actor entering relevancy.
- Pure state-setting ignores it.
Set Visibility/Set Pausedreach the same result either way, so the
graph above leaves the pin unconnected and that is correct.
- Anything that reads as an event needs it, including things with no animation. A power-down sound is
instantaneous, but a joining player should not hear the far pad discharge for a teleport that happened before they connected. Same for a Niagara burst or a camera shake.
The rule of thumb: if the response would look wrong played for someone who was never there, branch on bImmediate and skip it.
- Optionally implement
OnArrival/OnDeparturefor a Niagara burst at the pad. Both fire on every
machine, so bystanders see them.
- Set the Class Defaults —
ArrivalOverlayClass=WB_Teleport,TeleportSound2D=
HaloTeleporterTeleport_Cue, ShoveDamageEffect = GE_Damage. No graph work; C++ shows and removes the overlay and applies the damage.
#Placing a pair
TargetTeleporter is EditInstanceOnly — a reference to another actor in the level can only be set on a placed instance, not in the Blueprint editor. Drop two, and in the level set each one's TargetTeleporter to the other.
Leaving TargetTeleporter empty is legal and makes an exit-only pad: it still receives arrivals, it just never sends. Because that is also the commonest wiring mistake, an empty target logs a warning on the server at BeginPlay.
#4. Properties
#Linking and filtering
| Property | Default | Notes |
|---|---|---|
TargetTeleporter | none | Set per placed instance, in the level. Empty = exit-only pad. |
TriggerActorClass | APawn | Type gate. Keeps projectiles, pickups and dropped weapons from riding through. |
TriggerActorTags | empty | If non-empty the actor must also carry one of these AActor::Tags. Prefer leaving it empty — see Docs/Doors.md §4 for why a tag is a worse filter than a class. |
#Behaviour
| Property | Default | Notes |
|---|---|---|
ExitMomentum | Redirect | See below. |
MaxExitSpeed | 0 (off) | Clamp in cm/s. Worth setting. A dash is 1625 cm/s and arriving at that speed is how you punch through a thin floor or overshoot a ledge. |
bSetControlRotationOnArrival | true | Faces the player down the exit's +X. |
ExitOccupancyPolicy | Shove | What happens to a camper on the exit. See §4a. |
ExitOccupancyRadius | 60 | Roughly a character capsule. |
ShoveSpeed | 600 | cm/s. Follow-through only — the displacement is a TeleportTo. |
ShoveDamageEffect | none | Assign GE_Damage. Unset = shove with no damage. |
ShoveDamage | 25 | Survivable by design — see §4a. |
ArrivalCooldown | 1.0 | Seconds this pad is out of action after receiving someone — blocks both further arrivals and departures. This is the "destination disabled after a teleport" spacing; 1–2s is the useful range. Clamped above zero — 0 is a bounce-back loop, not a tuning choice. See §4b. |
DepartureCooldown | 0.25 | Seconds before it will send again. Does not block arrivals. Small by design: a squad should be able to follow each other through. 0 is fine here. |
bStartDisabled | false | Applied on the server in BeginPlay. |
bDebugLog | false | EditAnywhere, so one problem pad can be traced without every teleporter flooding the log. Lines are tagged SERVER / CLIENT. |
#ExitMomentum
| Value | Behaviour |
|---|---|
Redirect (default) | Horizontal speed is rotated onto the exit's forward axis, keeping its magnitude; vertical speed is left alone. Sprint in, sprint out. This is the Halo behaviour. |
Preserve | Keeps the incoming world velocity exactly. Only sensible when both pads face the same way — otherwise a player running north into a south-facing exit walks straight back in. |
Zero | Arrive stationary. Pick this if arrivals are punching through geometry. |
⚠ Redirect is an assumption, not a decision you made. It was chosen because it matches Halo and
because Zero makes a teleporter feel like a loading screen. If a mid-dash arrival causes trouble, reach
forMaxExitSpeedbefore switching toZero.
#4a. Someone is standing on the exit
ExitOccupancyPolicy decides it.
Shove (default). The occupant is pushed clear and the arrival goes through. Nobody is ever denied a teleporter, so a camper cannot stall a route.
- They are moved out of the exit radius this frame, with an encroachment-checked
TeleportTo— a launch
alone would not have landed before the arrival, which is the same as no shove at all.
LaunchCharacteratShoveSpeedis the follow-through, so it reads as being shoved rather than teleported.
- The push is horizontal, away from pad centre. Dead centre has no direction to pick, so it falls back to the
exit's right — sideways rather than into the arriving player's path.
ShoveDamageis applied throughShoveDamageEffect, attributed to the arriving player so a kill scores to
them rather than reading as a suicide.
⚠ ShoveDamage is not a telefrag dial. It runs the normal pipeline, so shields absorb it and an
Overshield tag forces health damage to zero (SLDamageExecution.cpp:64). Setting it to 99999 kills
players without overshield and leaves overshielded ones standing — which reads as broken rather than as a
rule. A genuinely lethal shove would have to clear the tag first the way the Kill Z failsafe does
(SLCharacterBase.cpp:1022), and that is a deliberate change, not a number.
Block. The teleport is refused, OnTeleportBlocked fires on the server, and the sender is retried automatically once the exit clears. A camper can deny the route while they stand there.
#4a-ii. Shooting a teleporter down — USLShootableComponent
A panel you shoot to knock the pad out for a while. Same component the door switches use.
#Setup
- Add a
USLShootableComponentto the teleporter Blueprint, positioned over the panel
- Size the box
- Done —
ASLTeleporterbinds every shootable component it finds inBeginPlay, and the default
behaviour disables the pad for ShotDisableDuration
Override OnTeleporterShot or bind the component's OnShot directly for anything else. bShotDisablesTeleporter = false stops the default disable while still firing the event.
| Property | Default | Notes |
|---|---|---|
bShotDisablesTeleporter | true | What a shot does by default. |
ShotDisableDuration | 8.0 | Seconds down. Cannot be stacked into a lockout — see below. |
bDisableHitboxesWhileDown | true | Hitboxes off whenever bReady is false, for any reason. |
#Three consequences worth knowing
Shooting one pad closes the route both ways. CanAcceptArrival checks bEnabled, so a downed pad also refuses incoming players. When it comes back, anyone who walked into the far pad meanwhile is sent automatically by the InboundSources retry (§4b).
The partner goes dark too. bReady factors in IsTargetAvailable(), so a pad whose only route leads to a downed teleporter reads as down as well — otherwise it sits there lit and spinning while refusing everyone, which looks broken rather than deliberate. Derived state, not a second flag, so it relights on its own when the partner recovers. Three deliberate limits:
- It reads only the target's
bEnabled, neverCanAcceptArrival. Arrival cooldowns and exit occupancy
are sub-second and transient; folding them in would flicker the partner every time anyone used the route.
- No target counts as available, so an exit-only pad is not permanently dark for having nothing to send to.
- It only stops the partner sending. It can still receive from a third pad, which matters for a hub
rather than a pair.
A downed pad cannot be re-shot. bDisableHitboxesWhileDown takes the hitboxes offline with the pad, so one player cannot hold a route closed indefinitely by keeping fire on it. The timer always runs out.
#⚠ The trap: hitboxes eat shots fired through the portal
A USLShootableComponent has to block ECC_WeaponTrace to be hittable at all. One sitting anywhere near the opening therefore eats every round fired through it — you cannot shoot a player standing on the other side of the portal. ASLDoor hit exactly this and it cost a commit.
bDisableHitboxesWhileDown narrows the window but does not solve it — the pad is ready most of the time, which is precisely when the hitbox is live. Placement is the fix: put the box on the frame, the pillar or the base, clear of the opening.
#4b. Queueing — the two gates
A pad has two cooldowns and they gate different things. Treating them as one thing was a real bug in both directions — first too strict, then too loose:
| Question | Gate |
|---|---|
| Can this pad send? | IsReady() — enabled, and not inside either cooldown. This is the bounce-back guard. |
| Can this pad receive? | bEnabled, not inside an arrival cooldown, plus occupancy under the Block policy. |
The asymmetry is the point. A pad that just sent someone has no reason to refuse an incoming player, so DepartureCooldown must not gate arrivals — that was the first bug, and it made two players going through back to back fail. A pad that just received someone does have a reason: ArrivalCooldown spaces arrivals out instead of stacking them, which is the old Blueprint's "destination disabled until it clears" behaviour expressed as a timer rather than a latch.
Why a timer and not occupancy. "Disabled until the player steps off" is what bricked the original:
EndOverlap is not delivered for an actor destroyed inside a volume, so dying on the destination pad left
it disabled for the match. A timer cannot get stuck. Anyone still standing there after the cooldown is
handled by ExitOccupancyPolicy instead, so both behaviours coexist rather than competing.
#Retry — and why it has to reach across actors
BeginOverlap is the only entry point, so a player refused while standing in the trigger would never get a second one: they would have to physically step out and back in. ASLTeleporter tracks who is in the volume and retries the first waiting actor when the pad comes back up.
⚠ That is not sufficient on its own, and the reason is easy to miss. The two pads in a refusal are different actors: a player is turned away at A because B is cooling down, shot down or occupied, and B reopening fires nothing on A. So each teleporter registers itself with its target in BeginPlay (InboundSources), and a pad that reopens calls NotifyInboundSourcesRetry() on everything pointing at it. Without that link the waiting player stands in A's trigger indefinitely with no event coming.
That call lives in RefreshReadyState, not at the individual causes, so it holds for every route back to ready — a cooldown lapsing, a shot wearing off, or SetTeleporterEnabled by hand. It was originally hooked only to cooldown expiry, and shot-recovery silently did not release anyone.
⚠ This is not the original Blueprint's Target Actor latch, and the difference matters. Nothing is gated on that list. A destroyed occupant fails the weak-pointer check and is pruned — worst case it skips one retry. The old latch gated the teleporter itself, which is why dying on a pad bricked it for the match.
Anyone who arrived by teleport is marked and excluded from the retry pass. Without that, the cooldown lapsing would send them straight back — the bounce-back bug arriving by a second route.
#Cosmetics
| Property | Scope | Notes |
|---|---|---|
ArrivalOverlayClass | the teleported player only | Full-screen flash. Any UUserWidget — WB_Teleport works as-is. |
ArrivalOverlayDuration | " | 0.6. Give the widget its own fade-out animation and match this to it, or it pops off mid-fade. |
ArrivalOverlayZOrder | " | 100, i.e. above the HUD. |
TeleportSound2D | " | Played 2D. It happened to them, not at a place. |
ArrivalSound | everyone | Played at the exit, so bystanders hear the far pad fire. |
DepartureSound | everyone | Played at the entry pad. |
#5. The HUD overlay
ArrivalOverlayClass is a plain TSubclassOf<UUserWidget> and C++ pushes it with AddToViewport, not PushWidgetToLayer. Two reasons, both deliberate:
- A momentary non-interactive flash must not enter the CommonUI activatable stack, take focus, or change the
input mode. Full-screen washes are not screens.
- It means
WB_Teleportworks exactly as it is — no reparenting toUSLCommonActivatableWidget, and
reparenting a Blueprint kills the editor (Docs/Footguns.md).
⚠ There are two WB_Teleport assets in the project — Content/SystemLink/Environment/Teleporter/ and Content/SystemLink/UI/HUD/Teleporter/. Same trap as the two overshield pickups. Check which one you have been editing before you assign it.
For anything beyond the flash and the whoosh — a camera shake, a post-process blip, a HUD message — implement OnLocalPlayerTeleported. It runs only on the teleported player's machine.
#Why it routes through the player controller
A Client_ RPC needs an owning connection to send to. A level-placed actor has no owner, so a Client_ RPC declared on ASLTeleporter would silently go nowhere. ASLPlayerController::Client_OnTeleported makes the hop and calls straight back into the teleporter, so all the authoring still lives in the teleporter Blueprint.
This is the same shape as ASLCharacterBase::Client_ShowPickupPrompt — a world actor that needs to reach one player goes through something that player owns.
#6. Blueprint / C++ API
| Call | Authority | Notes |
|---|---|---|
TeleportActor(Actor) | server only | Sends Actor to TargetTeleporter as though it had walked in. Returns whether the move happened. Client calls are ignored, not half-applied. |
SetTeleporterEnabled(bool) | server only | Replicated. A disabled pad refuses departures and arrivals and reports not-ready. |
IsReady() | anywhere | Replicated. This is what ring/hum visuals should read. |
IsTeleporterEnabled() | anywhere | Replicated. |
CanAcceptArrival(Actor) | server meaningful | A client's answer can differ — a pawn on the far pad may not be network-relevant to it. Deliberately does not require IsReady() (§4b). |
GetExitTransform() | anywhere | The ExitPoint component's world transform. |
OnActorTeleported (delegate) | server only | For level scripting: scoring a route, opening a gate, arming a trap. |
#Blueprint events
| Event | Runs on | For |
|---|---|---|
OnTeleporterStateChanged(bIsReady, bImmediate) | every machine | Rings, hum, materials. |
OnArrival | every machine | Arrival burst at the exit. |
OnDeparture | every machine | Departure puff at the entry. |
OnLocalPlayerTeleported | the teleported player only | Camera shake, post-process, HUD message. |
OnTeleportBlocked(Actor) | server only | Rejection buzz, red light. Fires when this pad is down as well as when the far side refuses — from the player's side both are "I stood on it and nothing happened". Deliberately silent for a pad with no TargetTeleporter, or an exit-only pad would buzz at everyone crossing it. ⚠ Server-only — a cosmetic hung straight off this is seen by the listen-server host and nobody else. |
OnTeleporterShot(Hit) | server only | A shootable panel was hit. Default implementation downs the pad for ShotDisableDuration. ⚠ Server-only, same caveat — drive cosmetics off OnTeleporterStateChanged instead, which is replicated. |
#7. Footguns
- Do not add your own trigger volume. C++ creates and binds
TriggerVolume. A second one is bound to
nothing and looks like the teleporter ignoring you.
- Do not teleport from a Blueprint overlap event. That is the original bug. Overlap is handled in C++
behind an authority check; a Blueprint overlap runs on every machine.
TargetTeleportercannot be set in the Blueprint editor. It isEditInstanceOnlybecause it is an
actor reference. Set it on the placed instances.
- Standing on a pad does not re-trigger it. By design — see §2. Step off and back on. Note this applies
only to someone who arrived there; a player who walked in and was refused is retried automatically (§4b).
ShoveDamagecannot be turned into a telefrag by raising the number. Overshield zeroes health damage.
→ §4a.
LaunchCharacteris not replayed on a client correction (FSavedMove_Characternever stores
PendingLaunchVelocity), so a shoved client may rubber-band slightly. The displacement itself is a TeleportTo and replicates properly — only the flourish is at risk. Same family as the melee lunge; if it is bad enough to matter the fix is a root motion source.
ArrivalCooldownis load-bearing. It is the bounce-back guard, not a feel setting.
- The trigger only responds to
ECC_Pawnoverlaps by default. ChangingTriggerActorClassto something
that is not a pawn also means widening the collision response, or the overlap never fires.
- The
ExitPoint's +X is the exit direction. Its pitch and roll are discarded — arrivals are always
yaw-only, because a pitched capsule is never what anyone wants.
OnTeleportBlockedis authority-only. Same family asUSLShootableComponent::OnShot.
- ⚠ Two
WB_Teleportassets exist. See §5.
#8. Status
ASLTeleporterwritten 2026-08-10 on branchlevel1-interactables.
- ✅ WORKING — tested by Beepers 2026-08-11, "as well as I can test with just me."
BP_SL_Teleporteris
built and six pads are placed in Level 1 as three bidirectional pairs (LowerLeft⇄UpperLeft, LowerRight⇄UpperRight, LowerRamp⇄UpperDeck, verified through the asset registry). The arrival overlay runs through USLScreenEffectWidget (WB_Teleport reparented to it).
- Later the same day:
ArrivalCooldownextended to gate arrivals, the cross-actor retry link, shootable
hitboxes, and partner shutdown. All built clean and covered by the same solo pass.
⚠ SOLO TESTING CANNOT COVER WHAT THIS CLASS IS FOR. On a listen host the host is the server, so every
authority bug it exists to prevent looks correct. The matrix below still needs a second player — case #1
especially. Treat "works" as "works for the host" until then.
- Revised the same day after a review question — "what if it gets backed up?" — which surfaced a real bug:
arrivals were gated on IsReady(), so a pad refused everyone for a full ArrivalCooldown after each use, and a refusal was permanent until the player stepped off and back on. Gates split, retry added, and the camper case resolved as shove-plus-damage rather than block (§4a, §4b).
ASLPlayerController::Client_OnTeleportedadded for the local-cosmetic hop.
- The migrated
BP_Teleporterstill contains the original non-authoritative logic and is not parented to
this class. The plan is a fresh BP_SL_Teleporter subclass rather than a reparent — reparenting a Blueprint kills the editor during the operation (Docs/Footguns.md).
#Test in 2-player PIE, not single-player
Every bug this class exists to prevent is invisible with one player, because the host is the server. The specific cases, in order:
- A client walks in. Confirms the server made the decision and the client did not move itself first.
- The host watches a client teleport, and vice versa — the proxy should vanish and reappear, not slide
across the level. (This one is worth watching for specifically; how a teleport reads on a simulated proxy depends on movement-correction smoothing and has not been verified here.)
- Two players go through back to back. The second must not be turned away by the first one's
ArrivalCooldown — that was a real bug (§4b). Then the same again with one blocked first, to confirm the retry fires without them stepping off the pad.
- Camp the exit, then send someone through. The camper should be thrown clear and take
ShoveDamage,
attributed to the arriving player. Repeat with the camper holding overshield — they should survive, and that is correct, not a bug (§4a).
- A player joins while a pad is mid-cooldown. It should snap to the powered-down look, not animate.
- Die on a pad. The teleporter must still work afterwards — this is the failure the original had.
- Teleport while dashing. Check the arrival against the
level1-fort-floorpaper floor
(Docs/CollisionMeshes.md §0), and set MaxExitSpeed if it punches through.