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.
The trap in one picture
Section titled “The trap in one picture”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 = chargeJump, 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.
The rule
Section titled “The rule”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.0else: charge = 0.0Now the action runs every frame and decides for itself, which is the whole reason it has a Condition in the first place.
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. |
Not every one of them takes a Condition
Section titled “Not every one of them takes a Condition”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.
Which one do I want
Section titled “Which one do I want”They all count, and they differ in what they count and what they hand back.
| Action | Counts | Reach for it when |
|---|---|---|
| Do Once | nothing, just the first frame | a hit sound should play once, not every frame |
| Do N Times | frames | the first few frames differ from the rest |
| Every N Times | frames | something is too expensive to do every frame |
| Cooldown | seconds since the last pass | holding fire should give one shot per second |
| Every N Seconds | seconds, on repeat | an enemy shoots on a fixed beat |
| Every | seconds, on repeat | same beat, but the steps live inside it |
| Every Random | seconds, on repeat | an idle grunt should not sound mechanical |
| For N Seconds | seconds since entering | an invulnerability window after being hit |
| Wait | seconds, once | a game over screen pauses before the menu |
| Time In State | seconds, as a value | a progress bar of how long the state has run |
| Held For | seconds a condition stayed true | a button has to be held two seconds |
| Hold Charge | the same, as 0 to 1 | that hold needs a bar on screen |
| Just Became True | the previous answer | only the frame a condition flips |
| When It Changes | the previous value | the weapon changed, and the old one matters |
| Crossed | which side of a limit | health dropped past 20 |
| Double Tap | the time of the last press | two quick taps mean dash |
| Buffered Press | the time of the last press | a jump pressed just before landing should still jump |
| Spring To | a velocity | a 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.