Input Actions

Named actions such as "jump", bound to keys, the mouse and gamepads, for several players, with rebinding.

Commands

Import blitz.inputactions As Input gives your program these, with Input. in front. Each name leads to its part of the reference.

Functions

Types

Enums

Guide

Input Actions lets your game ask "is the player jumping?" instead of "is Space down, or the gamepad's A button?". You give each action a name, such as "jump" or "move", and say which keys, mouse buttons and gamepad sticks work it. The game then only ever asks about actions, so players can have the controls they like, choose new keys while they play, and play together with keyboards and gamepads mixed.

A yellow ball jumping across the screen, with the move action reading 1.0

Try it

Open jumper.bb and run it. Move the ball with the arrows, with A and D, or with a gamepad's left stick, and jump with Space or the gamepad's bottom button. Then press R, and press any key to jump with that instead.

; Move with the arrows, A and D, or a gamepad's stick. Jump with Space or the
; gamepad's bottom button. R chooses a new jump key, and Escape quits.
Dialect "modern"
Import blitz.inputactions As Input

; Adds a key, button or stick to an action. The map keeps its own copy.
Function BindControl(controls:Input.InputMap, action:Str, kind:Int, code:Int, scale:Float = 1)
    Local binding:Input.Binding = New Input.Binding
    binding.kind = kind
    binding.code = code
    binding.scale = scale
    Input.BindAction controls, action, binding
    Delete binding
End Function

Graphics 640, 260, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79

; The game asks about actions by name, and never needs to know which keys,
; buttons or sticks work them. A scale of -1 makes Left and A move left.
Local controls:Input.InputMap = Input.CreateInput()
BindControl controls, "move", Input.Device.Keyboard, Input.Key.Left, -1
BindControl controls, "move", Input.Device.Keyboard, Input.Key.A, -1
BindControl controls, "move", Input.Device.Keyboard, Input.Key.Right
BindControl controls, "move", Input.Device.Keyboard, Input.Key.D
BindControl controls, "move", Input.Device.GamepadAxis, Input.PadAxis.LeftX
BindControl controls, "jump", Input.Device.Keyboard, Input.Key.Space
BindControl controls, "jump", Input.Device.GamepadButton, Input.PadButton.South
BindControl controls, "rebind", Input.Device.Keyboard, Input.Key.R
BindControl controls, "quit", Input.Device.Keyboard, Input.Key.Escape

; A frame holds what the keyboard, mouse and gamepads are doing.
Local frame:Input.InputFrame = Input.CreateInputFrame()
Local x:Float = 320
Local height:Float ; How far the ball is above the ground,
Local rising:Float ; and how fast it's going up.
Local running:Int = True
Local previous:Int = MilliSecs()
While running
    Local now:Int = MilliSecs()
    Local seconds:Double = Double(now - previous) / 1000D
    previous = now
    ; Read the devices once a frame, then let the map work out its actions.
    Input.ReadInput frame, seconds
    Input.UpdateInput controls, frame

    ; An action's value goes from -1 to 1. A stick gives anything in between.
    Local move:Float = Input.ActionValue(controls, "move")
    x = x + move * Float(seconds) * 300
    If x < 20 Then x = 20
    If x > 620 Then x = 620
    If Input.ActionHit(controls, "jump") > 0 And height = 0 Then rising = 600
    rising = rising - Float(seconds) * 1800
    height = height + rising * Float(seconds)
    If height < 0
        height = 0
        rising = 0
    EndIf
    ; While a new key is chosen, the map's other actions are switched off.
    If Input.ActionHit(controls, "rebind") > 0 Then Input.BeginRebind controls, "jump"
    If Input.ActionHit(controls, "quit") > 0 Then running = False

    Cls
    Color 40, 90, 140
    Rect 0, 220, 640, 40, True
    Color 20, 60, 90
    Oval x - 18, 224, 36, 10, True
    Color 255, 220, 0
    Oval x - 20, 180 - height, 40, 40, True
    Color 255, 255, 255
    Text 20, 16, "Move: arrows, A and D, or a stick.  Jump: Space or the pad's bottom button."
    If Input.RebindState(controls) = Input.RebindStatus.Waiting
        Text 20, 36, "Press a key or button to jump with, or Escape to cancel."
    Else
        Text 20, 36, "R: choose a new key for jumping."
    EndIf
    Text 20, 56, "move: " + move
    Flip
Wend
Delete controls
Delete frame
End

A map holds the game's actions, and a frame holds what the devices are doing. Each frame, the program reads the devices once, the map works out every action from that, and the game asks about the actions as often as it likes. Left and Right both work "move": Left with a scale of -1, so the two cancel out when you hold both. Escape is an action too, so that while you choose a key it cancels the choice instead of quitting.

How do I...

Do something while a button is held, or when it's let go?

ActionHit says the action has just been pressed, ActionDown that it's held, and ActionReleased that it has just been let go. Charge a shot while the button is held, and fire it on release:

If Input.ActionDown(controls, "fire") Then power = power + Float(seconds)
If Input.ActionReleased(controls, "fire")
    Print "Fire, with power " + power
    power = 0
EndIf

Use the mouse?

Bind its buttons like keys, and copy the pointer from the frame to see where it is.

BindControl controls, "fire", Input.Device.MouseButton, Input.MouseButton.Left
Local pointer:Input.PointerSample = New Input.PointerSample
; Then, each frame, after ReadInput:
Input.ReadPointer frame, pointer
Oval pointer.x - 4, pointer.y - 4, 8, 8

Give a pause menu its own keys?

Every action belongs to a context: "game" unless you name another. Only the current context's actions work, so put the menu's actions in a context of their own and switch to it.

Local binding:Input.Binding = New Input.Binding
binding.code = Input.Key.Enter
Input.BindAction controls, "choose", binding, "menu"
Delete binding
Input.UseContext controls, "menu" ; When the game pauses.
Input.UseContext controls, "game" ; When it carries on.

After a switch, keys that are still held do nothing until they're let go, so the key that opened the menu can't also choose something in it.

Let two players share the keyboard?

Give each player a map of their own. Read the devices once, and give the same frame to both maps.

Local playerOne:Input.InputMap = Input.CreateInput()
Local playerTwo:Input.InputMap = Input.CreateInput()
BindControl playerOne, "jump", Input.Device.Keyboard, Input.Key.W
BindControl playerTwo, "jump", Input.Device.Keyboard, Input.Key.Up
; Each frame:
Input.ReadInput frame, seconds
Input.UpdateInput playerOne, frame
Input.UpdateInput playerTwo, frame

Gamepads are told apart by their port: set a binding's port to 0 for the first gamepad, 1 for the second, and so on.

Save the player's keys?

SaveBindings writes every action's keys to a file, and LoadBindings puts them back.

If Not Input.SaveBindings(controls, "controls.json") Then Print Input.LastError()
; When the game starts again, after binding the usual keys:
If FileType("controls.json") = 1
    If Not Input.LoadBindings(controls, "controls.json") Then Print Input.LastError()
EndIf

Loading replaces all the map's keys, or, if the file is damaged, changes nothing.

Ignore the controls for a while?

Suspend the map, for example during a cut-scene, or while your game is in the background.

Input.SuspendInput controls        ; Every action reads as not pressed.
Input.SuspendInput controls, False ; Carry on, once held keys are let go.

Blip's tip

Once you use ReadInput, don't also call KeyHit, MouseHit, GamepadHit or the mouse's speed commands yourself. They count presses and movement since they were last called, so they would take them from the module, and your actions would miss them. KeyDown and MouseX are fine.

Good to know

More examples

Three more complete programs come with the module:

The reference below has every function and rule.

Reference

Needs
No other modules
Examples
quick_start.bb, rebinding.bb, two_players.bb

blitz.inputactions is an unpublished 0.1.0 source preview for Extended Blitz3D+. It turns one hardware capture into named, non-consuming actions for several players, one active context per player, and a rebinding menu. It has no module dependencies and changes no runtime dead zones or virtual controls. Requires the compiler/runtime capability object-lifecycle-v1.

Blitz3D+ comes with this module, so any Extended program can Import blitz.inputactions. Open an example and run it. No assets or network connection is needed.

Start with a named button

Dialect "modern"
Import blitz.inputactions As Input
Local controls:Input.InputMap = Input.CreateInput()
Local frame:Input.InputFrame = Input.CreateInputFrame()
Local binding:Input.Binding = New Input.Binding
binding.code = Input.Key.Space
Input.BindAction controls, "jump", binding
Delete binding ; The map copied it.

; Inside your loop, after the runtime pumps input (normally through Flip):
Input.ReadInput frame, 1D / 60D
Input.UpdateInput controls, frame
If Input.ActionHit(controls, "jump") > 0 Then Print "Jump!"

Delete controls
Delete frame
End

Run quick_start.bb for a complete source-generated moving cube. Arrows and the controller's left stick are interchangeable; Escape exits. Negative and positive key scales form an axis. The loop computes real elapsed seconds, limits an example movement step after a long pause, captures once, updates the map, moves the cube, draws and flips.

rebinding.bb adds a jumping cube and a working menu. Tab/controller Start opens it. Arrows/D-pad navigate and Enter/south chooses; mouse clicks choose the drawn buttons. Rebind replaces jump's alternatives, Save and Load operate on a session-owned file, and Escape/east or the visible Cancel button cancels capture. This example deletes its uniquely named save at exit. A game should instead choose its own persistent user-data path.

two_players.bb shares one frame among two player maps and a menu map. A/D and arrows move the players; Tab/Start pauses and resumes; Enter/south or the visible button resets both players while paused. Touch-capable runtimes create an existing standard virtual gamepad and bind its actual port. The group requests port 1 so it cannot also drive player 1; a creation failure is displayed. The example owns and frees that virtual-control group.

All three examples have finite --verify paths.

Captures and action semantics

Call ReadInput(frame, seconds) once per application tick, then pass that same frame to every UpdateInput. Do not also consume KeyHit, MouseHit, GamepadHit, MouseXSpeed, MouseYSpeed or MouseZSpeed elsewhere. Capture does not pump GUI events or call Flip, UpdateWorld, FlushKeys or UpdateMobileInput. The application remains responsible for the normal runtime event/update loop.

A frame captures keys 1..255, mouse buttons 1..5, pointer X/Y/wheel motion and normalised gamepad buttons 1..21 / axes 1..6 on ports 0..15. Missing controls and unmapped/missing pads read zero. It uses JoyGamepad, GamepadDown, GamepadHit and GamepadAxis; there is no invented GamepadConnected command. Captured hit counts preserve a press/release between snapshots even when Down is already zero.

Every capture publishes a unique increasing Long generation, shared across frame instances. A map ignores its last generation and older generations. Repeating Update cannot advance capture timing, consume hits or resurrect edges cleared by a mutation. Do not deliver snapshots out of order. Generation exhaustion fails before polling, instead of wrapping and accepting stale snapshots.

Queries do not consume anything and remain stable until a later update or an explicit mutation. Unknown actions/current contexts return zero. Names are case-sensitive, 1..64 ASCII letters, digits, _, - or .. The limits are 256 context/action pairs per map and 16 distinct physical bindings per pair.

Public operations

Types are qualified by the import alias in ordinary programs. Resource fields, constructors, device reader, action records and generation are private.

OperationContract
CreateInput():InputMapCaller owns an initially empty map in game.
CreateInputFrame():InputFrameCaller owns reusable buffers and the runtime reader. Does not poll.
New BindingCaller-owned copied input with kind, code, port, scale. Defaults: keyboard, Space, port 0, scale 1.
BindAction(input, action, binding, context="game")Copy/add an alternative; same physical slot updates its scale. Cancel pending rebind; clear action state and suppress held controls when editing an already updated map.
UseContext(input, context)Activate one context; clear edges, cancel capture and suppress held controls. Selecting the same context is a no-op. Unknown context is empty.
ReadInput(frame, seconds=1D/60D)Poll once. Seconds must be finite and nonnegative; use real input time, not game time scale. Zero is valid.
UpdateInput(input, frame)Borrow a captured frame only for this call. No callbacks or per-update resource allocation.
ActionValue/ActionDown/ActionHit/ActionReleased(input, action)Read the current context's resolved state as described above.
New PointerSampleCaller-owned reusable copied output; no destructor needed.
ReadPointer(frame, sample)Copy x,y,wheel,dx,dy,dz without polling. sample.ButtonDown(1..5) and ButtonHit(1..5) read captured buttons. Modifying the copy cannot mutate the frame.
BeginRebind(input, action, context="game")Begin/replace a pending capture for this action/context. Existing bindings stay intact until success. Creating a new named action is allowed within limits.
CancelRebind(input)Cancel only a pending capture; harmless otherwise. Does not save or change bindings.
RebindState(input):IntRebindStatus.Idle, Waiting, Complete or Cancelled. Last terminal state remains until the next Begin.
SuspendInput(input, suspended=True)Clear edges, cancel capture and suppress input; False resumes through a neutral gate. Idempotent for the current state.
SaveBindings(input, file):IntTrue only on successful SaveJsonAtomic; does not complete/cancel rebind or alter live state.
LoadBindings(input, file):IntValidate an owned candidate; atomically replace all bindings on success. Preserve active context and suspension.
LastError():StrDiagnostic for the last Save/Load. Cleared at the start of each; copy it before another fallible call. Main-thread, module-local.
Delete input, Delete frameDeterministic owned-resource cleanup. Never polls, saves, changes hardware configuration or commits a pending capture.

Device, Key, MouseButton, PointerAxis, PadButton and PadAxis name the runtime codes. Key names common game/menu keys; any valid 1..255 scan code may also be used. Other enum values match the complete normalised runtime ranges. Enums are Int constants, so boundaries validate them. Non-pad bindings require port 0. Scale must be finite, nonzero and within -1..1. Invalid programming input, Null/deleted resources, unread frames and invalid pointer buttons raise a useful blitz.inputactions: RuntimeError; they do not return a silent failure.

Procedures use Blitz's ordinary implicit Int result; ignore it. Save/Load and queries have meaningful results. There are no duplicate method/function APIs, public device injection hooks, Stop aliases or Commit commands. Capture commits only after its acceptance rules; Cancel and Save remain distinct explicit actions.

Context, lifecycle and rebinding

Input.UseContext controls, "menu"
Input.BeginRebind controls, "jump", "game"
; Continue ReadInput/UpdateInput every tick, even with the game paused.
If cancelButtonClicked Then Input.CancelRebind controls
If Input.RebindState(controls) = Input.RebindStatus.Complete Then Print "Rebound"
Input.UseContext controls, "game"

Switching context clears prior edges immediately and suppresses every previously held control until a neutral sample. If a map has never observed a frame, the next capture forms that barrier. Suppression clears after resolving a neutral frame, so a fast tap on that same release frame cannot leak into the new context. Do not rely on replaying a previous frame after changing context.

Suspend both on focus/lifecycle loss and during a game-owned modal pause, then resume when appropriate. Keep updating suspended maps if possible. Resume also examines its first new capture, so missing updates during suspension cannot leave unseen held controls active. Explicit suspension clears rather than emits release edges. In ordinary updates a disconnected pad contributes zero and emits releases from the map's last action state; a newly observed/reconnected held pad remains suppressed until neutral. Stable port numbers do not establish persistent physical device identity if a device is replaced entirely between captures.

There is no portable application-focus getter in the inspected runtime API and this module never consumes the GUI event queue. Wire SuspendInput from the application's own lifecycle/activation events where available. No universal automatic focus-handling guarantee is made.

A pending rebind suppresses normal actions on that map. Use a second menu map or the pointer sample for Cancel, and route cancellation BEFORE updating the capturing map. Otherwise the cancel button could itself become the binding. Capture first waits for all eligible keys, buttons and pad axes to be neutral in a fresh snapshot; pointer motion/wheel are ignored. Escape's captured hit cancels even before arming. The next eligible button/key (including a fast tap) wins; keyboard codes, mouse codes, then pad ports and button codes give a deterministic tie order. Digital candidates precede axes.

For an axis: enter at absolute magnitude >=0.65; stay on the same signed side at >=0.50 for 0.15 seconds. Dropping below 0.50 or disconnecting resets dwell. Axis neutral is <=0.25. Dwell starts at the first crossing, not during the preceding interval; repeated generations and zero seconds add no time. A negative-side capture uses scale -1, so that direction activates positively. Successful capture replaces the entire alternative set and suppresses the captured held control until release. There is no timeout: the application offers explicit cancellation.

Save format and failure behaviour

{"version":1,"bindings":[
  {"action":"jump","context":"game","kind":1,"code":57,"port":0,"scale":1}
]}
If Input.SaveBindings(controls, "controls.json") = False Then Print Input.LastError()
If Input.LoadBindings(controls, "controls.json") = False Then Print Input.LastError()

The schema rejects unknown root/row fields, missing members, wrong JSON types, unsupported versions, fractional/out-of-Int codes, invalid ranges, zero or nonfinite scales, duplicate physical slots per action/context, invalid names and capacity overflow. A numeric scale too small to survive conversion to Float is rejected. The runtime's bounded LoadJson reader rejects source files over 64 MiB before parsing; its own JSON depth/value/text limits also apply. Empty binding arrays are valid. Absent devices do not invalidate saved bindings. Bindings preserve insertion order for deterministic saves; no Map iteration order decides the document.

A failed load preserves bindings, action values/edges, active context, suspension and pending capture. A successful load clears action state, cancels capture and suppresses held controls. Version 1 stores no current context, generation, action state, dead-zone configuration or transient rebind state. Save uses the runtime's atomic-file implementation, without claiming persistence guarantees beyond the host/browser runtime.

Ownership and limits

OwnerOwned resourcesBorrowed/copied inputs
ApplicationMaps, frames, Binding and PointerSample records, entities, virtual controlsNone freed implicitly by this module
InputMapAction records, copied binding records, ordered list, lookup map, suppression/previous/connection buffersUpdate borrows frame for the call; Bind copies fields
InputFrameCapture Lists and private readerReader receives a borrowed frame only during Capture

Deletion abandons pending work, clears ownership links and deletes children before containers. Deleting one player cannot destroy a shared frame or another map. Leaving scope/assigning Null does not invoke destructors. Delete resources explicitly before End. Already-deleted/Null aliases are harmless to Delete, but other operations reject them. No arbitrary public callbacks run; private reader recursion, querying a partial capture and deleting a capturing frame are programming errors.

Update, query and real capture successful paths allocate no Types or collections.

Still deferred: stacked contexts, chord grammar, gestures, raw joystick drivers, per-device persistent identity, user callbacks and the later UI adapter. Core remains main-thread only.