Named actions such as "jump", bound to keys, the mouse and gamepads, for several players, with rebinding.
Import blitz.inputactions As Input gives your program these, with Input. in front. Each name leads to its part of the reference.
CreateInput:InputMap()LastError:Str()CreateInputFrame:InputFrame()ReadInput(frame:InputFrame, seconds:Double = 0.016666666666666667D)ReadPointer(frame:InputFrame, sample:PointerSample)BindAction(input:InputMap, action:Str, binding:Binding, context:Str = "game")UseContext(input:InputMap, context:Str)SuspendInput(input:InputMap, suspended:Int = True)ActionValue:Float(input:InputMap, action:Str)ActionDown:Int(input:InputMap, action:Str)ActionHit:Int(input:InputMap, action:Str)ActionReleased:Int(input:InputMap, action:Str)UpdateInput(input:InputMap, frame:InputFrame)BeginRebind(input:InputMap, action:Str, context:Str = "game")CancelRebind(input:InputMap)RebindState:Int(input:InputMap)SaveBindings:Int(input:InputMap, file:Str)LoadBindings:Int(input:InputMap, file:Str)Bindingkind:Int = Device.Keyboard, code:Int = Key.Space, port:Int = 0, scale:Float = 1PointerSamplex:Int, y:Int, wheel:Int, dx:Int, dy:Int, dz:IntButtonDown:Int(button:Int), ButtonHit:Int(button:Int)InputMapInputFrameDeviceKeyboard, MouseButton, PointerAxis, GamepadButton, GamepadAxisKeyEscape, One, Two, Backspace, Tab, Q, W, E, R, Enter, LeftControl, A, S, D, F, LeftShift, Z, X, C, V, Space, F1, F2, Up, Left, Right, DownMouseButtonLeft, Right, Middle, Extra1, Extra2PointerAxisX, Y, WheelPadButtonSouth, East, West, North, Back, Guide, Start, LeftStick, RightStick, LeftShoulder, RightShoulder, Up, Down, Left, Right, Misc, Paddle1, Paddle2, Paddle3, Paddle4, TouchpadPadAxisLeftX, LeftY, RightX, RightY, LeftTrigger, RightTriggerRebindStatusIdle, Waiting, Complete, CancelledInput 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.

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.
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
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
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.
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.
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.
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 callKeyHit,MouseHit,GamepadHitor 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.KeyDownandMouseXare fine.
ActionHit gives the same answer all
frame, however often you ask, until the next UpdateInput.Three more complete programs come with the module:
The reference below has every function and rule.
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.
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.
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.
ActionValue: signed contributions summed in Double, then clamped to -1..1
and returned as Float. Opposite keys cancel. Motion is captured in pixels and
wheel steps; choose a fractional binding scale for sensitivity.ActionDown: Abs(ActionValue) >= 0.5. This is an action threshold, independent
of the runtime's existing gamepad dead zones.ActionHit: maximum raw hit count among unsuppressed alternatives in this
snapshot, not their sum. Two simultaneous alternatives do not double-fire.
A down threshold crossing with no raw hit supplies one hit for analogue axes.
Physical hits still count when another alternative is held, or signed values
cancel. The same physical slot cannot occur twice in one action's binding set.ActionReleased: true on down-to-not-down, or when a hit ends with the action
not down. Thus a fast tap can have both Hit and Released while Down is false.
Released is a boolean, not a count of all intervening physical releases.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.
Types are qualified by the import alias in ordinary programs. Resource fields, constructors, device reader, action records and generation are private.
| Operation | Contract |
|---|---|
CreateInput():InputMap | Caller owns an initially empty map in game. |
CreateInputFrame():InputFrame | Caller owns reusable buffers and the runtime reader. Does not poll. |
New Binding | Caller-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 PointerSample | Caller-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):Int | RebindStatus.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):Int | True only on successful SaveJsonAtomic; does not complete/cancel rebind or alter live state. |
LoadBindings(input, file):Int | Validate an owned candidate; atomically replace all bindings on success. Preserve active context and suspension. |
LastError():Str | Diagnostic for the last Save/Load. Cleared at the start of each; copy it before another fallible call. Main-thread, module-local. |
Delete input, Delete frame | Deterministic 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.
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.
{"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.
| Owner | Owned resources | Borrowed/copied inputs |
|---|---|---|
| Application | Maps, frames, Binding and PointerSample records, entities, virtual controls | None freed implicitly by this module |
| InputMap | Action records, copied binding records, ordered list, lookup map, suppression/previous/connection buffers | Update borrows frame for the call; Bind copies fields |
| InputFrame | Capture Lists and private reader | Reader 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.