-
Notifications
You must be signed in to change notification settings - Fork 0
Technology
Terra makes use of a large collection of various tech in order to function. This page attempts to document most aspects of the project. In order to help potential contributors find a niche, it may be overly-verbose or use a lot of tech buzzwords. If you're looking for an area you're knowledgeable in to contribute - or even if you're just interested in the design - this is the place for you.
Terra is built in TypeScript. The code is transpiled and minified into static JavaScript bundles, which can then be served via any file host. This project has several custom GitHub Actions, which run whenever code is updated or released. These actions rebuild the bundled site files, and push them to the live website branch to be served via GitHub Pages. This process ensures that any released code is available to the end-users almost immediately.
Terra starts by defining a Campaign
. This is simply a named "container", which serves to help hosts separate multiple groups of boards/entities/etc.
In Terra, a Board
represents two separate layers, containing Terrain and Entities. The users create separate boards for each area/zone/scenario they want to display to their users.
Terrain
in Terra are simply flat, non-animated tiles on an (x,y,z) grid. These are the background images used to visually display the world.
Entities
are possibly-animated, and rendered on a separate layer from Terrain. They contain multiple properties, such as names and ownership status.
The UI can be split into two separate components - the standard HTML interface, and the Canvas (WebGL) interface. The HTML-based interface is built using React. This interface is responsible for handling most user input - including setting up new connections, creating new Entities, and adding Terrain. It primarily makes use of the Material-UI library for all of its components.
The Canvas UI is separate from the React DOM tree, and manages more game-specific rendering. The maps Terra produce can be large enough that it is prohibitively difficult to render enough DOM elements to fully cover one board - especially with zooming/panning support in some cases enabling massive sections of the board to be visible at once. Thus, the game logic handles this itself via its own elements. The game controller injects its own DOM nodes into the document root, which are controlled outside of the React tree.
All rendering done for Terra's game board is handled via WebGL
. To help abstract this into a simple-to-use 2D layered plane, we use the PIXI.js
library.
All Sprites are loaded from a single spritesheet, which is decoded and kept on a Canvas in memory. For those familiar with 3D graphics, think of this sheet instead as a Texture Atlas. This atlas comes in two pieces (the image itself and a JSON data file) and is custom-generated in order to provide additional metadata about each image that other existing tools do not. Fitting all sprites onto one sheet speeds up startup time, as only one single HTTP request is needed before the game is ready to render any board. This single-sheet-in-memory approach is also extremely fast at creating new textures on the fly.
The spritesheet is also pre-baked with data about each sprite's transparency. Whenever a new tile of Terrain is drawn, this data is used to determine if any tiles beneath the new tile are completely hidden, and thus can be automatically deleted. In effect, small terrain like flowers or torches will simply overlay on top of any background tiles, while full-size tiles such as dirt or walls will always automatically cull the tiles beneath them for the user.
In WebGL's context, all sprites are loaded only once. Subsequent calls to generate duplicate GL Textures will always result in reuse, and the textures are automatically cleaned from memory when nothing on the board is using them any longer. In order to avoid blocking the DOM, all loading and display logic is built to be "eventually consistent", and all calls to add/remove renderer things are generally asynchronous and self-managing in order to avoid race conditions. For example, calls to load or unload textures are async, but preform pre-checks to work even if one is still pending for the same resource.
All Entities are placed onto a separate layer. Entities support animated sprites, but are far fewer in number than the tiles that form the Terrain beneath them, so there are fewer optimizations required for their layer.
- In the future it would be nice to switch all sprite subimage copying to use an OffscreenCanvas & WebWorker, which should offer additional - though admittedly negligible - performance benefits. Currently this is not feasible with the lack of support in modern browsers.
Connections in Terra are handled via WebRTC, wrapped by my library Switchboard.js. When you connect to a host or a client, all data is sent directly to them - with no server in-between. This connection is established using one of several public matchmaking servers, and cryptographic IDs the Host and Clients generate. Once connected, the Host handles validating clients against known past users that were allowed into the lobby.
All data is transmitted in the form of zlib-compressed Protocol Buffers. These are extremely fast to pack and unpack (rivaling even JS's JSON optimizations), and have the benefit of being extremely tiny - which allows users with even limited bandwidth to use Terra without issue. This encoding and compression is handled outside the main thread, inside a pair of Web Workers. Because of this arrangement, many active connections can be managed concurrently in the background with minimal impact to the Host's network and UI responsiveness.
Terra uses multiple redundant matchmaking servers, all hosted publicly and typically used for other heavier applications. Connections are established to other users via a secure ID they generate. During the initial WebRTC connection phase each client must generate ICE Candidates and share them. The server facilitates this process until both clients are able to connect to each other, after which point the server is not used for any further communication.
All of the peer-to-peer connection and management logic is abstracted away and handled by one of my other libraries, Switchboard.js.
Terra does not rely on a server to store its data. Instead, all data is saved locally into the browser's IndexedDB
- which is an indexed, transactional Object Store. This is abstracted somewhat by using the library Dexie.js
, which provides nifty tools for easy migrations and Promise wrappers.
Currently, Boards are saved as compressed UInt8Array buffers, which save a dramatic amount of disk space - at the cost of having to re-encode the board whenever it is changed. This encoding+compressing is handled asynchronously in Web Workers, so the impact on the user is minimal. Tests with storing each board as standard DB objects have proven less performant than desired, due to the vast number of tiles each board may store.
All connection security is now handled by Switchboard.js, but this is a brief summary of how connections are validated. This may be slightly out of date as Switchboard progresses.
When the user loads Terra for the first time, the game uses an Elliptic Curve Algorithm (P-521) to generate a unique public & private key pair. Additionally, this public key is hashed using SHA-512. The hash of this key is stored as this browser's User ID
.
Whenever two users connect, they initiate a series of steps to complete the handshake protocol. The first step of this sequence involves sharing their own Public keys, as well as their User ID
. The user signs their packet containing this information using their private key before sending the packet.
When each side receives the other's authentication packet, it first validates the ID. By hashing the received public key, it is able to verify that this user's ID matches the ID they expect to be connected to. Finally, in order to validate the other side is really who they claim, the packet's signature is then validated using the confirmed public key.
This process allows users to host games and invite others simply by sharing their Host ID. This can be appended to the window as its hash, which will automatically connect the client to the Host.