Basic behaviors — Instant, Hold and Toggle

Instant, Hold and Toggle: parameters, defaults and how each behaves across the network.

Assumes you have finished quick-start.

This page covers the three shipped behaviours, what BehaviorValue means for each, the hold move rule, cooldowns and how to register your own behaviour.

The kit ships three behaviours and registers them for you. A behaviour is the piece that decides what a press means: whether it completes at once or after a hold, what it writes to the interactable's BehaviorValue, and whether a cancel starts a cooldown. You never construct one — you pick a preset asset, and the preset's class chooses the behaviour.


The three presets

DA_Instant (UUKPreset_Instant)DA_Hold (UUKPreset_Hold)DA_Toggle (UUKPreset_Toggle)
Behaviour classUUKBehavior_InstantUUKBehavior_HoldUUKBehavior_Toggle
Params structFUKInstantParams (no fields)FUKHoldParamsFUKToggleParams
Predicted hold at press0HoldParams.HoldSeconds0
What the press beginscompletes immediatelyholds for HoldSecondscompletes immediately
Each hold frame— (never ticks)cancels with Moved when Should Cancel Hold says so, else continues—
On completioncounter + 1counter + 1flips 0 ↔ 1
On cancelstate untouched → IdleCooldown when Cooldown On Cancel is set and the preset's CooldownSeconds > 0, else Idlestate untouched → Idle
Phase after completionCooldown when CooldownSeconds > 0, else Idlesamesame
Starting BehaviorValue00Initially On ? 1 : 0

Resolution is by class, not by asset. Your own UMyPreset_Hold : UUKPreset_Hold resolves to UUKBehavior_Hold with no registration of its own — the kit walks the preset's parent chain and uses the nearest behaviour it knows. Inherited params are read as usual; fields you add are ignored by the kit behaviour. If you want different logic, register your own behaviour against a preset class you own (§ Registering your own behavior).

(The kit ships three stateful presets beside these — Door, Lever and Pickup — whose BehaviorValue is an encoding rather than a counter, and whose door and lever inherit the hold rule below. They are presets.md; this page owns the three above.)


What BehaviorValue means

BehaviorValue is one replicated int32 per interactable. The kit gives it three readings, and they are the only three:

PresetReadingRange
Instanta counter — uses since the value was last setany int32
Holda counter — completed holds since the value was last setany int32
Toggleon / off — 0 is off, every non-zero is on{0, 1} after any press

The counter wraps to 0, never to a negative number. At 2147483647 the next completion writes 0 and On Behavior Value Changed fires everywhere with (2147483647, 0). There is no overflow and no trap. At the protocol's throttle ceiling this takes roughly eight and a half years, so it is a correctness guarantee rather than something you will see.

The Toggle rule is self-healing. A value outside {0, 1} — from your own Apply State, or from a restored save — reads as on, and the next press writes 0. One press normalises any int32.

Bind On Interaction Completed, not the counter. The counter is a state, and states coalesce: when two interactors complete an Instant in the same server frame, the authority fires two value changes but every client receives one, carrying n+2. If you need "every use", bind the per-request completion event. If you need "the current value", read the counter. Using the counter to count uses will lose one on a busy frame, and only on a busy frame — the worst kind of bug to find later.

An Instant preset's BehaviorValue does not stay 0. Every completion advances it and broadcasts. If you want a value that does not move, either ignore the counter and bind On Interaction Completed, or use a Toggle.

Which machine? On Behavior Value Changed — Every machine. Which machine? On Interaction Completed — Authority and owning client. (interactor) / Authority only. (interactable)


Hold: the move rule

A Hold asks one question on every hold frame: is the interactor moving too fast to keep holding? The kit's answer is Should Cancel Hold, and its shipped rule is three tests in this order:

  1. Cancel On Move is set on the preset, and
  2. the sampled speed is a finite number, and
  3. speed > Max Hold Speed — strictly greater. A speed of exactly Max Hold Speed keeps holding.

The flag is tested first, so with Cancel On Move off the speed is never read at all.

Where it runs, and how often. Once per hold frame on the authority, and once per predicted frame on the owning client's mirror. Each machine samples its own interactor velocity exactly once per call and writes nothing — Should Cancel Hold returns an answer and that is all it does. The speed is the length of Get Interactor Velocity, which is world space: the kit reads one number from the interactor and never touches Character Movement, Mover, or any movement component (see movement.md).

What a cancel looks like. The interaction resolves Cancelled with reason Moved, the interactable's Behavior Value is untouched, and the deciding machine logs one Verbose line:

Moved: 150.0 > 10.0 cm/s

The printed threshold is the preset's Max Hold Speed — always. If you override Should Cancel Hold and decide with a different number, the line still prints the preset's, because the format carries the sampled speed and the preset field and no third value. Read the line as "the kit cancelled with reason Moved at this speed", not as "this is the number your override compared against".

The moving-platform case

Get Interactor Velocity includes the carrier. A player standing on a moving platform, a lift or a vehicle is moving by that measure, so the hold cancels Moved on the first frame the carrier moves. This is a documented limitation, not a bug: the kit reads one number and cannot know the frame of reference, and looking one up would mean a hard dependency on a movement system the kit deliberately does not have.

Two answers, in order of cost:

  • Set Cancel On Move to false on that preset. Zero code. The hold then ends only by release, completion, or the protocol's own re-validation (out of range, line of sight, target disabled).

  • Override Should Cancel Hold on your own behaviour subclass and subtract the carrier's velocity before comparing — one node in Blueprint:

    Should Cancel Hold (Context, Speed Cm Per Sec)
        -> Return:  (Speed Cm Per Sec - <your carrier's speed>) > <your threshold>
    

    The parameters you are given are the interaction Context (the interactor, the interactable and the preset) and the already-sampled Speed Cm Per Sec. Read the carrier from your own pawn.

If the pawn uses Mover (Experimental on 5.6–5.8) the sampled velocity may be zero or a frame late; the supported answer is to override the interactor's Get Interactor Velocity hook on your pawn's interactor component. movement.md covers what each pawn shape reports and why.

The override contract

Should Cancel Hold is a BlueprintNativeEvent on the behaviour class, and a behaviour class is a CDO — one shared object. Your override therefore runs:

  • on both machines (authority and owning client),
  • every hold frame,
  • for every active hold in the level at once, through the same single object.

So write it as a pure function of its two parameters that reads only. A variable stored on the behaviour is shared by every hold in the game, and the two machines will not agree about it — the authority and the client will cancel at different moments, and the result you get is whichever machine decided first.

A body that breaks the seam contract is warned, not rejected. Calling Apply State, starting or ending another interaction, or destroying the target from inside Should Cancel Hold or Tick Hold does not fail the press: the protocol's re-entrancy guard refuses the re-entrant call, logs one Warning naming your class, and the interaction still produces exactly one result. The observable consequence is yours, not the kit's — for example a Tick Hold body that disables its own target is admitted, the cancel it tried to make is refused for the duration of the call, and the hold ends one tick later as Cancelled(NotUsable) with the interactable going Engaged → Disabled and no Idle in between. The full rule, and the reason it is a warning rather than a refusal, is in protocol.md § Behavior and arbiter seams.

If the velocity is not a number

If Get Interactor Velocity returns a NaN or an infinite vector, the hold continues and the kit logs one UK-VAL-062 Warning naming the pawn class:

UK-VAL-062: BP_MyPawn_C returned a non-finite velocity during a hold; the hold continues
(cancel-on-move disabled for this sample).

One Warning per interactor component, once for the lifetime of the loaded plugin — not once per frame. Continuing is deliberate: the alternative is a hold that cancels Moved every frame forever, which looks like broken gameplay rather than like a bug.

Who wins when both machines cancel

The authority and the owning client each sample their own velocity, so both can decide to cancel. Whichever fires first wins, and exactly one result is produced — Cancelled(Moved) if the server decided, Cancelled(Released) if the client's mirror got there first and released. Either way Behavior Value is untouched and the phase follows the cooldown rule below.

Do not build on which one it will be. Under real network latency the server's view of a remote pawn's velocity lags the client's by the travel time of a move, so the client usually wins and you usually see Released; on a listen host they are the same machine. The kit promises the invariant (one result, Cancelled, reason Moved or Released, value unchanged) and never the winner.


Cooldown

Two different moments can start a cooldown, and only one of them is a behaviour's choice.

On completion — all three behaviours, and it is not theirs to decide: the interactable goes to Cooldown when the preset's Cooldown Seconds is greater than 0, and to Idle otherwise. An Instant or a Toggle with a cooldown is the "you cannot spam this lever" preset.

On cancel — only Hold, and only when you ask for it. Cooldown On Cancel (a Hold param, default false) sends a cancelled hold to Cooldown as well:

Cooldown On CancelCooldown SecondsPhase after a cancelled hold
DefaultfalseanythingIdle
Commit-or-lose-ittrue> 0Cooldown
Set but pointlesstrue0Idle — no zero-length cooldown, no state change, no broadcast

Instant and Toggle never cool down on a cancel; there is no flag for it.

Cooldown On Cancel = true with a long Cooldown Seconds is a deliberate "commit or lose it" design: a player who lets go early waits the full cooldown before trying again. It reads as broken if the cooldown is long and the hold is short, so tune the pair together.

While an interactable is in Cooldown, Get Usability reports Cooldown, the local interactor refuses the press before it is sent, and a request that arrives anyway is rejected NotUsable. Get Cooldown Remaining is the number to put on a UI.


Hold

The field you edit first on a hold preset is HoldSeconds (float, seconds, default 1.0, clamped to 0.05 — 60; the shipped DA_Hold asset carries 2.0). It is the same number the owning client predicts its gauge from and the authority times the hold with, which is why it lives on the preset and not on the component: one asset, one length, both machines.

A hold above the project's Max Hold Seconds is clamped to it, with one Warning per behavior class per session — a clamp that logs, never a silent one. The other three FUKHoldParams fields — Cancel On Move, Max Hold Speed Cm Per Sec and Cooldown On Cancel — are covered by § Hold: the move rule and § Cooldown.

Toggle

The field you edit first on a toggle preset is bInitiallyOn (Initially On, bool, default false; the shipped DA_Toggle asset carries false). It decides the starting BehaviorValue: Initially On ? 1 : 0.

It is read at every GetInitialBehaviorValue() call and never mirrored into a stored field, so a preset edited in the editor takes effect on the next spawn without a resave. A replica that joins late is told the current value by the state it receives, not by this field — the flag is the STARTING value, never a running one.

Registering your own behavior

You do not need to register anything to use the kit's behaviours: RegisterKitBehaviors() runs from the plugin's own module startup, on every machine, before any level loads.

Resolution walks the preset's parent chain. A preset resolves to the behaviour registered against the nearest ancestor class that has one, so UMyPreset_Hold : UUKPreset_Hold gets UUKBehavior_Hold with no registration of its own. Inherited params are read as usual; fields you add to your preset are simply not read by the kit behaviour.

You register only when you want different logic, and then you register it against a preset class you own — never over UUKPreset_Instant, UUKPreset_Hold or UUKPreset_Toggle, because that would change what a shipped DA_* asset does for every actor in the project.

// In your module's StartupModule():
UK::Interaction::Behaviors::Register(UMyPreset_Charge::StaticClass(),
                                     UMyBehavior_Charge::StaticClass());

Two things worth knowing before you write that line:

  • You do not have to root what you register. The registry keeps every registered UClass alive across garbage collection, including a Blueprint class.
  • Registering later is fine. Startup is the convenient moment, not a requirement; a preset with no behaviour simply rejects with NoBehavior until one is registered.

The seam itself — the five events, which machine runs each one, and the contract a body may not break — is documented once, in protocol.md § Behavior and arbiter seams. Two copies of a contract is one copy and one stale copy, so this page does not restate it.


What a behaviour costs

Measured in the kit's performance run, on 5.8 only, on a stand-in hardware tier, in an uncooked editor build — so read every figure as an upper bound that is advisory, not a promise:

CallMeasured (median)Budget
TickHold (the native move rule, one hold frame)0.1 µstarget 1 µs, ceiling 3 µs
ServerBegin + ServerComplete for an Instant, in one accept0.1 µsceiling 2 µs

A 2-second hold at 60 Hz is therefore ~120 of the first row plus one begin and one complete. Your own Should Cancel Hold override is outside these budgets — the kit measures its own classes. Get Protocol Stats reports HoldTickMicroseconds if you want to see what an override costs in your project.


Lite

The Lite edition strips content, never code. All three behaviour classes ship in Lite, so a project that authors its own DA_* preset assets gets Hold and Toggle exactly as the full edition does.

What Lite does not ship is two of the three preset assets: DA_Instant is included and DA_Hold / DA_Toggle are excluded. Because the three shipped presets live in one folder, the Lite manifest excludes those two by name today. A later version may move the presets into per-behaviour sub-folders so the exclusion becomes a folder rule; that packaging decision has not been made yet.


Known gaps

Recorded so you can read a claim as narrow as it actually is.

  • Engine coverage. In one line: "supported on 5.6–5.8; multiplayer end-to-end suite run on all three". The behaviours compile on 5.6 / 5.7 / 5.8 and their logic, reflection and structural tests run on all three, and so do the listen- and dedicated-server session tests. Same wording as protocol.md § Known gaps.
  • The packaged Lite archive has not been observed carrying the exclusion. The two exclusions are asserted against the Lite manifest in the test suite; no Lite package has been staged and listed since they were added.
  • A Blueprint-asset override of Should Cancel Hold has not been measured in a cooked build. A native subclass override is measured dispatching on both machines. The Blueprint-on-a-CDO case in a cooked -game or dedicated-server build is expected to work and is not yet proven.
  • The move rule has no grace window. It is instantaneous and stateless: one frame over the threshold cancels. A "moving for N seconds" rule is a candidate for a later version, not something you can configure today.
  • The two editor-time validator warnings are not in the Lite edition. UK-VAL-060 (a preset's Hold Seconds above the project's Max Hold Seconds) and UK-VAL-061 (Cancel On Move with a Max Hold Speed of 0) are checked before Play by the kit's editor validator (validator.md), which the Lite edition does not include. At runtime the first is clamped with a Warning and the second is legal.
  • Under real latency the cancel race has no promised winner — see § Hold: the move rule.