Skip to content

Setting Up the Environment

The Gamewall Engine executes games in a sandbox, assigning and enforcing a specific order of lifecycle events:

  1. The sandbox kernel boots up, loads the assets, and prepares the environment

  2. The kernel executes the game package. The game is expected to call gamewall.register() within 1000ms.

  3. Once registered, the engine calls the setup function of the game exactly once, passing information about the environment. The game is given an opportunity to preprocess Metadata at this step. Setup is considered complete at this point.

  4. After setup is complete, the engine can run an arbitrary number of rounds, each of which entails:

    • A call to initialize at first, to compute the starting game state

    • Repeated calls to loop at a fixed tickRate (including the reruns the backtrack mechanism necessitates), generating a continuous stream of game states

    • Repeated call to poll, synchronized with the tickRate, to collect input states, which are then both sent to the game loop and shared over the netcode

    • Potential repeated calls to anticipate to generate anticipated versions of inputs which have not been received yet

    Only one round can run at once. However, timing is controlled entirely by the game engine, it may stop and resume calling loop at any time. It may also backtrack and rerun loop for past ticks with different inputs at any time, so to avoid side effects, all data relating to the state of a game round must be contained within State.

  5. Parallel to the game loop, the engine also starts a render loop, calling render at the frame interval of the device. This will often not line up with the tickRate, so render is given two ticks at all times, as well as a ratio between them to guide interpolation.

Importantly, this applies to both screens and controllers. The game code first receives information about which role it is expected to act in in setup.

gamewall sandbox environment flowchart

The setup function takes the following signature:

typescript
type setup = (meta: {
    game: Metadata
    env: GamewallEnvironment
    renderOnDemand: RenderOnDemand
}) => Metadata

meta.game and meta.env are two values that are present in nearly all function signatures. We will discuss those in the rest of this document. meta.renderOnDemand will be discussed in the section about rendering.

The GamewallEnvironment Type

GamewallEnvironment, commonly passed as meta.env, is controlled entirely by the engine, and contains important information about how the game instance is expected to act. Its signature is:

typescript
interface GamewallEnvironment {
    role: string
    playerCount: number
    playersControlled: number[]
    seed: string
    inputDelay: number
    autoInputDelay: boolean
    container: Element
}

Where

  • role is the screen the environment wishes to display. Its value is guaranteed to be one of the values of screensAvailable in the manifest.
  • playerCount is the number of players currently in the game. The number of inputs presented to the game at any given time will match this value. This cannot change within a round, but might change in-between rounds.
  • playersControlled is a list of player indices controlled by the current game instance.
  • seed is the current root seed of the engine. This value changes between every round.
  • inputDelay is the currently effective artificial delay on inputs. This is applied in-engine on polled inputs automatically, and on manually sent inputs too unless explicitly overridden. This is usually inconsequential for the game code but provided just in case.
  • autoInputDelay indicates whether the automatic input delay adjustment mechanic is enabled. If this is true, the input delay will be periodically adjusted in relation to a moving average of ping.
  • container is a reference to the HTML element the game is expected to build its interface inside. This is usually, but not guaranteed to be, equal to document.body.

For the purposes of setup the most important fields tend to be role and container. playerCount usually matters a lot to initialize, while input polling can often make good use of playersControlled.

Typings

Each Gamewall game is characterized by three types: State, Input, and Metadata:

  • State includes everything concerning the game state. This is created from scratch for every new round by the initialize function, then continuously stepped through loop as long as the round is active. This should contain all data necessary to represent a discrete moment in the game.
  • Input is the type/shape of inputs submittable to the game, either through polling or manual sending. These should represent the game controller state of the given user in a format which makes sense for the game.
  • Metadata is an object containing optionally configurable data fields regarding the game. This is provided externally and remains the same for the entire duration of a game instance's lifetime.

Setting up some example types

By default, if a game is generated with the command-line utility, these types are found in types.ts. For our bouncy ball demo, we're going to specify a game state of:

typescript
export interface GameState extends GamewallGameState {
    altitude: number // how high the ball is right now
    vario: number // how fast the ball is moving up or down
}

This data structure can accurately represent and reproduce any given moment in our game (which, in this case, consists of a ball which can move up or down). While the bouncy ball demo simplifies this to an extreme, this representation can often be surprisingly compact.

Our input scheme is going to be simple: the user can lift the ball by tapping and holding the screen. This can be encoded in a very simple, game-specific format:

typescript
export interface PlayerInput extends GamewalllGameInput {
    raise: boolean // if the player is lifting the ball up
}

We're going to produce and receive an input in this shape each tick, and this object allows us to convey the exact contribution of a given user to the game. This is also what's going to be primarily transported over the network, so ideally we'd like to keep this as small as feasible.

We'll specify some metadata fields to allow for changing the behavior of our game:

typescript
export interface GameMetadata extends GamewallGameMetadata {
    gravity: number // how much speed gravity adds to the ball per tick
    bounciness: number // how much speed the ball retains on a bounce
    raiseSpeed: number // how fast the player can move the ball up
}

There are two main purposes of defining constants here: these can later be marked as configurable in the manifest, and they make maintenance easy as well. For example, if we were to change our gravity setting even in a more complicated game, by using meta.game.gravity everywhere it is calculated, it would be easy to change the gravity of a game in a consistent way with a low surface for bugs. Thus, it is recommended to keep constants here as much as possible.

In our example, all three values we have defined here have been marked as configurable in the manifest in the previous chapter. This allows screen operators to fine-tune the behavior of the game to their liking.

Exporting and using types

With these types set up, it is recommended to export a GameTypes type, and optionally a GameClass as well:

typescript
export type GameTypes = GamewallGameTypes<
    GameState,
    PlayerInput,
    GameMetadata
>
export type GameClass = GameTypes['game']

ℹ️Noteℹ️

In the default boilerplate, the above type declarations reside in types.ts

GameTypes is a helper type which can greatly simplify the typing of Gamewall's fundamental function signatures, and allow for easy and hassle-free typing. We commonly use it through a property accessor: for example, the loop and render function can be found under GameTypes['loop'] and GameTypes['render'] and is commonly used in one of these two ways:

typescript
// for functional-style games (like the default boilerplate)
export const loop: GameTypes['loop'] = (state) => {}
export const render: GameTypes['render'] = (state) => {}
typescript
// for class-style games
class MyGame implements GameClass {
    loop: GameTypes['loop'] = (state) => {}
    render: GameTypes['render'] = function (this: MyGame, setup) {}
}

ℹ️Noteℹ️

In the class-style example, we used an arrow function for loop. This is recommended because loop should always be a pure function, and in the vast majority of use cases everything it needs to reference can be found in its meta argument. render, on the other hand, will usually use stateful class properties, such as a canvas rendering context saved into a private property, so a regular function-style signature with an explicitly typed this is recommended.

GameClass, on the other hand, is only necessary if we use a class-style game. It is provided by GameTypes and it is usually broken out for a simpler, more canonical implements declaration, but it is also unused in the default boilerplate. The Gamewall Engine runs each game instance in its own sandbox which already provides (and enforces) per-game encapsulation, so the benefits of using a class on this particular level are minimal. However, the option is provided just in case.

GameTypes also provides shorthands for State, Input, and Metadata through its 'state', 'input', and 'metadata' properties. See the Gamewall API Reference for the full list of options.

Defaults

For two of the three game-specific types, Input and Metadata, the Gamewall Engine requires the developer to export a default state. For inputs, this will be used in edge cases such as the initial inputs in the first ticks, and metadata configuration allows for partial changes at every step of the way.

typescript
export const defaultMetadata: GameTypes['metadata'] = {
    gravity: 0.2,
    bounciness: 0.8,
    raiseSpeed: 5
}

export const defaultInput: GameTypes['input'] = {
    raise: false
}

ℹ️Noteℹ️

In the default boilerplate, defaultMetadata and defaultInput reside in defaults.ts

For the third type, State, there is no default, instead the engine calls initialize at the start of each round to compute a starting game state. This will be discussed in detail in the chapter focusing on the game loop.

Registering The Game

The Gamewall Engine expects games to call gamewall.register() near-immediately. This is one of the very few times a game needs to initiate a call to the engine:

typescript
gamewall.register({
    setup,
    initialize,
    loop,
    anticipate,
    render,
    poll,
    defaults: defaultMetadata,
    defaultInput
})

The expected type of this call is GamewallGamePack, which is compatible with GameClass, and it defines the interface the engine requests from the game. When using the class-based style, this can be replaced with:

typescript
gamewall.register(new MyGame())

If needed, this function returns a handle (GamewallGameHandle) which provides access to on-demand rendering, manual input sending, and allows the script to read certain variables of the game instance whenever needed:

typescript
const handle = gamewall.register({
    // ...
})


window.addEventListener('mousedown', () => {
    handle.env.playersControlled.map(player => {
        handle.sendInput({ raise: true }, player)
    })
})

ℹ️Noteℹ️

handle.env and handle.meta are getters which always return fresh data. The environment can change between rounds, so it is recommended to use the full version of these and not cache either of these with snippets like const env = handle.env or const { playersControlled } = handle.env in any long-lived context.

The full list of options the handle can be used to access can be found in the reference.

Using the setup function

Immediately after registration, the Gamewall Engine calls setup, passing in the environment and metadata for the first time. The most important tasks to run at this step are

  • DOM injection: this is the step where we receive env.container, allowing us to inject the app's content into the correct spot in the DOM.

  • Differential setup for env.role values: we can define and initialize different renderers for the same game state and create custom controllers.

For simplicity, we're going to use a <canvas> element for both the game and the controller. We're going to set this one up beforehand and just place it into the right spot in the DOM when setup is called:

typescript
export const canvas = document.createElement('canvas')

// if we'd also like other contexts, such as a webgl renderer,
// we can add more types here and decide when setup() runs
let context: CanvasRenderingContext2D | null
export const getContext = () => context // for render.ts

export const setup: GameTypes['setup'] = (meta) => {
    // create the context (we can add logic here based on meta.env.role)
    context = canvas.getContext('2d')

    // inject the dom
    meta.env.container.appendChild(canvas)

    // return the game metadata (we can modify it here, if needed)
    return meta.game
}

If we wanted to use custom DOM for the controller, we could simply create tags here based on the value of meta.env.role. For complicated controller setups, we can even use a web framework:

typescript
// NOTE: THIS IS NOT PART OF THE BOUNCY BALL DEMO

export const setup: GameTypes['setup'] = (meta) => {
    // ...

    if (meta.role.env === 'controller') {
        setupVue(meta.env.container)
    } else {
        context = canvas.getContext('2d')
        meta.env.container.appendChild(canvas)
    }

    // ...
}

The most common use of differential setup here is to initialize a large, feature-rich game engine specifically only on the main screen. Most game engines have excellent visual effects and simply synchronizing Gamewall's game state with them can create striking visuals while sacrificing nothing of the Gamewall Engine's gameplay consistency and ease of multiplayer development.

Made by Mondriaan with ❤️