-
Notifications
You must be signed in to change notification settings - Fork 1
Creating a Game Module
SparkEngine uses a dynamic module system similar to Unreal Engine and Unity. Your game logic is compiled as a shared library (DLL on Windows, .so on Linux) that the engine loads at runtime. This architecture enables hot-reload during development, clean separation of engine and game code, and the ability to load multiple modules simultaneously.
Source: SparkSDK/Include/Spark/SparkSDK.h, cmake/SparkGameModule.cmake, cmake/SparkEnginePreflight.cmake
The fastest way to start is using the EmptyProject template:
cp -r Templates/EmptyProject MyGameThis gives you:
MyGame/
├── CMakeLists.txt # Build configuration
├── Source/
│ ├── GameModule.h # Module class (IModule implementation)
│ └── GameModule.cpp # Module factory exports
├── spark.project.json # Project metadata
└── spark.modules.json # Module manifest
Every game module implements Spark::IModule from <Spark/SparkSDK.h>. This is the contract between the engine and your game code.
class IModule
{
public:
virtual ~IModule() = default;
// Return metadata about your module
virtual ModuleInfo GetModuleInfo() const = 0;
// Called once when the DLL is loaded
virtual bool OnLoad(IEngineContext* context) = 0;
// Called before the DLL is unloaded
virtual void OnUnload() = 0;
// Called every frame
virtual void OnUpdate(float deltaTime) = 0;
// Called every frame after OnUpdate (optional)
virtual void OnRender() {}
// Called when the window is resized (optional)
virtual void OnResize(int width, int height) {}
};The ModuleInfo struct provides metadata about your module:
| Field | Type | Description |
|---|---|---|
name |
const char* |
Human-readable module name |
version |
const char* |
Semantic version string (e.g. "1.0.0") |
sdkVersion |
uint32_t |
SDK version this module was built against (SPARK_SDK_VERSION) |
loadOrder |
int |
Initialization priority (lower = earlier, default 1000) |
| Method | Required | When Called | Description |
|---|---|---|---|
GetModuleInfo() |
Yes | After DLL load | Return module name, version, SDK version, load order |
OnLoad() |
Yes | After CreateModule()
|
Initialize the module; return false to abort loading |
OnUnload() |
Yes | Before DestroyModule()
|
Clean up resources, unsubscribe events |
OnUpdate() |
Yes | Every frame | Main game logic tick |
OnRender() |
No | Every frame after update | Custom rendering (HUD, debug overlays) |
OnResize() |
No | Window resize | Handle viewport changes |
#include <Spark/SparkSDK.h>
class MyGameModule : public Spark::IModule
{
public:
Spark::ModuleInfo GetModuleInfo() const override
{
Spark::ModuleInfo info{};
info.name = "MyGame";
info.version = "0.1.0";
info.sdkVersion = SPARK_SDK_VERSION;
info.loadOrder = 1000; // lower values load first
return info;
}
bool OnLoad(Spark::IEngineContext* context) override
{
m_context = context;
// Access engine systems through the context
m_graphics = context->GetGraphics();
m_input = context->GetInput();
m_timer = context->GetTimer();
m_events = context->GetEventBus();
// Subscribe to engine events
m_collisionSub = m_events->Subscribe<Spark::CollisionEvent>(
[this](const Spark::CollisionEvent& e) {
HandleCollision(e);
});
m_sceneSub = m_events->Subscribe<Spark::SceneLoadedEvent>(
[this](const Spark::SceneLoadedEvent& e) {
OnSceneLoaded(e.sceneName);
});
return true; // return false to abort loading
}
void OnUnload() override
{
// Clean up event subscriptions
m_events->Unsubscribe<Spark::CollisionEvent>(m_collisionSub);
m_events->Unsubscribe<Spark::SceneLoadedEvent>(m_sceneSub);
m_context = nullptr;
}
void OnUpdate(float deltaTime) override
{
// Game logic here
UpdatePlayer(deltaTime);
UpdateEnemies(deltaTime);
}
void OnRender() override
{
// Custom rendering (HUD, debug overlays)
DrawHUD();
}
void OnResize(int width, int height) override
{
m_viewportWidth = width;
m_viewportHeight = height;
}
private:
void HandleCollision(const Spark::CollisionEvent& e) { /* ... */ }
void OnSceneLoaded(const std::string& name) { /* ... */ }
void UpdatePlayer(float dt) { /* ... */ }
void UpdateEnemies(float dt) { /* ... */ }
void DrawHUD() { /* ... */ }
Spark::IEngineContext* m_context = nullptr;
GraphicsEngine* m_graphics = nullptr;
InputManager* m_input = nullptr;
Timer* m_timer = nullptr;
Spark::EventBus* m_events = nullptr;
Spark::SubscriptionID m_collisionSub = 0;
Spark::SubscriptionID m_sceneSub = 0;
int m_viewportWidth = 1280;
int m_viewportHeight = 720;
};In exactly one .cpp file, use the SPARK_IMPLEMENT_MODULE macro to generate the required DLL exports:
// GameModule.cpp
#include "GameModule.h"
#include <Spark/ModuleDllMain.h> // Emits the canonical Windows DllMain
SPARK_IMPLEMENT_MODULE(MyGameModule)This generates the following exported functions:
extern "C" SPARK_MODULE_API Spark::IModule* CreateModule();
extern "C" SPARK_MODULE_API void DestroyModule(Spark::IModule*);The engine calls CreateModule() to instantiate your module and DestroyModule() to clean it up. You should never call these functions directly.
SPARK_IMPLEMENT_MODULE expands to:
extern "C" __declspec(dllexport) Spark::IModule* CreateModule()
{
return new MyGameModule();
}
extern "C" __declspec(dllexport) void DestroyModule(Spark::IModule* mod)
{
delete mod;
}On Linux, __declspec(dllexport) is replaced with __attribute__((visibility("default"))).
If your game module lives inside the SparkEngine source tree, use the spark_add_game_module() helper from cmake/SparkGameModule.cmake:
cmake_minimum_required(VERSION 3.25)
project(MyGame LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
include(${CMAKE_SOURCE_DIR}/cmake/SparkGameModule.cmake)
file(GLOB_RECURSE GAME_SOURCES "Source/*.cpp" "Source/*.h")
spark_add_game_module(MyGame ${GAME_SOURCES})
target_include_directories(MyGame PRIVATE "Source")The spark_add_game_module(TARGET_NAME ...) function performs the following:
| Step | Action |
|---|---|
| 1 | Creates a SHARED library target from the provided sources |
| 2 | Defines SPARK_MODULE_DLL and SPARK_GAME_DLL compile definitions |
| 3 | Links against Spark::SparkEngineLib
|
| 4 | Sets up SDK include directories (SPARK_ENGINE_INCLUDE_DIR) |
| 5 | Enforces C++23 standard (cxx_std_23) |
| 6 | On MSVC, sets the runtime library to MultiThreadedDLL / MultiThreadedDebugDLL
|
For standalone projects outside the engine tree, use find_package. Including the preflight check first gives clear diagnostics if the SDK installation is incomplete:
cmake_minimum_required(VERSION 3.25)
project(MyGame LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
# Pre-flight validation (optional but recommended)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../cmake")
include(SparkEnginePreflight)
find_package(SparkEngine REQUIRED)
file(GLOB_RECURSE GAME_SOURCES "Source/*.cpp" "Source/*.h")
spark_add_game_module(MyGame ${GAME_SOURCES})
target_include_directories(MyGame PRIVATE "Source")The SparkEnginePreflight.cmake module validates that the SparkEngine SDK installation is complete before find_package runs. It checks for:
| Required File | Purpose |
|---|---|
SparkEngineConfig.cmake |
CMake package configuration |
SparkEngineTargets.cmake |
Imported target definitions |
SparkGameModule.cmake |
Game module helper function |
If any file is missing, the preflight check emits a FATAL_ERROR with the exact missing file and remediation steps, rather than letting CMake produce a cryptic error message.
Project metadata describing your game:
{
"name": "MyGame",
"version": "0.1.0",
"engineVersion": "1.0.0",
"description": "My first SparkEngine game",
"modules": ["MyGame"],
"defaultScene": "Assets/Scenes/Default.scene"
}| Field | Required | Description |
|---|---|---|
name |
Yes | Project display name |
version |
Yes | Semantic version of your game |
engineVersion |
Yes | Minimum compatible engine version |
description |
No | Human-readable project description |
modules |
Yes | Array of module names to load |
defaultScene |
No | Scene to load on startup |
Module manifest that tells the engine which DLLs to load:
{
"modules": [
{
"name": "MyGame",
"path": "MyGame.dll",
"loadOrder": 1000
}
]
}| Field | Type | Description |
|---|---|---|
name |
string |
Must match the module's GetModuleInfo().name
|
path |
string |
Relative or absolute path to the DLL/SO |
loadOrder |
int |
Initialization priority (lower = earlier) |
Place spark.modules.json next to the engine executable (build/bin/).
You can load multiple modules simultaneously. They are initialized in loadOrder and shut down in reverse order:
{
"modules": [
{ "name": "CoreGameplay", "path": "CoreGameplay.dll", "loadOrder": 100 },
{ "name": "UIModule", "path": "UIModule.dll", "loadOrder": 200 },
{ "name": "DebugTools", "path": "DebugTools.dll", "loadOrder": 9000 }
]
}The engine finds modules using three fallback mechanisms (in order):
-
Command-line argument:
SparkEngine.exe -game path/to/MyGame.dll -
Manifest file:
spark.modules.jsonnext to the executable -
Directory scan: Scans for
*Game*.dllor*Module*.dllin the executable directory
Module Discovery Flow
│
├── Check command line: -game <path>
│ └── Found? → Load specified DLL
│
├── Check for spark.modules.json
│ └── Found? → Load all listed modules in loadOrder
│
└── Scan executable directory
└── Match *Game*.dll or *Module*.dll → Load matched DLLs
Engine Startup
│
├── Load DLL/SO (platform LoadLibrary / dlopen)
├── Resolve CreateModule() symbol
├── Call CreateModule() ← allocate module instance
├── Call GetModuleInfo() ← metadata, SDK version check
├── Validate SDK version
│ └── Mismatch? → Log warning, skip module
├── Call OnLoad(context) ← initialization
│ └── Returns false? → Call DestroyModule(), skip
│
├── Main Loop ──────────────────┐
│ ├── OnUpdate(deltaTime) │ ← every frame
│ └── OnRender() │ ← every frame (if overridden)
│ └────────────────────────────┘
│
├── Call OnUnload() ← cleanup
├── Call DestroyModule() ← deallocate module instance
└── Unload DLL/SO (FreeLibrary / dlclose)
Multiple modules are loaded in loadOrder and shut down in reverse order.
Once you have the IEngineContext*, access any engine subsystem. The context uses a service locator pattern backed by a generic registry.
class IEngineContext
{
public:
virtual GraphicsEngine* GetGraphics() = 0;
virtual InputManager* GetInput() = 0;
virtual Timer* GetTimer() = 0;
virtual Spark::EventBus* GetEventBus() = 0;
virtual AudioEngine* GetAudio() = 0;
virtual PhysicsSystem* GetPhysics() = 0;
virtual Spark::Animation::AnimationSystem* GetAnimation() = 0;
virtual Spark::AI::AISystem* GetAI() = 0;
virtual Spark::NetworkManager* GetNetwork() = 0;
virtual SceneManager* GetSceneManager() = 0;
virtual AngelScriptEngine* GetScriptEngine() = 0;
virtual Spark::SaveSystem* GetSaveSystem() = 0;
virtual Spark::CoroutineScheduler* GetCoroutineScheduler()= 0;
virtual uint32_t GetEngineVersion() const = 0;
virtual uint32_t GetSDKVersion() const = 0;
virtual bool IsHeadless() const = 0;
};| Getter | Returns | Description |
|---|---|---|
GetGraphics() |
GraphicsEngine* |
DX11/Vulkan/GL rendering engine |
GetInput() |
InputManager* |
Keyboard, mouse, gamepad input |
GetTimer() |
Timer* |
Frame timing and delta time |
GetEventBus() |
EventBus* |
Publish/subscribe event system |
GetAudio() |
AudioEngine* |
XAudio2 / miniaudio spatial audio |
GetPhysics() |
PhysicsSystem* |
Jolt Physics simulation |
GetAnimation() |
AnimationSystem* |
Skeletal animation pipeline |
GetAI() |
AISystem* |
Behavior trees and NavMesh |
GetNetwork() |
NetworkManager* |
UDP multiplayer (requires ENABLE_NETWORKING) |
GetSceneManager() |
SceneManager* |
Scene hierarchy and serialization |
GetScriptEngine() |
AngelScriptEngine* |
AngelScript VM and hot-reload |
GetSaveSystem() |
SaveSystem* |
Game state serialization |
GetCoroutineScheduler() |
CoroutineScheduler* |
Coroutine-based async tasks |
IsHeadless() |
bool |
True if running without graphics (dedicated server) |
The EngineContext also supports registering custom subsystems via a generic type-based registry:
// Register a custom subsystem
context->RegisterSystem<MyCustomManager>(&myManager);
// Retrieve it later
MyCustomManager* mgr = context->GetSystem<MyCustomManager>();This uses a compile-time TypeId system that works with forward-declared types. No RTTI is required.
Game modules typically subscribe to events during OnLoad() and unsubscribe during OnUnload():
bool OnLoad(Spark::IEngineContext* context) override
{
auto* bus = context->GetEventBus();
m_damageSub = bus->Subscribe<Spark::EntityDamagedEvent>(
[this](const Spark::EntityDamagedEvent& e) {
OnEntityDamaged(e);
});
m_killSub = bus->Subscribe<Spark::EntityKilledEvent>(
[this](const Spark::EntityKilledEvent& e) {
OnEntityKilled(e);
});
m_frameSub = bus->Subscribe<Spark::FrameBeginEvent>(
[this](const Spark::FrameBeginEvent& e) {
OnFrameBegin(e.deltaTime);
});
return true;
}
void OnUnload() override
{
auto* bus = m_context->GetEventBus();
bus->Unsubscribe<Spark::EntityDamagedEvent>(m_damageSub);
bus->Unsubscribe<Spark::EntityKilledEvent>(m_killSub);
bus->Unsubscribe<Spark::FrameBeginEvent>(m_frameSub);
}See Event System for the full list of built-in event types.
Game modules can create entities and attach components using the World:
bool OnLoad(Spark::IEngineContext* context) override
{
// Get the ECS world from the scene manager
auto* sceneMgr = context->GetSceneManager();
// Create a player entity
EntityID player = world.CreateEntity("Player");
auto& tf = world.AddComponent<Transform>(player);
tf.position = {0.0f, 1.0f, 0.0f};
auto& hp = world.AddComponent<HealthComponent>(player);
hp.maxHealth = 100.0f;
hp.health = 100.0f;
auto& mesh = world.AddComponent<MeshRenderer>(player);
mesh.meshIndex = 0;
mesh.materialIndex = 0;
mesh.visible = true;
return true;
}See Entity Component System for the full component and system reference.
Common causes of module load failures:
| Symptom | Cause | Fix |
|---|---|---|
| DLL not found | Wrong path in manifest | Check spark.modules.json path |
CreateModule symbol not found |
Missing SPARK_IMPLEMENT_MODULE macro |
Add macro to exactly one .cpp file |
| SDK version mismatch | Module built with different SDK version | Rebuild module against current SDK |
OnLoad() returns false |
Initialization error in module code | Check module logs for details |
| Missing DLL dependency | Module links against absent library | Use dumpbin /dependents (Windows) or ldd (Linux) |
The engine provides console commands for module inspection:
module_list # List all loaded modules with version and load order
module_info <name> # Show detailed info for a specific module
module_reload <name># Hot-reload a module (development only)
-
Store the context pointer -- Save
IEngineContext*inOnLoad()and use it throughout your module's lifetime. The pointer remains valid untilOnUnload(). -
Clean up subscriptions -- Always unsubscribe from events in
OnUnload(). Dangling subscriptions cause crashes after the module DLL is unloaded. -
Check for null subsystems -- Some subsystems may be null in headless or minimal configurations. Always check before using:
if (auto* audio = context->GetAudio()) audio->PlaySound("startup");
-
Use
loadOrderwisely -- Modules with lowerloadOrdervalues are initialized first. If your module depends on another module's initialization, give it a higherloadOrder. -
Avoid global state -- Keep all state inside your module class. Global variables in a DLL can cause issues with hot-reload and multiple module instances.
-
Match SDK versions -- Always build your module against the same SDK version as the engine. The engine logs a warning on version mismatch.
- Entity Component System -- Work with entities and components
- Scripting with AngelScript -- Add scripted gameplay logic
- Scene Management -- Load and manage scenes
- Event System -- Subscribe to engine events
- Input System -- Handle player input
- Architecture Overview -- Engine architecture and module system
- Entity Component System -- Work with entities and components
- Scripting with AngelScript -- Add scripted gameplay logic
- Scene Management -- Load and manage scenes
- Event System -- Subscribe to engine events
- Input System -- Handle player input
- Build System and CMake Modules -- Build configuration
Published from 8fd2c5bdc5cd. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Golden Image Testing
- Utilities
- Testing
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers