Skip to content

Actions That Count

Most actions do their whole job in the frame they run: move a body, play a sound, spawn a scene. A handful do something different. They remember a number between frames and only pay off once enough frames have gone by.

That memory is what makes them useful, and it is also the one thing in Hengo that behaves differently from everything around it. This page is about that difference.

Say you want to charge a shot, but only while the player is standing on the floor. The obvious graph puts the question first and the charge inside it:

Is On Floor
└ True → Hold Charge (Condition: Key pressed X)

That reads correctly and it is wrong. Here is the code it generates:

if _ref.is_on_floor():
if Input.is_key_pressed(KEY_X):
charge += delta / 1.0
else:
charge = 0.0 # the reset lives in here
_ref.charge = charge

Jump, and is_on_floor() goes false. Nothing inside runs, and that nothing includes the line that clears the charge. The bar freezes half full, in the air, and only snaps back when you land.

The reason is that a counting action is two things sharing one body: the counting and the reset. Wrapping it in a branch takes both away at once. An action that only does something is happy to be skipped, because not happening is the point. An action that counts is not.

Nesting an action under a branch is fine. Firing, spawning, playing a sound: if the branch is false, it does not happen, which is exactly what you asked for.

Nesting an action that counts freezes it. Its clock stops instead of resetting, and it keeps whatever it had until the branch opens again.

When the branch was meant to be a condition and not a gate, put it where it belongs: in the action’s own Condition field, joined by Combine Checks.

Hold Charge
Condition: Combine Checks( Is On Floor , and , Key pressed X )
if (_ref.is_on_floor() and Input.is_key_pressed(KEY_X)):
charge += delta / 1.0
else:
charge = 0.0

Now the action runs every frame and decides for itself, which is the whole reason it has a Condition in the first place.

Every Frame
Hold Charge Flow
Condition Is On Floor and Key pressed | Seconds 1.0 | → Full | → Charging

Fills a value from 0 to 1 while the condition stays true, and drops it back to 0 the moment it breaks.

Input Type Default Description
Condition bool - The test that has to stay true. Put every part of the test in here, joined by Combine Checks, instead of wrapping the action in a branch.
Seconds float 1.0 How long holding takes to reach a full charge.
Output Type Description
Yes bool How full the charge is, from 0 to 1. Read it straight into a progress bar.
Branch Goes to
Full Once the charge reached 1, every frame the condition holds.
Charging While it is still filling, or empty.

Phases Every FramePhysics

Four of the counting actions have a Condition field, so the fix above applies directly: Hold Charge, Held For, Just Became True and Double Tap.

The rest have no such field, and for those nesting really is the only way to gate them. That is not always a bug: a Cooldown that pauses while the player is dead is usually what you want. Ask yourself which of the two you meant:

  • the clock should keep running while the condition is false: do not nest it. Move the gate up into a state, so the action is not running at all.
  • the clock should hold where it is: nest it, and know that it will pick up where it left off.

Freezing is a real answer. It just has to be the one you chose.

They all count, and they differ in what they count and what they hand back.

ActionCountsReach for it when
Do Oncenothing, just the first framea hit sound should play once, not every frame
Do N Timesframesthe first few frames differ from the rest
Every N Timesframessomething is too expensive to do every frame
Cooldownseconds since the last passholding fire should give one shot per second
Every N Secondsseconds, on repeatan enemy shoots on a fixed beat
Everyseconds, on repeatsame beat, but the steps live inside it
Every Randomseconds, on repeatan idle grunt should not sound mechanical
For N Secondsseconds since enteringan invulnerability window after being hit
Waitseconds, oncea game over screen pauses before the menu
Time In Stateseconds, as a valuea progress bar of how long the state has run
Held Forseconds a condition stayed truea button has to be held two seconds
Hold Chargethe same, as 0 to 1that hold needs a bar on screen
Just Became Truethe previous answeronly the frame a condition flips
When It Changesthe previous valuethe weapon changed, and the old one matters
Crossedwhich side of a limithealth dropped past 20
Double Tapthe time of the last presstwo quick taps mean dash
Buffered Pressthe time of the last pressa jump pressed just before landing should still jump
Spring Toa velocitya value should overshoot and settle instead of snapping

The two closest pairs are worth spelling out. Cooldown fires right away and then makes you wait; Every N Seconds waits first and then fires. Held For tells you the hold is complete; Hold Charge tells you how far along it is.