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
BehaviorValuemeans 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 class | UUKBehavior_Instant | UUKBehavior_Hold | UUKBehavior_Toggle |
| Params struct | FUKInstantParams (no fields) | FUKHoldParams | FUKToggleParams |
| Predicted hold at press | 0 | HoldParams.HoldSeconds | 0 |
| What the press begins | completes immediately | holds for HoldSeconds | completes immediately |
| Each hold frame | — (never ticks) | cancels with Moved when Should Cancel Hold says so, else continues | — |
| On completion | counter + 1 | counter + 1 | flips 0 ↔ 1 |
| On cancel | state untouched → Idle | Cooldown when Cooldown On Cancel is set and the preset's CooldownSeconds > 0, else Idle | state untouched → Idle |
| Phase after completion | Cooldown when CooldownSeconds > 0, else Idle | same | same |
Starting BehaviorValue | 0 | 0 | Initially 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:
| Preset | Reading | Range |
|---|---|---|
| Instant | a counter — uses since the value was last set | any int32 |
| Hold | a counter — completed holds since the value was last set | any int32 |
| Toggle | on / 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, carryingn+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:
- Cancel On Move is set on the preset, and
- the sampled speed is a finite number, and
speed > Max Hold Speed— strictly greater. A speed of exactlyMax Hold Speedkeeps 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 overrideShould Cancel Holdand 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 Movetofalseon 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 Holdon 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-sampledSpeed 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 Cancel | Cooldown Seconds | Phase after a cancelled hold | |
|---|---|---|---|
| Default | false | anything | Idle |
| Commit-or-lose-it | true | > 0 | Cooldown |
| Set but pointless | true | 0 | Idle — 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
UClassalive 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
NoBehavioruntil 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:
| Call | Measured (median) | Budget |
|---|---|---|
TickHold (the native move rule, one hold frame) | 0.1 µs | target 1 µs, ceiling 3 µs |
ServerBegin + ServerComplete for an Instant, in one accept | 0.1 µs | ceiling 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 Holdhas 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-gameor 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'sHold Secondsabove the project'sMax Hold Seconds) andUK-VAL-061(Cancel On Movewith aMax Hold Speedof0) 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.