Skip to content

Processing Inputs

Gamewall exposes two main paradigms for submitting inputs: polling and manual input. When using input polling, the Gamewall Engine queries the game at a predictable rate, while manual input involves calling the sendInput function whenever the game deems necessary. We recommend using polling for most games using continuous inputs. However, for some game types, like card games and most board games, or games using exclusively instantaneous "tap" and no sustained "hold" style inputs, the sparse pattern of sendInput might work better with a well-designed anticipate function.

gamewall input anticipation

Input Polling

To use the built-in polling functionality, we simply need to define the poll function:

typescript
type poll = (previousInputs: Input[], meta: {
    game: Metadata,
    env: GamewallEnvironment,
    seed: number,
    random: PRNG,
}) => Array<{ input: Input, player: number | null }>

This function is automatically called at a regular tick interval, intended to collect the current state of the inputs to fill a dense chart. Gamewall does expect the developer to keep track of their own physical buttons, so a simple input might look something like this:

typescript
import { canvas } from './setup'

let holding = false
export const getHolding = () => holding // for future use in render.ts

document.body.addEventListener('keydown', (event) => {
    if (event.key === ' ') holding = true
})

document.body.addEventListener('keyup', (event) => {
    if (event.key === ' ') holding = false
})

canvas.addEventListener('touchstart', () => holding = true)
canvas.addEventListener('touchend', () => holding = false)

canvas.addEventListener('mousedown', () => holding = true)
canvas.addEventListener('mouseup', () => holding = false)

export const poll: GameTypes['poll'] = (_previousInputs, meta) => {
    return meta.env.playersControlled.map(player => ({
        input: { raise: holding },
        player,
    }))
}

ℹ️Noteℹ️

In the default boilerplate, the poll function resides in input.ts

By default, any inputs we don't control are rejected at multiple levels (the game engine, the netcode, even the backing Gamewall service), but it's still best practice to loop through meta.env.playersControlled so that we only give inputs to the players we're expected to control.

If we do not need access to the previous inputs, or any other player-specific detail, we can also set the player parameter of the returned value to null. This broadcasts the same input to all controlled players, cutting out an unnecessary loop:

typescript
export const poll: GameTypes['poll'] = (_previousInputs, meta) => {
    return [{
        input: { raise: holding },
        player: null,
    }]
}

Whether or not this is useful depends on the game's nature and architecture. For the simple bouncy ball demo, this does make things easier, but if we would like to run filtering functions on inputs, it can be better to stick to looping through meta.env.playersControlled.

Manual Sending

To send manual inputs, we need to capture the game handle when we register the game:

typescript
const handle = gamewall.register({
    // game method definitions here
})


document.body.addEventListener('click', () => {
    handle.sendInput({ raise: true }, 0)
})

ℹ️Noteℹ️

In the default boilerplate, gamewall.register() is called in index.ts

Where handle.sendInput() has the following signature:

typescript
type sendInput = (input: Input, player: number | null, options?: {
    tick?: number,
    delay?: number
}) => void

ℹ️Noteℹ️

The player parameter in handle.sendInput() is nullable. Sending null is equivalent to sending a polled input to null, the engine will broadcast the input to all players controlled by the current instance.

This allows us to manually control the tick and delay parameters, which the engine normally takes control of. The function of these is to set the input timing:

  • tick sets the absolute tick for the input (allowed to be fractional)

  • delay sets a millisecond delay on the input (allowed to be negative)

For example, since using a single tick action would be infeasible for our bouncy ball, we can manually elongate it:

typescript
document.body.addEventListener('click', () => {
    const tick = handle.currentFractionalTick
    for (let i = 0; i < 10; i++) {
        handle.sendInput({ raise: true }, null, { tick: tick + i })
    }
})

ℹ️Noteℹ️

We can use both handle.currentTick and handle.currentFractionalTick to request timings, but there is a subtle difference between the two: currentFractionalTick always follows the clock at the highest resolution available, while currentTick reflects the current status of the game calculation. In edge cases, these might desynchronize briefly. Inputs are submitted according to currentFractionalTick by default.

When using sendInput, please keep in mind that the engine is only able to process the first input for each tick. Subsequent inputs sent at overlapping ticks will be silently ignored. If your game uses manual sending and necessitates each input to be manually sent, we recommend specifying both an exact tick number and a delay of 0, as well as keeping track of the next empty tick.

When neither tick nor delay is specified, the engine automatically sets tick to currentFractionalTick and sets the delay according to the input delay setting of the environment. With polling, tick is set directly to the polled tick's number, while the input delay setting is automatically used.

Final input tick numbers are calculated in-engine and preserved through the netcode. An input sent to a final tick of 42 on a user's device is going to be sent to tick 42 on the big screen or screens and all other devices as well.

Input Anticipation

It is typical for a Gamewall game to not have access to the true inputs of the final few ticks at any given time. For this reason, the Gamewall Engine requires the developer to specify an anticipate function in the following format:

typescript
type anticipate = (input: Input, state: State, meta: {
    game: Metadata,
    env: GamewallEnvironment,
    seed: number,
    random: PRNG,
}) => Input

ℹ️Noteℹ️

In the default boilerplate, the anticipate function resides in input.ts

The purpose of anticipate is to generate an approximation of an as-yet-unknown player input. This anticipated input will then be used in place of the actual input in running loop, then, if the true input is ever received, the game state will be rewound to the appropriate time and loop will be reran with the correct inputs.

The simplest two input anticipation strategies are to either:

  • presume no input, or

  • presume a continuation of the last input

Which of these makes more sense varies on a case-by-case basis. Typically, continuous inputs such as directional joysticks play well with a continuation pattern, while with instantaneous actions it's generally better to assume no input. But it is also important to take into account the game's input pattern: sparse inputs with sendInput will behave very differently from a dense input stream of polling where anticipation merely fills the gaps.

In the bouncy ball example, raise is a continuous input, so if we use polling, the best implementation of anticipate might look like this:

typescript
export const anticipate: GameTypes['anticipate'] = (previous) => {
    return { raise: previous.raise }
}

In this case, if we miss an input, we'll simply presume the user kept doing what they were doing before: if they were raising the ball, we assume they kept raising it, while if they let it fall, we assume they haven't pressed the button again. This will be corrected when (and if) we receive the true input for any given tick.

On the other hand, using the above anticipation function with the sparse input would result in a permanently stuck input. In that case, we need to switch to this implementation:

typescript
export const anticipate: GameTypes['anticipate'] = (previous) => {
    return { raise: false }
}

Which, at any point when we miss an input, presumes the user let go of the button. Since in the sparse input example we only get the inputs where the user doesn't press the button, this is not only ideal, it is necessary.

Please note that the anticipate function does have access to the game state of the most recent tick at the time of the input, and is consequently recalculated each time the engine backtracks. This allows for richer, context-aware input anticipation mechanisms, but it also requires this function to be similarly deterministic as loop.


In theory, by this point in the tutorial, whichever path we took for the inputs, we should already have a fully complete simulation. In practice though, we cannot see any of it yet, so next we need to set up our render functionality.

Made by Mondriaan with ❤️