Skip to main content

Input module

Input splits the player's input into actions: "jump", "shoot", "left". Each action is a number from 0 to 15, and with map() you tie it to a key (on Windows) or to the GPIO pin of a button (on the boards). The game then asks about the action with pressed(), without knowing what is behind it. With set(), another source (an I2C gamepad, the touch screen) can drive the same actions.

JARU Input module screenshot for mapping keyboard actions, buttons, and game controls

use Input

Platforms​

WindowsESP32 / ESP32-S3ESP32-P4ESP32-S31
What map() tiesA key, by its nameA GPIO pin, from 0 to 63A GPIO pin, from 0 to 63A GPIO pin, from 0 to 63, or a key of a USB keyboard
When what it reads changesThe keys, only inside Display.update()When you query itWhen you query itWhen you query it

Each platform takes only one kind: on Windows, a pin number throws ERR_TYPE_MISMATCH, and on the boards a key name throws it (except on the ESP32-S31). For the same program to work on all of them, choose the mapping with platform():

use Input

const JUMP = 0

if (platform() == HW_WIN32) then
Input.map(JUMP, "SPACE") // on Windows, the space bar
else
Input.map(JUMP, 15) // on the board, a button between pin 15 and GND
end
  • Which pin each button of your board has, the I2C gamepad and the USB keyboard of the ESP32-S31: Buttons, pins and gamepad.
  • In a game with rooms, the controls of the Game tab are mapped by SceneInputMap(): see The room and the game.

Functions​

FunctionWhat it doesReturns
map(action, key or pin)Ties the action to a key or a pin.true
pressed(action)Tells whether the action is pressed.true or false
justPressed(action)Tells whether the action has just been pressed.true or false
released(action)Tells whether the action has just been released.true or false
set(action, state)Sets the state of the action from your program.true
update()Reads the inputs again.true

All the functions throw their errors, so try catches them:

  • the wrong number of arguments: ERR_INVALID_ARG_NUMBER;
  • an action that is not an integer: ERR_TYPE_MISMATCH;
  • an action outside 0 to 15: ERR_INVALID_VALUE.

map​

map(action, key or pin) ties the action to a key or a pin.

use Input

// A keyboard game
Input.map(0, "W") // up
Input.map(1, "S") // down
Input.map(2, "A") // left
Input.map(3, "D") // right
Input.map(4, "SPACE") // jump
Input.map(5, "CTRL") // shoot
ParameterTypeWhat it is
actionintegerThe action, from 0 to 15.
key or pinstring or integerOn Windows, the name of the key. On the boards, the pin number, from 0 to 63. On the ESP32-S31, either of them.

It returns true. Besides the errors of all the functions, it throws:

  • with a type the platform does not take (a number on Windows, a name on the boards): ERR_TYPE_MISMATCH;
  • with a key name that does not exist, or a pin outside 0 to 63: ERR_INVALID_VALUE.

Mapping an action again changes its key or pin, clears whatever set() had put and leaves it released.

On the boards, the pin becomes an input with the internal pull-up resistor: connect the button between the pin and GND. The action is pressed while the pin is low. The VM does not check that the pin exists on your board: only that it is between 0 and 63.

Pins 34 to 39 of the ESP32

On the ESP32 (not on the S3), pins 34 to 39 have no internal pull-up resistor. A button on them, without an external resistor between the pin and 3.3 V, leaves the pin floating and the action does not read the button properly. Use another pin or add the resistor. The pins that do not work for a button on each chip are in Buttons, pins and gamepad.

The keys​

On Windows and on the USB keyboard of the ESP32-S31, the keys are named like this, in capitals ("space" gives ERR_INVALID_VALUE):

GroupKeys
LettersA to Z
Numbers0 to 9
ArrowsUP, DOWN, LEFT, RIGHT
SpecialSPACE, ENTER, ESC, TAB
ModifiersSHIFT, CTRL, ALT (the left ones)

pressed​

pressed(action) returns true while the action is pressed: by its key or pin, or by set(). An unmapped action returns false.

use Input

const LEFT = 2
const RIGHT = 3
var x = 160
var speed = 2

Input.map(LEFT, "A")
Input.map(RIGHT, "D")

// In each frame: continuous movement while it is held down
if (Input.pressed(LEFT)) then
x = x - speed
end
if (Input.pressed(RIGHT)) then
x = x + speed
end

It is the safe query: if you read it once per frame, you see every press that lasts at least one frame (see When the inputs are read).

justPressed​

justPressed(action) returns true if the action has gone from released to pressed since the previous reading of the inputs. It is for what happens once per press: jumping, shooting.

use Input

const JUMP = 4
Input.map(JUMP, "SPACE")

if (Input.justPressed(JUMP)) then
println("Jump!")
end
justPressed can miss presses in 0.9.7

The VM reads the inputs again every time you query an action in a different millisecond from the previous reading, and each reading turns the current state into the "previous" one (see When the inputs are read). The edge of justPressed() and released() only lasts until the next reading. If within one frame you query the inputs several times (the same action or others) and the millisecond changes between one query and the next, the second reading wipes out the edge and the press is lost. It is a known issue of 0.9.7.

Read pressed() only once per frame for each action and work out the edges yourself:

use Input

const JUMP = 4
Input.map(JUMP, "SPACE")

// Before the main loop
var jumpBefore = false

// In each frame
var jump = Input.pressed(JUMP)
var jumpPressed = jump and not jumpBefore // instead of Input.justPressed(JUMP)
var jumpReleased = not jump and jumpBefore // instead of Input.released(JUMP)
jumpBefore = jump

released​

released(action) returns true if the action has gone from pressed to released since the previous reading of the inputs. It misses edges just like justPressed(): use the same workaround.

use Input

const JUMP = 4
Input.map(JUMP, "SPACE")

if (Input.released(JUMP)) then
println("You let go of the jump")
end

set​

set(action, state) sets the state of an action from your program. It is the external source of the action: an I2C gamepad driver, a port expander or the touch screen can feed the same actions as the keyboard or the GPIOs, and the game keeps reading pressed(), justPressed() and released() without knowing what is behind them.

use Input

const JUMP = 4
var buttonA = false // what your gamepad driver reads

// In each frame, the gamepad driver sets the state of the button
Input.set(JUMP, buttonA)

if (Input.justPressed(JUMP)) then
println("Jump!")
end
ParameterTypeWhat it is
actionintegerThe action, from 0 to 15.
statebooleantrue if the action is pressed. An integer works too: 0 is released. Any other type throws ERR_TYPE_MISMATCH.

It returns true.

  • If the action also has a key or a pin, the two sources add up: the action is pressed if either of them is. The edge is worked out as always, when the inputs are read again.
  • If the action has no key or pin, set() leaves it mapped to this source only, and the edge of justPressed() or released() is worked out in set() itself: it lasts until the next call, however many times you query in between. Call set() once per frame.
  • map() on that action clears the external source.

update​

update() reads the inputs again, as the next query would, and returns true. If it falls in the same millisecond as the previous reading, it does nothing. It is hardly ever needed: pressed(), justPressed() and released() already read by themselves.

When the inputs are read​

The VM does not read the inputs at a fixed rate:

  • It reads them when you query them. Every time you call pressed(), justPressed(), released() or update() in a different millisecond from the previous reading, the VM reads again all the actions that have a key or a pin. The queries that fall in the same millisecond share the reading.
  • Each reading turns the current state into the "previous" one. justPressed() and released() compare the two: that is why their edge only lasts until the next reading.
  • On Windows, the keyboard only changes inside Display.update(), which is where the window receives the key presses. Without Display.update() in the loop, the keys never change. On the boards, the pin and the USB keyboard are read at that moment.
  • An action that only set() drives is not read again: its state changes at each set().

The safe way is to read each action once per frame with pressed(), keep the value in a variable and work out the edges you need yourself, as in the Full example.

Full example​

A square that moves with the arrows, counts the jumps and closes with ESC. It reads each action once per frame and works out the edge of the jump itself.

use Display, Input

const LEFT = 0
const RIGHT = 1
const JUMP = 2
const QUIT = 3

if (platform() == HW_WIN32) then
Input.map(LEFT, "LEFT")
Input.map(RIGHT, "RIGHT")
Input.map(JUMP, "SPACE")
Input.map(QUIT, "ESC")
else
// Example pins: use the ones of your board's buttons
Input.map(LEFT, 32)
Input.map(RIGHT, 33)
Input.map(JUMP, 25)
Input.map(QUIT, 26)
end

Display.open(320, 240)
Display.mode(2)
Display.fps = 30
var draw = Display.draw

var x = 160
var jumps = 0
var jumpBefore = false
var quit = false

while (not quit)
// Each action, once per frame
var left = Input.pressed(LEFT)
var right = Input.pressed(RIGHT)
var jump = Input.pressed(JUMP)
quit = Input.pressed(QUIT)

if (left) then
x = x - 3
end
if (right) then
x = x + 3
end
if (jump and not jumpBefore) then // the edge, by hand
jumps = jumps + 1
end
jumpBefore = jump

draw.cls(0x000000)
draw.color = 0x00FF00
draw.rect(x - 8, 112, 16, 16, true)
draw.text(8, 8, "Jumps: " + jumps.toString(), 0xFFFFFF)
Display.update() // on Windows, the keys change here
end
Display.close()

Pitfalls and known issues​

  • justPressed() and released() can miss presses if you query the inputs several times in the same frame: see justPressed.
  • On Windows, without Display.update() no key changes. The keyboard is read from the window messages, which are handled there.
  • Keys in capitals: "SPACE", not "space".
  • At most 16 actions, from 0 to 15. If you need more, make the same action mean different things depending on the moment of the game (in a menu, during play).
  • On the ESP32-S31, an action has a pin or a key, not both: the last map() replaces the previous one. To read both, use two actions, or set().
  • Input does not filter button bounce. A mechanical button can open and close several times when pressed, and each change that falls in a different reading is an edge. If it happens to you, ignore the changes for a few milliseconds after each one.
  • When the program ends, the actions go back to unmapped, also when the board loads the program again without restarting.