The Sprunk Game Engine is a component-based game development framework built in TypeScript.
It features a well-structured architecture that separates concerns through a GameObject hierarchy with attachable behaviors, global engine components, and a robust event and dependency injection system.
The engine architecture is built on three key elements:
The engine follows a clear separation of concerns through its architecture, where:
The GameEngineWindow
class serves as the entry point and global container for the game engine. It:
The GameObject
class represents an entity in the game world. It:
Behaviors provide functionality to GameObjects. The engine defines a clear hierarchy of behavior types:
The abstract Behavior
class defines the base structure for all behaviors. It:
The engine implements a clear separation of concerns through specialized behavior types:
InputBehavior
handles input and passes it to logic behaviors. It:
LogicBehavior
contains the game logic and state. It:
OutputBehavior
handles rendering, audio, and other outputs. It:
This separation creates a unidirectional data flow: Input → Logic → Output.
For more information, see Behaviors.
GameEngineComponent
classes provide engine-wide systems that are not tied to specific GameObjects. They:
@InjectGlobal
abstract class GameEngineComponent {
protected attachedEngine: GameEngineWindow | null;
onAttachedTo(gameEngine: GameEngineWindow): void;
onDetached(): void;
}
See Game Window Components for more information.
The engine implements several design patterns to solve common problems:
The engine uses the Observer pattern extensively through the Event<T>
class. This allows:
class Event<T> {
addObserver(observer: (data: T) => void): void;
removeObserver(observer: (data: T) => void): void;
emit(data: T): void;
}
For more information, see Events.
The engine uses dependency injection to manage dependencies and promote loose coupling:
@Inject
and @InjectGlobal
) simplify dependency resolutionclass DependencyContainer {
register<T>(token: Token<T>, instance: T): void;
resolve<T>(token: Token<T>): T;
}
The dependency injection (when using GameObjects) provides:
@Inject
and @InjectGlobal
)function Inject(token: Token<any>, recursive?: boolean): PropertyDecorator;
function InjectGlobal(token: any): PropertyDecorator;
This system allows behaviors to access services without tight coupling to specific implementations.
Usage example:
class KeyboardInputBehavior extends DeviceInputBehavior {
@Inject(PlayerLogicBehavior)
private _logic: PlayerLogicBehavior;
protected onMouseLeftClickDown(): void {
this._logic.jump();
}
}
The entire engine is based on the Component pattern:
The engine uses facades to simplify complex subsystems:
Sprunk
class provides a simplified interface for engine initializationRenderGameEngineComponent
acts as a facade for WebGPU functionalityclass Sprunk {
static newGame(canvasToDrawOn: HTMLCanvasElement | null, debugMode?: boolean, componentsToEnable?: ComponentName[]): GameEngineWindow;
}
The engine uses factory methods to create complex objects:
Sprunk.newGame()
creates a configured GameEngineWindowObjLoader.load()
creates MeshData from OBJ filesAll frequent math tools (like Vector2, Vector3, Quaternion, etc.) also have factory methods to create them:
Vector3.zero()
or Vector3.up()
Quaternion.fromEulerAngles(0, 0, 90)
or Quaternion.identity()