Reference · Updated 2552.08.11.09.20

Doors — ASLDoor

Server-authoritative proximity doors. C++ owns the decision and the replicated state; the Blueprint subclass owns the meshes and the animation.

Server-authoritative proximity doors. C++ owns the decision and the replicated state; the Blueprint subclass owns the meshes and the animation.

Class: Plugins/SystemLinkCore/Source/SystemLinkCore/Public/World/SLDoor.h


#1. Why this class exists

The migrated BP_ForerunnerDoor1 (from the 5.6 project) got three things wrong, and each one is worth recognising because they recur in any world interactable:

1. Overlap with no authority check. It opened straight off OnComponentBeginOverlap. OnComponentBeginOverlap fires on every machine that simulates the overlap — the server and each client independently. So a client opened its own door locally, any NetMulticast it called was silently dropped (multicasts only replicate from the server), and the server's own multicast then played the animation a second time on that client.

2. Animation driven by a multicast. A multicast is fire-and-forget. A player who was not network-relevant when it fired never receives it, so anyone arriving after the door opened saw it closed — permanently. Replicated state plus OnRep fixes that for free, because a client entering relevancy receives the current value.

3. A filter that failed silently. The trigger check was "does the overlapping actor implement this interface", and the interface reference was empty after migration — so the branch was always false and the door could not open at all. Nothing logged.

The general rule: a door is world state that every machine must agree on. Decide on the server,
replicate the decision, and let each machine animate itself.

#2. How it works


Overlap (AUTHORITY ONLY) ──▶ TrackedOccupants ──▶ bIsOpen = true

                                                     │

                        ┌────────────────────────────┴────────────────────────┐

                        ▼                                                     ▼

              OnRep_IsOpen (clients)                          BroadcastDoorState (server)

                        └──────────────▶ OnDoorStateChanged(bOpen, bImmediate) ◀────┘

                                                     │

                                        Blueprint runs the timeline

The door's visual position is never replicated and should not be. Each machine animates itself from one replicated bool, rather than the server streaming a transform.

bLocked is replicated the same way, with OnDoorLockedChanged.


#3. Making a door

  1. Create a Blueprint deriving from SLDoor (the C++ class is Abstract, so it must be subclassed).
  1. Add your meshes as children of the inherited Root. Do not add another trigger volume — C++
  2. creates TriggerVolume and binds it. Resize that one instead.

  1. Set the box extent on TriggerVolume to the approach area.
  1. Implement OnDoorStateChanged(bOpen, bImmediate):

OnDoorStateChanged (bOpen, bImmediate)

  │

  ├── bImmediate TRUE  ──▶ Set Timeline position to (bOpen ? end : start)   [SNAP, no animation]

  │

  └── bImmediate FALSE ──▶ bOpen ? Timeline Play : Timeline Reverse

                           + play the door sound

Honour bImmediate. It is true when the door was already in that state as you arrived — a late joiner, or the door entering network relevancy. Animating then makes it look like the door just opened for a player who was never there to trigger it. Snap to the end pose instead.

  1. Optionally implement OnDoorLockedChanged(bNowLocked) for a light or hologram.

#4. Properties

PropertyDefaultNotes
TriggerActorClassAPawnType gate. Keeps projectiles, pickups and dropped weapons from triggering the door.
TriggerActorTagsemptyIf non-empty the actor must also carry one of these AActor::Tags. Empty accepts any actor of TriggerActorClass.
bAutoClosetrueOff makes a door that opens once and stays open.
AutoCloseDelay0.75Seconds after the last occupant leaves. 0 closes instantly, which reads as snapping at your back.
MaxOpenTime5.0Longest the door stays open with nothing in the trigger volume. 0 disables. Needed because AutoCloseDelay only runs when the volume empties, and someone who shoots a switch from cover never fills it — without this a shot-opened door stays open for the rest of the match. Occupancy always wins.
bShootableSwitchOpenstrueWhat a shot switch does by default. Off still fires OnDoorSwitchShot, it just stops opening the door on its own.
bDisableSwitchesWhileOpentrueStops switches blocking weapon traces while open — see §4a.
bStartLockedfalseApplied on the server in BeginPlay.
bDebugLogfalseEditAnywhere, so one problem door can be traced without every door flooding the log. Lines are tagged SERVER / CLIENT.

#On TriggerActorTags versus an interface

This replaces the old "implements this interface" check. An interface asset is one more thing to migrate and lose — which is exactly how the original ended up with an empty reference and a door that could never open.

But note the trade: a mistyped actor tag matches nothing and says nothing about it. Same failure shape as a BindWidgetOptional name mismatch. So:

  • Prefer leaving TriggerActorTags empty. TriggerActorClass = APawn is usually the whole filter you
  • need, and it is compile-checked rather than string-matched.

  • Only add tags when a door is genuinely selective (a team-only door, a keycard door).

A GameplayTag would not be a better choice here: reading gameplay tags off an arbitrary actor requires an ASC or IGameplayTagAssetInterface, so it swaps one interface dependency for another.


#4a. Shootable switches — USLShootableComponent

A panel the player shoots to open the door. Reusable: teleporters, consoles and destructible targets want the same thing.

#Setup

  1. Add a USLShootableComponent to the door Blueprint, positioned over the switch panel
  1. Size the box to the panel
  1. DoneASLDoor binds every shootable component it finds in BeginPlay, and the default
  2. behaviour opens the door

For anything else, override OnDoorSwitchShot or bind the component's OnShot directly. Setting bShootableSwitchOpens = false stops the default open while still firing the event.

#How a shot reaches it

The weapon pipeline dispatches non-pawn hits through ISLDamageable on the hit actor (SLWeaponsComponent.cpp ~987), passing the full FHitResult:


weapon trace hits ──▶ ASLDoor::ReceiveWeaponDamage   (authority, ISLDamageable)

                        └─▶ USLShootableComponent::RouteWeaponDamage

                              matches Hit.GetComponent() ──▶ OnShot ──▶ OnDoorSwitchShot

The component match is what makes "shoot the panel" different from "shoot the door" — the interface is actor-level, so without it any stray round clipping the frame would count.

For a different actor to use shootable components, implement ISLDamageable and forward one line to USLShootableComponent::RouteWeaponDamage. It returns false when the shot hit the actor somewhere other than a shootable component, so the caller can fall back to its own behaviour.

#Properties

PropertyDefaultNotes
bArmedtrueWhile false the component ignores hits. Server-side state, not replicated.
RetriggerDelay0.25Ignore further hits for this long. Without it a full-auto burst counts as twenty activations and the door opens, closes and opens again mid-magazine.

#Footguns

  • OnShot is AUTHORITY ONLY. It fires inside the server's shot processing and never runs on a
  • client. Do gameplay here and let replication carry the result — a cosmetic hung straight off OnShot is only ever seen by the listen-server host.

  • The collision setup is the whole trick and is deliberately narrow: blocks ECC_WeaponTrace,
  • ignores everything else. Shots register, players walk through it, and it never pushes the camera or the movement capsule. If it seems unresponsive, the tempting wrong fix is to make it block everything.

  • A shot switch opens, it does not toggle. A toggle means a stray round during a firefight slams the
  • door in your face. Auto-close handles closing.

  • Hidden in game, visible in editor. The panel art is your own mesh; this is just the target volume.

#5. Blueprint / C++ API

CallAuthorityNotes
OpenDoor()server onlyIgnored while locked. Client calls are ignored, not half-applied.
CloseDoor()server onlyCloses even with occupants inside; the next overlap change re-evaluates. A nudge, not a latch — use SetLocked to hold it shut.
SetLocked(bool)server onlyLocking closes immediately. Unlocking re-evaluates, so someone already standing inside opens it.
IsOpen() / IsLocked()anywhereReplicated, safe on clients.
GetOccupantCount()server meaningfulClients always read 0 — the occupant list is server-side and replicating it would buy nothing.

#6. Footguns

  • Do not add your own trigger volume. C++ creates and binds TriggerVolume. A second one will not be
  • bound to anything and will look like the door is ignoring you.

  • Do not open the door from Blueprint overlap events. That is the original bug. Overlap is handled in
  • C++ behind an authority check; a Blueprint overlap runs on every machine.

  • GetOccupantCount() is 0 on clients. Do not branch client visuals on it.
  • Dying in a doorway does not wedge the door open. EndOverlap is not reliably delivered for an actor
  • destroyed inside the volume, so ASLDoor also binds OnDestroyed per occupant. If you ever track occupants yourself, you need the same — a pruning pass alone will not fire without an event.

  • The trigger only responds to ECC_Pawn overlaps by default. Changing TriggerActorClass to
  • something that is not a pawn also means widening the collision response, or the overlap never fires.

  • bImmediate exists for a reason. Ignoring it produces a door that appears to open by itself
  • whenever a player joins or walks into relevancy.


#7. Status

  • ASLDoor written 2026-08-07 on branch level1-interactables. **Not yet used by any Blueprint and not
  • PIE-tested.**

  • BP_ForerunnerDoor1 still contains the original non-authoritative logic. It needs either reparenting to
  • SLDoor (with the overlap → Open Door graph deleted and OnDoorStateChanged implemented instead) or replacing with a fresh subclass. ⚠ Reparenting a Blueprint kills the editor during the reparent — the asset saves first, so relaunch and verify rather than redoing it. → Docs/Footguns.md.

  • Test in 2-player PIE, not single-player. Every bug this class exists to prevent is invisible with one
  • player. The specific cases: a client triggering the door, a client watching the host trigger it, and a door that is already open when a second player joins.