Skip to content

Interactable System

What You'll Learn

  • The N-interactor slot system and how join requests are validated
  • The interactable state machine and lifecycle modes
  • How widget ranges gate attention/interaction widget visibility
  • Which of the five component types to pick per action

Core Concepts

What Is an Interactable?

An interactable component sits on the actor being interacted with - one component per available action. A workbench might have three sibling interactable components (Craft, Repair, Inspect); a door needs just one. Each is independently a Tier 2 entry in whatever interactor's action list currently focuses that actor.

UMounteaInteractableComponentBase : public UWidgetComponent, public IMounteaInteractableInterface

It's a UWidgetComponent

The base class extends UWidgetComponent directly - it hosts the interaction widget itself (shown when this component is the selected action, in interaction range). The attention widget is a separate system entirely; see User Interface.

N-Interactor Slots

Unlike a single "who's using this" reference, interactables track a replicated array of occupant slots, enabling more than one interactor to use the same interactable simultaneously (a two-player lever, a shared crafting station):

USTRUCT(BlueprintType)
struct FInteractorSlot
{
    TScriptInterface<IMounteaInteractorInterface> Interactor;
    FGameplayTagContainer MatchedTags;   // tags the interactor satisfied on join
    float JoinServerTime;                // server world time at join - drives Hold/Mash progress
};

MaxInteractors caps concurrent occupants (default 1). Joining is fully server-validated:

Execute_RequestInteractorJoin(Interactable.GetObject(), Interactor);
    // → RequestInteractorJoin_Server (Reliable, WithValidation)
    // → checks: slot available? Required tags matched, Excluded tags avoided?
    //           not blocked by a sibling interactable already Active on the same actor?
    // → ActiveInteractorSlots.Add(FInteractorSlot{...})
    // → Execute_InteractionStarted(this, JoinServerTime, Interactor)

Tag Filtering

Two independent FGameplayTagContainer fields gate who can join:

Field Effect
RequiredInteractorTags Interactor must match at least one (empty = accept all)
ExcludedInteractorTags Interactor disqualified if it matches any (empty = exclude none)

This lets an interaction stay fully set up (colliders, widgets, everything) while still being turned off for specific groups - e.g. an NPC-tagged interactor excluded from a player-only console, or a Team.B-tagged interactor excluded from Team A's vehicle.

State Machine

UENUM(BlueprintType)
enum class EInteractableStateV3 : uint8
{
    EIS_Active,      // being interacted with
    EIS_Awake,       // can be interacted with, reacts to interactors (default)
    EIS_Attention,   // reserved - never assigned; see EInteractionWidgetMode::EWM_Attention
    EIS_Cooldown,    // recovering, will awake after cooldown
    EIS_Paused,      // retains progress, does not react further until resumed
    EIS_Completed,   // terminal - interaction completed, cannot re-activate
    EIS_Disabled,    // can be awakened manually
    EIS_Suppressed,  // suppressed by a dependency, cannot be interacted with
};

State changes are server-authoritative (SetState_Server) and replicated to all clients via OnRep_InteractableState, which also drives the coarse widget-visibility gate (bWidgetStateEligible) - see User Interface for how that combines with the fine-grained distance check.

Lifecycle

UENUM(BlueprintType)
enum class EInteractableLifecycle : uint8
{
    EIL_OnlyOnce,  // once finished, no more interaction allowed
    EIL_Cycled,    // finished → cooldown period → awake again; optionally bounded by LifecycleCount
};

LifecycleCount (-1 = infinite) tracks total allowed cycles when Cycled; RemainingLifecycleCount is the read-only live counter.

Timing Without FTimerHandle Replication

Progress is computed from a single replicated server timestamp rather than replicating timer state:

// Client-side progress calculation
float Progress = (ServerTime - InteractionStartServerTime) / InteractionPeriod;

InteractionStartServerTime replicates via OnRep_InteractionStartServerTime; every client derives the same progress value from its own (replicated) AGameStateBase::GetServerWorldTimeSeconds() read, avoiding the clock-skew problems of replicating a local FTimerHandle.

Component Types

Component Completion trigger Best for
Press Instant, on join Buttons, switches, quick pickups
Automatic Timer, no input required Proximity triggers, notification-style interactions
Hold Hold key for InteractionPeriod seconds Doors, levers, channeled actions
Mash N presses within a threshold QTE-style struggle interactions
Hover Cursor dwell time (extends Hold) Mouse-driven UI-style interactables

Widget Ranges

struct FInteractableWidgetRanges
{
    float AttentionRange = 500.f;    // must be >= InteractionRange
    float InteractionRange = 200.f;
};

An interactor's RefreshFocus() compares its distance to each detected interactable against these thresholds, toggling between EWM_None → EWM_Attention → EWM_Interaction - see User Interface.

Lifecycle Events

Every state transition and slot change fires a matching delegate: OnInteractorFound/Lost, OnInteractionStarted/Stopped/Completed/CycleCompleted/Canceled, OnLifecycleCompleted, OnCooldownCompleted, OnInteractorSlotsChanged, OnInteractorRejected. Each has a BlueprintImplementableEvent counterpart (OnInteractionStartedEvent, etc.) for pure-Blueprint subclasses that don't want to bind a dynamic delegate.

Setup

Interactable->Execute_SetDefaults(Interactable.GetObject());   // pulls from Interactable Runtime Config
Interactable->Execute_ActivateInteractable(Interactable.GetObject(), ErrorMessage);

See Configuration → Interactable Runtime Config, K2Nodes & Statics for the full Blueprint-facing API, and Interactor System for the detection side.