Overview
Compose isn't a placed device — it's a Verse language pattern for gluing small functions into bigger ones. On an island, gameplay is full of little transformations: a player's raw score becomes a bonus, a bonus becomes a reward tier, a reward tier decides which door opens. Writing all of that inside one 60-line event handler is where bugs are born.
Instead you write each step as its own tiny function — Double, Add1, ClampToTier — and then wire them together with Compose. The game problem it solves: readable, reusable, testable logic. Reach for it whenever you catch yourself nesting F(G(H(x))) by hand, or copy-pasting the same math into three different buttons on your sunny boardwalk.
The core building blocks are:
- Compose — takes two functions and returns a new function that runs them in sequence (
Compose(F, G)appliesFfirst, thenG— or the reverse depending on how you define it; be explicit!). - Partial application — bakes one argument into a function to produce a smaller, more specialized function (
Add5 := Partial(Add, 5)). - Effect subtyping — because
<computes>functions compose cleanly, your tiny helpers stay pure and predictable.
We'll trigger all of this from real placed devices — a button_device on the dock and a conditional_button_device at the vault — so the composed math actually does something in-game.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Picture a sunny cove: a player smacks a Beach Score Button on the boardwalk. Each press feeds their tap-count through a chain of tiny functions to compute a reward score, and when the score crosses a threshold we play a celebratory VFX and grant an item. All the math lives in small composed functions — the event handler just calls the composed result.
# A cel-shaded beach-day scoring device built from composed small functions.
beach_score_device := class(creative_device):
# The dock button the player taps to earn points.
@editable
ScoreButton : button_device = button_device{}
# A visual/audio pop when the player hits the reward tier.
@editable
RewardVfx : vfx_spawner_device = vfx_spawner_device{}
# How many times this player has tapped.
var Taps : int = 0
# --- tiny, pure building-block functions ---
# Double the raw taps.
Double(X : int)<computes> : int = X * 2
# Add a flat sunny-day bonus.
Add1(X : int)<computes> : int = X + 1
# Compose two int->int functions: run F first, then G.
Compose(F : type{_(:int)<computes>:int}, G : type{_(:int)<computes>:int})<computes> : type{_(:int)<computes>:int} =
Local(X : int)<computes> : int = G(F(X))
Local
OnBegin<override>()<suspends> : void =
# Wire the button press to our handler.
ScoreButton.InteractedWithEvent.Subscribe(OnTapped)
# Runs every time the player taps the beach button.
OnTapped(Agent : agent) : void =
set Taps += 1
# Build the reward formula ONCE by composing small functions:
# first Double, then Add1 -> Score = Taps*2 + 1
ScoreOf := Compose(Double, Add1)
Score := ScoreOf(Taps)
# When the composed score crosses the tier, celebrate.
if (Score >= 11):
RewardVfx.Enable()
RewardVfx.Spawn()
Line by line:
@editable ScoreButton : button_device— the placed dock button; you assign the real device in the UEFN Details panel. Calling a device method requires it be an@editablefield, never a bare variable.@editable RewardVfx : vfx_spawner_device— the celebration effect at the cove.DoubleandAdd1— two tiny<computes>functions. Each does exactly one thing, so each is trivial to test and reuse.Compose(F, G)— takes twoint->intfunctions and returns a new functionLocalthat appliesFthenG. The return typetype{_(:int)<computes>:int}describes "a function from int to int". Note it returns the function valueLocal, not a call to it.OnBegin— subscribesOnTappedto the button'sInteractedWithEvent. Subscriptions belong here.OnTapped— increments taps, buildsScoreOf := Compose(Double, Add1)soScoreOf(Taps)computesTaps*2 + 1, then enables and spawns the VFX once the tier is reached. The math reads like English because it's composed from named pieces.
Common patterns
1. Partial application to specialize a reward function
Bake in a fixed bonus so each button on the boardwalk grants a different amount without duplicating logic. Here a granted item pops when the partially-applied function clears a threshold.
# Uses Partial to pre-bake a bonus into a reusable scoring function.
beach_partial_device := class(creative_device):
@editable
BoardwalkButton : button_device = button_device{}
@editable
ItemGranter : item_granter_device = item_granter_device{}
# Generic two-arg adder.
Add(X : int, Y : int)<computes> : int = X + Y
# Partial: capture the first argument, return a one-arg function.
Partial(F : type{_(:int, :int)<computes>:int}, X : int)<computes> : type{_(:int)<computes>:int} =
PartialFunc(Y : int)<computes> : int = F(X, Y)
PartialFunc
OnBegin<override>()<suspends> : void =
BoardwalkButton.InteractedWithEvent.Subscribe(OnPressed)
OnPressed(Agent : agent) : void =
# Pre-bake a +5 sunny bonus into a specialized function.
Add5 := Partial(Add, 5)
Reward := Add5(3) # 8
if (Reward >= 8):
ItemGranter.GrantItem(Agent)
2. Composing across three steps into one door-check
Chain three tiny functions so a vault door on the clifftop opens only when the composed value clears the gate. Here the composed function drives a conditional_button_device state.
# Chains three composed functions to decide whether the vault unlocks.
cliff_vault_device := class(creative_device):
@editable
VaultButton : conditional_button_device = conditional_button_device{}
var Charge : int = 3
Double(X : int)<computes> : int = X * 2
Add1(X : int)<computes> : int = X + 1
Compose(F : type{_(:int)<computes>:int}, G : type{_(:int)<computes>:int})<computes> : type{_(:int)<computes>:int} =
Local(X : int)<computes> : int = G(F(X))
Local
OnBegin<override>()<suspends> : void =
VaultButton.ActivatedEvent.Subscribe(OnActivated)
OnActivated(Agent : agent) : void =
# Compose Double then Add1, then feed it into itself for a 2-stage chain.
Stage1 := Compose(Double, Add1) # X*2 + 1
Final := Compose(Stage1, Stage1) # ((X*2+1)*2+1)
Result := Final(Charge) # ((3*2+1)*2+1) = 15
if (Result >= 15):
VaultButton.SetShowInteractionTime(true)
3. Composing effect-consistent functions passed into iteration
Pure <computes> functions compose cleanly and can be handed to array logic — great for scoring several beach checkpoints at once.
# Applies a composed transform to each checkpoint score.
checkpoint_sum_device := class(creative_device):
@editable
TallyButton : button_device = button_device{}
@editable
ResultVfx : vfx_spawner_device = vfx_spawner_device{}
Double(X : int)<computes> : int = X * 2
Add1(X : int)<computes> : int = X + 1
Compose(F : type{_(:int)<computes>:int}, G : type{_(:int)<computes>:int})<computes> : type{_(:int)<computes>:int} =
Local(X : int)<computes> : int = G(F(X))
Local
OnBegin<override>()<suspends> : void =
TallyButton.InteractedWithEvent.Subscribe(OnTally)
OnTally(Agent : agent) : void =
Transform := Compose(Double, Add1)
Scores := array{1, 2, 3}
var Total : int = 0
for (S : Scores):
set Total += Transform(S) # (1*2+1)+(2*2+1)+(3*2+1) = 3+5+7 = 15
if (Total >= 15):
ResultVfx.Enable()
ResultVfx.Spawn()
Gotchas
- Order matters.
Compose(F, G)in our examples runsFfirst, thenG(G(F(X))). If you flip the argument order you flip the math. Always document which function runs first — a comment saves hours. - Return the function, not a call. Inside
Compose, you defineLocaland then writeLocalon its own line to return the function value. WritingLocal(X)would try to call it with no valid arg and fail to compile. - Effect specifiers must line up. Composing
<computes>functions produces a<computes>function. If one helper had a stronger effect (like<transacts>), the composed type must reflect it. Keep your tiny helpers pure (<computes>) whenever possible so they compose freely. - No int↔float auto-convert. If one function returns
intand the next expectsfloat, composition won't type-check. Keep the chain in one number type, or add an explicit conversion function to the chain. - Devices still need
@editable+ subscription. Composition is pure math; nothing happens in-game until a real device event (InteractedWithEvent,ActivatedEvent) calls your composed function AND the handler calls a real device method likeEnable(),Spawn(), orGrantItem(). - Unwrap agents when the event provides one. Handlers subscribed to
InteractedWithEventreceive anagent; use it directly (as above) or, forlistenable(?agent)events, unwrap withif (A := Agent?):before use.