Game state and user actions. ARTris — Part #5
This article is part #5 of a series of articles on ARTris. A multiplayer real-time 3D AR Tetris game built with ARKit, Node.js, and Firebase. If you’re new to this series, we recommend you begin with the introduction. Fork the iOS Client and the Game Engine repos to start experimenting.
Today we dive into the components that make up our game-engine, what happens when a player takes an action, and how we calculate the next state for each game session.
Bird’s eye view of the engine
Board, Block, Engine, and Gesture are the four building blocks of our game-engine.
- Block is the falling block that a player can move or rotate by interacting with the iOS client. A Block knows about its position in the game coordinate system, and it can check for collisions.
- When a Block falls, it becomes part of the Board. The Board is just a coordinate space where we keep track of the fallen Blocks.
- Engine is responsible for updating the game state by interacting with the Block. Engine is the middle man between the Block and the Gesture components.
- Gesture subscribes to the Firebase events for move and rotation actions of each player and communicates the player’s intentions to the Engine.
Engine and Gesture coordinate the events in the game-engine and are responsible for communicating the next state to Firebase. The main-loop of our game-engine is in Engine (no surprises here).
In the rest of this article, we focus on the relationship between the Gesture and the Engine.
Engine
run
is the main-loop of the game-engine where we update the game state and dispatch the pending actions.
In run
, we invoke the processQueue
function which iterates through the player actions and updates the orientation of the Block while enforcing the game constraints.
In the above snippet, we pass a callback function to move
. After the position of the block is updated the callback function is fired which in turn emits the new game-state to the Firebase engine. We will cover the newEngineState event when we cover the index
file later in this article.
processQueue
function is a large switch statement that depending on the action type invokes either move or rotation function on the block. It also calculates the parameters based on the action direction (← → ↑ ↓).
Gesture
The gestureListner
module implements the Pub/Sub pattern by emitting events that the game cares about to all subscribed listeners. this implementation requires all subscriber to pass the event channel they are interested in and an asynchronous callback method to execute when the event is published.
The player actions are added under game-session/:game-session-id/:player-id/gestures
in Firebase realtime database. Under gesture
node are the action types move
and rotate
where we push the action’s direction.
When initializing the game engine, we pass in the path to the gesture
node of each player to the gesture listener module.
This module listens for new player actions added under move
and rotate
nodes of each player and emits them to a new channel. We later subscribe to the this channel to push the player actions to the processQueue
. By doing so, Guesture acts as a mediator and decouples Engine from Firebase.
Finally when we create a new game session we subscribe to the player actions we are interested in and push them into the gestureQueue
.
To recap, when the game engine starts, we subscribe to Firebase to receive the new game sessions. As part of initializing the game session, we create a gesture listener object that subscribes to Firebase for all possible player actions. It emits each action to a different channel. We then subscribe to the channels we are interested in to push the player actions to gestureQueue.
The main-loop in the engine runs in a fixed interval and in each execution, it processes all the player actions to create the next state. This new state is then emitted, processed and sent to Firebase.
We know part 5 kind of came late.
Thanks for sticking with us ❤️