Creating a Game Module¶
Step-by-step guide to building a new QS-Bridge game module for any supported engine.
Prerequisites¶
Before creating a game module, ensure you have:
- Rust toolchain (stable, with
wasm32-unknown-unknowntarget) - SpacetimeDB CLI (
spacetimecommand) - C++17 compiler (GCC 11+ or Clang 14+) for bridge plugins
- The target game's dedicated server binary (Linux)
- Access to a running SpacetimeDB instance
# Install WASM target for SpacetimeDB modules
rustup target add wasm32-unknown-unknown
# Verify SpacetimeDB CLI
spacetime version
Step 1 — Scaffold the STDB Module¶
Create a new Rust crate for your game's SpacetimeDB module:
Cargo.toml:
[package]
name = "my-game-module"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = "1.1"
qs-bridge-module-sdk = { path = "../qs-bridge-module-sdk" }
log = "0.4"
Step 2 — Implement the GameModule Trait¶
src/lib.rs:
use qs_bridge_module_sdk::prelude::*;
use spacetimedb::{spacetimedb, ReducerContext, Table, Timestamp};
// ---------- Tables ----------
#[spacetimedb::table(name = player_state, public)]
pub struct PlayerState {
#[primary_key]
pub player_id: String,
pub server_id: String,
pub x: f32,
pub y: f32,
pub z: f32,
pub health: f32,
pub last_update: Timestamp,
}
// Add more game-specific tables as needed...
// ---------- Module Registration ----------
#[qs_game_module]
struct MyGame;
impl GameModule for MyGame {
const MODULE_NAME: &'static str = "my-game";
fn on_player_join(ctx: &ReducerContext, player_id: &str, server_id: &str) {
// Create initial player state
PlayerState::insert(PlayerState {
player_id: player_id.to_string(),
server_id: server_id.to_string(),
x: 0.0, y: 0.0, z: 0.0,
health: 100.0,
last_update: Timestamp::now(),
}).unwrap();
log::info!("Player {} joined server {}", player_id, server_id);
// Report updated count to platform
let count = PlayerState::iter()
.filter(|p| p.server_id == server_id)
.count();
bridge::report_online_count(ctx, server_id, count as u32);
}
fn on_player_leave(ctx: &ReducerContext, player_id: &str) {
// Clean up player state
if let Some(player) = PlayerState::filter_by_player_id(player_id) {
let server_id = player.server_id.clone();
PlayerState::delete_by_player_id(player_id);
let count = PlayerState::iter()
.filter(|p| p.server_id == server_id)
.count();
bridge::report_online_count(ctx, &server_id, count as u32);
}
}
fn on_heartbeat(ctx: &ReducerContext, server_id: &str) {
// Periodic maintenance
let count = PlayerState::iter()
.filter(|p| p.server_id == server_id)
.count();
bridge::report_online_count(ctx, server_id, count as u32);
bridge::report_server_status(ctx, server_id, ServerStatus::Running);
}
}
Step 3 — Build and Publish¶
# Build the WASM module
spacetime build
# Publish to your SpacetimeDB instance
spacetime publish my-game-module --server your-stdb-host
Step 4 — Create the Bridge Plugin (C++)¶
The bridge plugin runs inside the game server process. You need an engine adapter for your target engine.
Supported Engine Adapters¶
| Engine | Adapter | Method | Status |
|---|---|---|---|
| UE4 4.27 | engine-ue4 |
vtable hooking, LD_PRELOAD |
✅ Production |
| UE5 | engine-ue5 |
vtable hooking, LD_PRELOAD |
🔜 Planned |
| Unity (IL2CPP) | engine-unity |
IL2CPP function hooking | 🔜 Planned |
| Godot | engine-godot |
GDExtension API | 🔜 Planned |
Bridge Hook Structure¶
// my_game_hooks.h
#include <qs-bridge/game_interface.h>
class MyGameModule : public GameInterface {
public:
const char* module_name() override { return "my-game"; }
void on_tick(float delta_time) override {
// Called every engine tick
// Read game state from memory, send to STDB
}
void on_rpc(const char* function_name, const void* params) override {
// Called when game server receives an RPC
// Parse params, call appropriate STDB reducer
}
void register_hooks(EngineAdapter* adapter) override {
// Register which engine functions to intercept
adapter->hook_function("APlayerController::ServerMove", &on_player_move);
adapter->hook_function("AGameMode::PostLogin", &on_player_login);
}
};
Step 5 — Configure the Bridge¶
Create a configuration file for your module:
{
"module_name": "my-game",
"stdb_host": "ws://localhost:3000",
"stdb_module": "my-game-module",
"server_id": "server-01",
"heartbeat_interval_s": 30,
"worker_threads": {
"ai": { "frequency_hz": 2 },
"world": { "frequency_hz": 2 }
}
}
Step 6 — Test Locally¶
# Start SpacetimeDB
spacetime start
# Publish the STDB module
spacetime publish my-game-module
# Start game server with bridge
LD_PRELOAD=/path/to/libqsbridge.so ./GameServer-Linux-Shipping
# Verify registration
spacetime sql my-game-module "SELECT * FROM player_state"
Step 7 — Add Game-Specific Tables¶
Design your tables based on what state needs to be persistent and queryable:
| Category | Guideline |
|---|---|
| Player State | Positions, vitals, inventory — updated frequently, queried by server_id |
| World State | Buildings, vehicles, containers — updated on interaction, persistent across restarts |
| AI State | Zombie/NPC positions, brain state — high frequency, can be reconstructed |
| Social | Factions, chat, mail — low frequency, long retention |
| Events | Combat logs, deaths, security — append-only, used for analytics |
Table Design Best Practices¶
- Always include
server_idfor per-server tables - Use
#[primary_key]on every table — SpacetimeDB requires it - Prefer flat schemas — JSON blobs for game-specific data the platform doesn't need to query
- Separate hot and cold data — frequently-updated position data in one table, rarely-changed config in another
- Use SpacetimeDB enums for type-safe state machines (e.g.,
PlayerStatus::Alive)
Step 8 — Add Reducers¶
#[spacetimedb::reducer]
fn update_position(ctx: &ReducerContext, player_id: String, x: f32, y: f32, z: f32) {
if let Some(mut player) = PlayerState::filter_by_player_id(&player_id) {
player.x = x;
player.y = y;
player.z = z;
player.last_update = Timestamp::now();
PlayerState::update_by_player_id(&player_id, player);
}
}
Step 9 — Publish as a Mod¶
Once your module is tested:
- Build the
.sofile and WASM module - Create a mod manifest (see Mod Manifest)
- Upload to the mod registry
- Document on wiki.qs-zuq.com
Server operators can then install your module via the admin panel's mod management interface.
Module Checklist¶
-
GameModuletrait implemented with all required callbacks - STDB module builds and publishes successfully
- Bridge plugin compiles and loads via
LD_PRELOAD - Player join/leave properly tracked
- Heartbeat updates server status and player count
- Tables designed for multi-server (include
server_idwhere needed) - Unit tests for all reducers (aim for >80% coverage)
- Integration test: full server → bridge → STDB → panel flow
- Documentation in wiki
Related Pages¶
- Game Module SDK — Rust SDK reference
- HumanitZ Module — reference implementation
- Mod Manifest — publishing your module
- Bridge Internals — C++ framework details (restricted)