
Sidekick Companion Module: Your Moods, Epic's Reactions
Overview
On July 23rd the publishing hold on creature-based islands lifts, and islands built on Epic's new creature toolkit go live. If you want a companion NPC in yours, this is the working starter kit: the sidekick_companion module, a three-file mood FSM grounded on the fresh UEFN Release-41.20 Sidekick surface and compile-verified as a set on the real UEFN compiler.
It rides on the npc-state-machines module (NPC state-machine module): same race-per-state engine, same no-polling FSM, same mood-vs-reaction split. What's new is what each half of the split points at. the NPC state-machine module invented both halves (guard_state / npc_reaction) because guards are a custom domain. Sidekicks are not: Epic ships a real sidekick_mood enum (Bored, Combat, Neutral, Worried — fixed by the platform, Verse can't add values to it) and a real sidekick_reaction enum (Angry, Attack, Dance, Eat, Emote, Happy, HitReact, Sleeping, Worried). So this set keeps the NPC state-machine module's shape but re-aims it:
- MOOD is ours —
sidekick_mood_state(Content/Curious/Excited/Scared), our own persistent domain enum, written only by the FSM runner. Same role as the NPC state-machine module'sguard_state. - REACTION is Epic's — the genuine
sidekick_reaction, fired via the realSidekick.PlayReaction[...]. No custom reaction enum needed, because Epic already ships one.
Every mood transition fires exactly one reaction on the way in: Content -> Curious plays Emote, -> Excited plays Happy, -> Scared plays Worried, and the idle-timeout fall back to Content plays Sleeping. That mapping lives in one function below.
The module, file by file
Three files, one responsibility each, and the dependency arrow points one way — example → engine → types, one link longer than the NPC state-machine module's. The engine never reaches back into beach_companion: a shopkeeper, a pet-care sim, or a creature battler copies companion_brain.verse unchanged and writes its own example file.
Heads-up if you've read the module plan: its file table had the roles reversed. The built set deliberately puts the reusable pure-logic engine in companion_brain.verse and the actual class(npc_behavior) in beach_companion.verse — mirroring the NPC state-machine module's own mode_flow.verse / mode_device.verse split, the only precedent that's compile-verified as a set. This article teaches the structure as built.
companion_moods.verse — the shared type surface
Types only, zero logic; both other files import these and nothing redefines them. The mood enum is the FSM's actual state:
sidekick_mood_state<public> := enum:
Content
Curious
Excited
Scared
The transition triggers are one-shot event()s — Verse's event(t) is both signalable and awaitable, so each field below is the entire contract between the sensing side (which calls Signal()) and the FSM side (which calls Await()). Each trigger is a generic-framework stimulus the beach example feeds from distance checks:
companion_triggers<public> := class:
# A player came within notice range. -> Curious. (Generic-framework
# "player proximity"; the beach example feeds this from distance to
# the owner.)
ProximityEvent<public> : event(agent) = event(agent){}
# Something delightful happened. -> Excited. (Generic-framework
# "being fed/petted"; the beach example feeds this from distance to
# the treasure marker.)
InteractionEvent<public> : event() = event(){}
# Something threatening is nearby. -> Scared. (Generic-framework
# "combat nearby"; the beach example feeds this from distance to
# the creature marker.)
DangerEvent<public> : event() = event(){}
Three events for four scoped transitions — the fourth, idle timeout, needs no event; the Walkthrough explains why. Last, the per-companion blackboard:
# Shared per-companion brain data. One instance per companion-driven
# NPC — never global, never shared across sidekicks (the NPC state-machine module's design:
# "no cross-NPC statics").
companion_blackboard<public> := class:
# The FSM's current MOOD. Owned by companion_brain.verse's
# RunCompanionFSM; nothing else writes it.
var CurrentMood<public> : sidekick_mood_state = sidekick_mood_state.Content
companion_brain.verse — the MOOD engine
Pure Verse: no devices, no navigation, and — deliberately — no SpatialMath import of either flavor. The engine only ever touches the Sidekick reference and Sleep(), so the mood/reaction mapping reads without any editor context. Concurrency IS the state machine, exactly as in the NPC state-machine module: every mood races its own body against the shared triggers, whichever leg wins cancels the rest, and the outer loop re-enters the new mood. Here's the runner with its Content arm:
RunCompanionFSM<public>(Blackboard : companion_moods.companion_blackboard, Triggers : companion_moods.companion_triggers, Sidekick : npc_sidekick_component)<suspends> : void =
loop:
case (Blackboard.CurrentMood):
companion_moods.sidekick_mood_state.Content =>
race:
Settle()
block:
Triggers.ProximityEvent.Await()
TransitionTo(Blackboard, companion_moods.sidekick_mood_state.Curious, Sidekick, sidekick_reaction.Emote)
block:
Triggers.InteractionEvent.Await()
TransitionTo(Blackboard, companion_moods.sidekick_mood_state.Excited, Sidekick, sidekick_reaction.Happy)
block:
Triggers.DangerEvent.Await()
TransitionTo(Blackboard, companion_moods.sidekick_mood_state.Scared, Sidekick, sidekick_reaction.Worried)
Read one arm and you've read them all — except Scared, which breaks the pattern on purpose:
companion_moods.sidekick_mood_state.Scared =>
# No race legs against the other triggers here, on
# purpose: a companion actively cowering from a threat
# finishes cowering before anything else interrupts it —
# unlike the other three moods, which listen for every
# trigger simultaneously.
block:
Cower()
TransitionTo(Blackboard, companion_moods.sidekick_mood_state.Content, Sidekick, sidekick_reaction.Sleeping)
The mood bodies themselves (Settle, Watch, Celebrate, Cower) are each just a Sleep(...) — that's what makes idle timeout free, and what makes this file impossible to trip on the SpatialMath landmine (more on both below).
And the one function that IS the mood/reaction mapping this module exists to teach:
# One-shot MOOD transition: fires the matching real Sidekick reaction,
# then settles into the new mood — this line IS the mood/reaction
# mapping the WO scope calls for. `PlayReaction[...]` is failable (the
# Sidekick might be mid-reaction); we only log success, same as the
# grounding article (article-verse-npc-sidekick-companion).
TransitionTo(Blackboard : companion_moods.companion_blackboard, NewMood : companion_moods.sidekick_mood_state, Sidekick : npc_sidekick_component, Reaction : sidekick_reaction) : void =
if (Sidekick.PlayReaction[Reaction]):
Print("companion: reaction played, settling into new mood")
set Blackboard.CurrentMood = NewMood
PlayReaction[...] is failable — the Sidekick might already be mid-reaction — so it's square-bracketed inside an if and only success is logged, the same idiom the Release-41.20 grounding uses.
beach_companion.verse — the wiring
The teachable example: a beach companion that follows its owner, gets excited near treasure, and gets scared near creatures. beach_companion is a class(npc_behavior), not a creative_device — it needs its siblings to compile, and it needs a real Sidekick to drive:
OnBegin<override>()<suspends> : void =
if:
Agent := GetAgent[]
Character := Agent.GetFortCharacter[]
Entity := GetEntity[]
# Pull the Sidekick API off our own entity (article-verse-npc-
# sidekick-companion: GetComponent[...] is failable — it only
# succeeds when this character is actually a Sidekick).
Sidekick := Entity.GetComponent[npc_sidekick_component]
Nav := Character.GetNavigatable[]
then:
# Lock a baseline so Epic's own automatic mood system doesn't
# fight our domain FSM's reactions (same idiom as the grounding
# article's `set Sidekick.MoodOverride = ...`).
set Sidekick.MoodOverride = option{sidekick_mood.Neutral}
Sidekick.ChangeMoodEvent.Subscribe(OnEpicMoodChanged)
Blackboard : companion_moods.companion_blackboard = companion_moods.companion_blackboard{}
Triggers : companion_moods.companion_triggers = companion_moods.companion_triggers{}
race:
companion_brain.RunCompanionFSM(Blackboard, Triggers, Sidekick)
FollowOwner(Entity, Nav)
RunProximityPoll(Entity, Triggers)
else:
Print("beach_companion: could not resolve agent/sidekick/navigation — behavior idle.")
Three lines in that if: deserve a slow read. Entity.GetComponent[npc_sidekick_component] is failable — it only succeeds when the character behind this behavior really is a Sidekick, which is the entire reason this module needed the fresh Sidekick docs. set Sidekick.MoodOverride = option{sidekick_mood.Neutral} locks Epic's own automatic mood system to a neutral baseline so it doesn't fight our domain FSM's PlayReaction calls for the idle animation — MoodOverride is a ?sidekick_mood, hence the option{} wrap. And the ChangeMoodEvent.Subscribe exists purely to log Epic's own mood swings — the handler never touches our blackboard, the same "log and don't touch state" idiom as the NPC state-machine module's LogReaction.
FollowOwner (not shown — it's the NPC state-machine module's chase shape) navigates to the owner forever, independent of mood. The last piece is the one polling loop in the entire set:
Script identity note: the Sidekick surface is component-shaped, and this module keeps the rule every scene-graph build follows — a
.versefile declares EITHER a device OR a component, never both. Components run in edit and at play; devices wake at game start. The device file here only coordinates — it never declares component logic of its own.
Walkthrough
Dropping it in. Spawn a Sidekick from an NPC Spawner using a Sidekick character definition — you cannot build one from scratch in Verse, only script the one the spawner hands you. Assign beach_companion as the Verse behavior on that Character Definition, then set TreasureAnchor and CreatureAnchor to the world positions of a treasure prop and a creature spawn on your beach level. In play: approach the companion as its owner and it turns Curious (plays Emote); approach the treasure marker and it turns Excited (Happy); approach the creature marker and it turns Scared (Worried).
Idle timeout needs no event. The scope lists four transitions — proximity, interaction, danger, and idle timeout — but companion_triggers only has three events. That's not an omission. Idle timeout is what happens when a mood's body (Settle(), Watch(), Celebrate(), Cower()) finishes before any trigger fires: race: cancels whichever leg didn't win, so if nothing interrupts the Sleep, the body itself completes and the case arm falls through to TransitionTo(..., Content, ..., sidekick_reaction.Sleeping). Same pattern the NPC state-machine module used for Investigate -> Return — no separate timer to manage, ever.
The learning path. If Sidekicks are new to you, start with the Sidekick Component reference — the 1:1 grounding for npc_sidekick_component's surface — then the Creating with Sidekicks stream synopsis for the platform picture: the July 23rd launch window, the spawner workflow, and "cosmetic sync". On that last one: this module deliberately does not call ApplyEquippedSidekickCosmetic (digest-verified in that guide as a failable <transacts><decides> call) — cosmetic swaps are the synopsis guide's lesson, not this one's.
Gotchas
The engine that can't step on the landmine. the NPC state-machine module's rule for UEFN's two incompatible vector3 worlds was "stay in the NEW /Verse.org/SpatialMath everywhere." This set makes a stronger guarantee: companion_brain.verse never imports SpatialMath at all — every vector3 and every Navigatable call lives in beach_companion.verse alone, where positions come from GetGlobalTransform().Translation (NEW-world) for anchors and companion alike. The engine file can't mix the two types because it never imports either one.
Danger beats treasure. RunProximityPoll checks the creature anchor before the treasure anchor, and the else if chain short-circuits the tick — so a companion standing between both markers gets scared, not excited. If you reorder those checks in your own sensing loop, you've changed the companion's personality.
Two mood systems, one writer. Epic's automatic sidekick_mood system keeps running underneath your FSM. The module's peace treaty has two clauses: pin MoodOverride to a baseline so the platform doesn't fight your reactions, and keep the ChangeMoodEvent handler informational-only — it never writes Blackboard.CurrentMood. And the usual the NPC state-machine module ownership rules hold: one blackboard per companion (never global, never shared across sidekicks), and CurrentMood has exactly one writer, RunCompanionFSM.
One sanctioned timer. A Signal() with no active Await() is a harmless no-op — only the mood currently racing against a trigger is listening — so the proximity poll needs no "did I already tell it" bookkeeping. It is also the ONE place this module set samples on a timer (PollInterval, mirroring the NPC state-machine module's PerceptionInterval); the MOOD FSM itself never polls. A second Sleep() loop in your extension is probably a race leg you haven't written yet.
Get the complete code — free
You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.
Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.
Check your understanding
Test yourself with an interactive quiz and track your progress + earn XP — free for members.
Turn this into a guided course
Add A reusable three-file companion-mood FSM for Epic's new Sidekicks — the NPC state-machine module's mood-vs-reaction split re-aimed at the real sidekick_reaction surface, with a beach companion as the worked example, ahead of the July 23 creature-islands launch to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.
Original guide generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.