Game Engine Architecture
Relevant source files
The following files were used as context for generating this wiki page:
The Farm Siege project is built upon a custom 2D game engine written in TypeScript. This engine uses the HTML5 Canvas for rendering and employs a declarative, component-based architecture for structuring game elements, which is noted to be strongly inspired by Flutter syntax. The engine code is located in the /lib directory, while the game-specific logic and components reside in /src.
This architecture separates the core rendering loop, event handling, and component lifecycle management from the actual game logic. The engine is initiated by a single entry point which sets up the main canvas, instantiates a root component (Game), and starts the continuous render loop.
Sources: README.md, src/main.ts, lib/Canvas.ts
Core Concepts
The engine is built around a few key classes and concepts that work together to create the game experience.
| Class/Concept | Description | Source File |
|---|---|---|
Canvas | The main engine controller. Manages the <canvas> element, the render loop, and top-level event propagation. | lib/Canvas.ts |
Component | The basic building block for all visible and logical elements in the game. | lib/Component.ts |
Store | A state management pattern used to hold and update game state, accessible by components. | src/Game.ts:151-162 |
RenderingContext | An object passed during the render cycle, containing the canvas 2D context and other rendering data. | lib/RenderingContext.ts |
Engine Initialization
The entire game is bootstrapped in src/main.ts. A Main class instantiates the Canvas class, which acts as the engine's core.
The Canvas constructor requires a configuration object specifying the HTML canvas element, the grid dimensions for scaling, and the root component to render.
// src/main.ts:6-15
class Main {
public constructor() {
// Initialize the game
new Canvas({
el: <HTMLCanvasElement>document.getElementById('canvas'),
grid: new Dimensions(1600, 1200),
// Use the custom root component
root: new Game(),
showFPS: true
});
}
}
The initialization flow is as follows:
This diagram illustrates the startup sequence from the HTML entry point to the beginning of the game loop.
Sources: src/main.ts:6-15, lib/Canvas.ts:24-60
The Game Loop and Rendering
The engine's heart is the render method within the Canvas class. It is called continuously using window.requestAnimationFrame to ensure smooth animation tied to the browser's refresh rate.
On each frame, the game loop performs these steps:
- Calculates the time elapsed since the last frame (
timeDifference). - Clears the entire canvas.
- Calls the
rendermethod of the root component (Game), passing a newRenderingContext. - The root component then recursively calls the
rendermethod of its children, propagating the context. - Optionally, it renders an FPS counter.
This sequence diagram shows the flow of a single frame update within the game loop.
The RenderingContext is a crucial object passed down the component tree during rendering. It holds the canvas 2D context, scale factor, time difference, and other data necessary for components to draw themselves correctly.
Sources: lib/Canvas.ts:95-121
Component-Based Architecture
All game elements, from UI screens to individual characters and effects, are Component instances. This provides a standardized structure for game logic and rendering.
Component Class
The Component class is an abstract class that defines the structure for all other components.
This diagram shows the key properties and methods of the base Component class.
Sources: lib/Component.ts
Component Lifecycle
Components have two primary lifecycle methods that can be overridden:
onInit(): Called once after the component is initialized. This is where initial setup, such as creating input maps or subscribing to stores, occurs.onTick(timeDifference): Called on every frame for components that need to update their state over time, such as for animations or physics. TheGamecomponent'sonTickmethod is the entry point for these updates.
Sources: lib/Component.ts:28-31, src/Game.ts:91-125, src/components/CharacterContainer.ts:65-197
The Component Template
A component's visual structure is defined by its template property. The template is an array of objects, where each object defines a child component, its position, and its properties (props). This declarative approach, inspired by frameworks like Flutter and React, allows for a clear definition of a component's composition.
// src/Game.ts:127-131
protected template: Template = [
{
component: new Background(),
position: (): Coordinates => new Coordinates(0, 0)
},
// ... more child components
];
The render method of a component iterates through its template, conditionally showing and rendering each child component.
Sources: lib/Component.ts:108-154, src/Game.ts:127-183
State Management with Stores
Game state is managed centrally using a "Store" pattern. The main Game component initializes and holds references to all stores.
This diagram shows how the Game component manages stores and how other components access them to read and update state.
Components can access these stores via this.stores to retrieve data or call methods that modify the state. For example, CharacterContainer accesses this.stores.character to move the player and this.stores.score to add points.
Sources: src/Game.ts:151-162, src/components/CharacterContainer.ts:66-73
Event Handling
User input events (keyboard and mouse) are captured by the Canvas class, which adds event listeners to the window and the <canvas> element.
When an event is fired, the Canvas instance calls the propagateEvent method on the root component. This method then recursively traverses the component tree, checking if the event's coordinates fall within the bounds of any child component. This allows components to handle events that occur within their specific area on the screen.
This flowchart details the propagation of a user input event from the DOM to the relevant component.
Sources: lib/Canvas.ts:40-56, lib/Component.ts:156-200