Reference Verse compiles

Public vs Private in Verse: Controlling What Your Code Exposes

Every field, function, and class you write in Verse has a visibility level — and getting it wrong means either exposing internals that break when touched, or locking yourself out of your own helpers. This article walks through Verse access specifiers using a sunny clifftop lighthouse scenario: one device manages the beacon light, and a second device on the dock tries to interact with it. By the end you'll know exactly which specifier to reach for and why.

Updated Examples verified on the live UEFN compiler
Watch the Knotpublic_vs_private in ~90 seconds.

Overview

When you write a creative_device in Verse, every identifier you declare — fields, functions, type aliases — has an access specifier that controls who can read or call it. Verse has four levels:

Specifier Who can see it
<public> Any Verse code anywhere — other devices, other modules
<protected> The class itself and any subclass
<internal> Only code in the same module (the default when you write nothing)
<private> Only the class itself — no subclasses, no outsiders

The default — writing no specifier at all — is <internal>, which is often more permissive than beginners expect. Explicitly marking things <private> is the right habit for implementation details.

Why does this matter on a Fortnite island? A large island might have a lighthouse_controller device that manages a beacon, a dock_manager device that needs to trigger the beacon, and a shore_patrol device that should never touch the beacon internals at all. Access specifiers let you draw those lines cleanly in code.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario: A clifftop lighthouse sits above a sunny cove. The lighthouse_controller device owns the beacon state. A separate dock_manager device on the shore can call the one public function ActivateBeacon() to light it up — but it cannot reach the private helper ComputeFlashInterval() or the private field FlashCount. A cinematic trigger fires when the beacon activates.

This example demonstrates:

  • <private> fields and helpers that are implementation details
  • A <public> function that forms the device's contract
  • An <internal> event subscription that only the same module needs
  • How a second device calls the public API via an @editable reference
# lighthouse_controller.verse
# Place this device on the clifftop in UEFN.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Verse }

lighthouse_log := class(log_channel){}

lighthouse_controller := class(creative_device):

    # --- Public API -------------------------------------------
    # Other devices (e.g. dock_manager) may call this to light
    # the beacon from the shore.
    ActivateBeacon<public>(): void =
        if (not BeaconLit?):
            set BeaconLit = true
            Print("Beacon activated! Cove is safe.")
            # Enable the cinematic trigger so the beacon sequence plays.
            BeaconTrigger.Trigger()
            set FlashCount = ComputeFlashInterval()

    # --- Editable references (internal by default) ------------
    # Wire these in the UEFN Details panel.
    @editable
    BeaconTrigger : trigger_device = trigger_device{}

    # --- Private state ----------------------------------------
    # No outside device should read or write these directly.
    var BeaconLit<private> : logic = false
    var FlashCount<private> : int = 0

    # --- Private helpers --------------------------------------
    # Implementation detail — the dock has no business knowing
    # how flash intervals are computed.
    ComputeFlashInterval<private>(): int =
        # Sunny cove: short bright flashes.
        return 3

    # --- Lifecycle --------------------------------------------
    OnBegin<override>()<suspends> : void =
        Print("Lighthouse controller ready on clifftop.")```

```verse
# dock_manager.verse
# Place this device on the dock. Wire LighthouseCtrl in the Details panel.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

dock_log := class(log_channel){}

dock_manager := class(creative_device):

    # Wire to the lighthouse_controller actor placed on the clifftop.
    @editable
    LighthouseCtrl : lighthouse_controller = lighthouse_controller{}

    # The pressure plate on the dock — step on it to signal the lighthouse.
    @editable
    DockPlate : trigger_device = trigger_device{}

    OnBegin<override>()<suspends> : void =
        Log(dock_log, "Dock manager online. Waiting for plate...")
        DockPlate.TriggeredEvent.Subscribe(OnDockPlateTriggered)

    # Called when a player steps on the dock pressure plate.
    OnDockPlateTriggered(Agent : ?agent) : void =
        # We can call ActivateBeacon because it is <public>.
        LighthouseCtrl.ActivateBeacon()
        # This would be a COMPILE ERROR — BeaconLit is <private>:
        # LighthouseCtrl.BeaconLit

Line-by-line highlights:

  • ActivateBeacon<public>() — the only door the dock is allowed to knock on. Mark the contract public, hide everything else.
  • BeaconLit<private> — mutable state. If the dock could write this directly, you'd lose the guard logic inside ActivateBeacon.
  • ComputeFlashInterval<private>() — a helper that belongs to the lighthouse's internal logic. Keeping it private means you can change the algorithm without touching any other device.
  • @editable fields have no explicit specifier, so they default to <internal>. They're visible inside the same Verse module (the UEFN project) but not exported to external packages.
  • BeaconTrigger.Trigger() — calling the real trigger_device API to fire the cinematic sequence.

Common patterns

Pattern 1 — A protected helper shared with a subclass

A base_gate_controller exposes a <protected> helper so a subclass timed_gate_controller can reuse the open/close logic without making it public to the whole island.

# base_gate_controller.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

gate_log := class(log_channel){}

base_gate_controller := class(creative_device):

    @editable
    GateTrigger : trigger_device = trigger_device{}

    # Subclasses can call this; the rest of the island cannot.
    OpenGate<protected>(): void =
        Log(gate_log, "Gate swinging open on the shore path.")
        GateTrigger.Trigger()

    # Public entry point — anyone can request the gate open.
    RequestOpen<public>(): void =
        OpenGate()

    OnBegin<override>()<suspends> : void =
        Log(gate_log, "Base gate controller ready.")

timed_gate_controller := class(base_gate_controller):

    @editable
    DelayTrigger : trigger_device = trigger_device{}

    # Subclass reuses the protected helper — totally legal.
    OpenAfterDelay<public>(): void =
        DelayTrigger.Trigger()   # fire a sequencer delay
        OpenGate()               # then open

    OnBegin<override>()<suspends> : void =
        Log(gate_log, "Timed gate controller ready on clifftop path.")

Pattern 2 — Internal-only wiring (the default)

When you write no specifier, the identifier is <internal> — accessible anywhere inside the same UEFN Verse module, but invisible to external packages. This is the right default for cross-device wiring that lives entirely on your island.

# shore_signal_hub.verse
# Coordinates several devices on the shore — all internal wiring.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

hub_log := class(log_channel){}

shore_signal_hub := class(creative_device):

    # No specifier = <internal>. Fine for same-island wiring.
    @editable
    FlareDevice : trigger_device = trigger_device{}

    @editable
    BellDevice : trigger_device = trigger_device{}

    # Internal helper — other island devices in this module can call it.
    FireAllSignals(): void =
        Log(hub_log, "Shore signals firing — cove alert!")
        FlareDevice.Trigger()
        BellDevice.Trigger()

    OnBegin<override>()<suspends> : void =
        Log(hub_log, "Shore signal hub armed.")
        # Self-test on start.
        FireAllSignals()

Pattern 3 — Type alias visibility

Type aliases follow the same rules, but with one extra constraint: an alias cannot be more public than the type it aliases. This pattern shows a private implementation type and a public-facing alias for a safe subset.

# beacon_state_types.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

state_log := class(log_channel){}

# A private implementation detail — the raw flash count type.
beacon_flash_count<private> := type{_X : int where 0 <= _X, _X <= 10}

# A public alias is ILLEGAL here because beacon_flash_count is private.
# public_flash_count<public> := beacon_flash_count  # COMPILE ERROR

# Instead, expose a plain int publicly and validate internally.
beacon_state_demo := class(creative_device):

    FlashLevel<private> : int = 0

    # Public setter validates the range so callers never see the
    # internal constrained type.
    SetFlashLevel<public>(Level : int): void =
        if (Level >= 0, Level <= 10):
            set FlashLevel = Level
            Log(state_log, "Flash level updated for cove beacon.")

    OnBegin<override>()<suspends> : void =
        Log(state_log, "Beacon state demo device ready.")
        SetFlashLevel(5)

Gotchas

1. The default is <internal>, not <public>

Beginner mistake: assuming that writing nothing means private. It doesn't — it means <internal>, which is visible to every other Verse file in your UEFN project. If you truly want something hidden, write <private> explicitly.

2. <private> blocks subclasses too

<private> is stricter than most languages' private — it is invisible even to direct subclasses. If you need subclasses to reach a member, use <protected> instead.

3. @editable fields are always <internal> or weaker

UEFN's Details panel wiring only works within the same project module. You cannot make an @editable field <public> in a way that external packages can set it through the panel — keep @editable fields at <internal> (the default) or <private> if only the owning device should reference them.

4. Type aliases cannot exceed their aliased type's visibility

If the underlying type is <private>, the alias must also be <private> or narrower. Trying to create a <public> alias for a <private> type is a compile error.

5. Access specifiers live on the definition, not the use site

You cannot "grant" access at the call site. If ComputeFlashInterval is <private>, no amount of casting or indirection from outside the class will reach it. Design your public API surface deliberately — it is the only door you give to callers.

Build your own lesson with public_vs_private

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →