Setting Up the Environment
The Gamewall Engine executes games in a sandbox, assigning and enforcing a specific order of lifecycle events:
The sandbox kernel boots up, loads the assets, and prepares the environment
The kernel executes the game package. The game is expected to call
gamewall.register()
within 1000ms.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 preprocessMetadata
at this step. Setup is considered complete at this point.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 stateRepeated calls to
loop
at a fixed tickRate (including the reruns the backtrack mechanism necessitates), generating a continuous stream of game statesRepeated call to
poll
, synchronized with the tickRate, to collect input states, which are then both sent to the game loop and shared over the netcodePotential 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 rerunloop
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 withinState
.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, sorender
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
.
The setup
function takes the following signature:
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:
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 ofscreensAvailable
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 todocument.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 theinitialize
function, then continuously stepped throughloop
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:
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:
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:
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:
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:
// for functional-style games (like the default boilerplate)
export const loop: GameTypes['loop'] = (state) => {}
export const render: GameTypes['render'] = (state) => {}
// 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 becauseloop
should always be a pure function, and in the vast majority of use cases everything it needs to reference can be found in itsmeta
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 typedthis
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.
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
anddefaultInput
reside indefaults.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:
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:
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:
const handle = gamewall.register({
// ...
})
window.addEventListener('mousedown', () => {
handle.env.playersControlled.map(player => {
handle.sendInput({ raise: true }, player)
})
})
ℹ️Noteℹ️
handle.env
andhandle.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 likeconst env = handle.env
orconst { 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:
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:
// 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.