MotaEngine User Manual
MotaEngine is a C++20 game engine from Motaware LLC. You write games in C++ (and optionally Lua) against the engine's runtime libraries, and ship them as windowed game clients and headless dedicated servers. It uses an archetype-based entity component system, a Vulkan 1.3 renderer, Jolt Physics, miniaudio, LuaJIT, and encrypted UDP networking.
This manual explains the engine's concepts and systems with short examples. Two companion documents go deeper:
sdk-reference.md: the detailed reference for game projects, the prebuilt SDKs, the command line, networking, and packaging.lua-api.md: the complete Lua scripting API.
A visual editor is planned for a later release. This release is the runtime engine: everything in this manual is done from code, the command line, and the in-game developer console.
Table of Contents
- Introduction
- Getting the Engine
- Your First Game Project
- Core Concepts
- Entity Component System
- Input
- Physics
- Rendering
- Audio
- Animation
- Scripting with Lua
- AI
- Networking
- Headless Mode and Dedicated Servers
- UI and Screens
- Gameplay Framework
- Data, Localization, and Files
- Packaging and Asset Cooking
- Configuration and the Console
- Debugging and Profiling
- Building the Engine from Source
- Module and Header Reference
1. Introduction
1.1 Supported platforms
| Target | Status |
|---|---|
| Windows 10/11 x64 game client | Supported (Vulkan 1.3 GPU required) |
| Windows x64 headless / dedicated server | Supported |
| Linux x64 dedicated server (Ubuntu 24.04, Clang) | Supported (headless only, no GPU needed) |
| Android, iOS | Experimental; not part of this release's support scope |
| macOS, Linux game client | Not supported |
1.2 How a game is structured
A MotaEngine game is its own project directory with its own executable. The engine boots, opens your project (a .motaproj file), and instantiates your game module, a C++ class implementing mota::app::IGameModule. Your module seeds the ECS world in Initialize, runs gameplay in Update on the fixed simulation tick, and reaches every engine subsystem through a GameModuleContext. See section 3.
1.3 Conventions used in this manual
- Code samples assume
using namespace mota;and qualify sub-namespaces (core::,rhi::,physics::, and so on). - Public headers are included as
<motaware/<module>/<file>.h>. The root C++ namespace ismota. - Windows build commands go through
build_helper.bat, which sets up the x64 MSVC environment. Without it, CMake can pick up the x86 compiler and fail against the x64 dependencies.
2. Getting the Engine
There are two ways to build games with MotaEngine. Both use the same project layout and the same mota command-line tool, so a project can move between them.
| Prebuilt SDK | Source checkout | |
|---|---|---|
| What you get | Headers, static libraries (Debug, Development, Shipping), the dependency tree, the mota CLI, a sample game, docs | The full engine source tree |
| First build | Configures in seconds, links in about a minute | Builds all vcpkg dependencies and the engine once (can take tens of minutes) |
| Modify the engine | No | Yes |
| Mobile packaging | No | Yes (experimental) |
2.1 Requirements
| Component | Requirement |
|---|---|
| OS | Windows 10 or 11, x64 |
| Compiler | Visual Studio 2026 (version 18) or later with the "Desktop development with C++" workload. Visual Studio 2022 is not supported |
| CMake | 3.25 or later |
| Vulkan SDK | 1.3 or later, from vulkan.lunarg.com |
| vcpkg | Source checkout only. Visual Studio includes one; set VCPKG_ROOT to it |
| GPU | Vulkan 1.3 capable (not needed for headless servers) |
2.2 Prebuilt SDK (Windows)
The Windows SDK is a zip named MotaEngineSDK-<version>-win64.zip. Extract it to a short path (for example C:\MotaSDK) and use its tools\mota to create and build projects:
C:\MotaSDK\tools\mota new MyGame -Dir C:\
cd C:\MyGame
C:\MotaSDK\tools\mota build
C:\MotaSDK\tools\mota run
The SDK libraries are compiled with a specific MSVC toolset, and an older linker cannot read objects from a newer compiler. Your Visual Studio's MSVC toolset must be at least the SDK's; find_package(MotaEngine) stops with a clear message if it is older. The SDK libraries are built without link-time optimization so that your toolchain can link them.
A Linux server SDK (MotaEngineSDK-<version>-linux-x64.tar.gz) builds dedicated servers with only Clang 18+, CMake, Ninja, and libvulkan-dev. See section 14.5 and sdk-reference.md.
2.3 Source checkout
With a source tree, configure and build from its root:
build_helper.bat cmake --preset windows-default
build_helper.bat cmake --build --preset windows-debug
The first configure installs every vcpkg dependency. Games then use the checkout's tools\mota exactly as with the SDK; the first mota build of a project also compiles the engine. See section 21 for presets, tests, and Linux builds.
3. Your First Game Project
3.1 Create, build, run
tools\mota new MyGame # scaffold; keep the project path short (vcpkg + MAX_PATH)
cd MyGame
..\tools\mota build # configure (first time) + build Debug
..\tools\mota run # build, then run windowed (--game --project=MyGame.motaproj)
..\tools\mota run --headless --frames=120 # headless smoke run
..\tools\mota package # Shipping build + packaged game in dist\MyGame\
| Command | What it does |
|---|---|
mota new <Name> [-Dir <parent>] | Scaffolds a project. The name must be a C++ identifier. |
mota build [-Config Debug|Development|Shipping] | Configures on first use, then builds. |
mota run [-Config <cfg>] [<engine args>] | Builds, then runs the game with --project. Adds --game unless you pass --headless or --server. Extra arguments go straight to the engine (no -- separator). |
mota package | Shipping build, then --package (see section 18). |
3.2 Project anatomy
MyGame/
├── CMakeLists.txt builds MyGame.exe against the engine (SDK or source checkout)
├── CMakePresets.json Windows presets, plus linux-server / linux-sdk presets
├── MyGame.motaproj project settings; "gameModule" names your module
├── README.md
├── src/
│ ├── main.cpp registers the module, calls RunEngineMain
│ ├── game_module.h
│ └── game_module.cpp
└── assets/
The scaffolded CMakeLists.txt detects whether MOTA_ENGINE_ROOT points at a source checkout or a prebuilt SDK, and the mota CLI picks the matching presets.
3.3 The game module
main.cpp registers your module by name before the engine boots:
#include <motaware/app/app_main.h>
#include <motaware/app/game_module.h>
#include "game_module.h"
#include <SDL3/SDL_main.h>
#include <memory>
int main(int argc, char** argv) {
mota::app::GameModuleRegistry::Instance().Register(
"MyGame", []() { return std::make_unique<MyGameModule>(); });
return mota::app::RunEngineMain(argc, argv);
}
The module itself (this is what mota new generates, slightly shortened):
#include <motaware/app/game_module.h>
#include <motaware/core/ecs/world.h>
#include <motaware/core/math/transform.h>
#include <motaware/rhi/material.h>
#include <motaware/rhi/mesh.h>
class MyGameModule final : public mota::app::IGameModule {
public:
bool Initialize(const mota::app::GameModuleContext& ctx) override {
m_ctx = ctx;
core::World& world = *ctx.world;
m_cube = world.CreateEntity();
Transform tf;
tf.position = Vec3(0.0f, 1.0f, 0.0f);
world.AddComponent<Transform>(m_cube, tf);
world.AddComponent<rhi::MeshComponent>(m_cube, {}); // default mesh: the built-in cube
rhi::MaterialParams mat;
mat.baseColor = Vec4(0.9f, 0.35f, 0.15f, 1.0f);
mat.roughness = 0.4f;
world.AddComponent<rhi::MaterialParams>(m_cube, mat);
return true; // false aborts boot
}
void Update(f64 dt) override { // every fixed simulation tick (60 Hz default)
m_spin += static_cast<f32>(dt) * 0.8f;
auto& tf = m_ctx.world->GetComponent<Transform>(m_cube);
tf.rotation = glm::angleAxis(m_spin, Vec3(0.0f, 1.0f, 0.0f));
}
void Shutdown() override {}
private:
mota::app::GameModuleContext m_ctx{};
EntityID m_cube{};
f32 m_spin = 0.0f;
};
IGameModule also has optional per-render-frame hooks: UpdateHUD(), UpdateWeather(f64 totalGameTime), and CollectDebugLines(std::vector<app::DebugLine>&).
3.4 GameModuleContext
Initialize receives a GameModuleContext with a pointer to each subsystem. Null pointers are meaningful, so check them:
| Field | Null when |
|---|---|
world | never |
renderer | --headless and --server (no GPU) |
physicsWorld | physics failed to initialize |
screenManager | --headless |
audioEngine, musicSystem | --server |
particleManager, vfxManager, decalManager, clothSimulator, ropeSimulator | --headless |
weatherSystem, cameraManager | never |
scriptEngine | Lua failed to initialize (it runs every ScriptComponent script; see section 11) |
networkManager, replicationManager, rpcManager | the process has no network role (no --server or --connect) |
savedDirectory | never empty in a running game: the writable Saved/ directory for profiles and save games |
requestQuit | callable in a running game; asks the engine to shut down cleanly |
A module that guards every renderer and screenManager use boots identically in a window, under --headless, and as a dedicated server. The full table is in sdk-reference.md.
3.5 Reference project
samples/StormForge in the SDK (projects/StormForge in a source checkout) is a complete game in this shape: front-end menus, HUD, AI bots, and a dedicated-server build.
4. Core Concepts
4.1 The main loop
The engine runs a fixed-timestep simulation with variable-rate rendering:
while (running) {
Input SDL3 events -> InputSystem
Network receive packets, dispatch, handshakes (when a network role exists)
Simulate fixed 60 Hz steps: ECS systems + your module's Update(dt)
Render Vulkan scene + UI, present (skipped in headless mode)
Cleanup reset the frame allocator, dispatch deferred events
}
Accumulated frame time is capped at 0.25 s so a long stall cannot trigger a "spiral of death". In headless mode the loop is paced to the fixed timestep, which --tickrate=N (1 to 240) can change.
4.2 Coordinate system
- Left-handed, Y-up: X right, Y up, Z forward.
- 1 unit = 1 meter.
- Rotations are quaternions (
Quat, identityQuat(1, 0, 0, 0)in w, x, y, z order). - Clip-space depth range is [0, 1] (Vulkan convention).
4.3 Basic types
Fixed-width types from <motaware/pal/types.h>:
u8, u16, u32, u64 // unsigned integers
i8, i16, i32, i64 // signed integers
f32, f64 // floating point
SizeType // size_t
EntityID // { u32 index; u32 generation; }
Math types (GLM aliases) from <motaware/core/math/math_types.h>:
Vec2, Vec3, Vec4, IVec2, IVec3, IVec4, UVec2, UVec3, UVec4, DVec2, DVec3, DVec4
Mat3, Mat4, Quat
Color3, Color4 // RGB / RGBA in [0, 1]
Transform (<motaware/core/math/transform.h>) holds position, rotation, and scale.
4.4 Memory
<motaware/core/memory/> provides LinearAllocator, PoolAllocator, ObjectPool, and the per-frame FrameAllocator (16 MB, reset every frame):
#include <motaware/core/memory/frame_allocator.h>
auto* temp = static_cast<MyStruct*>(
core::FrameAllocator::Get().Allocate(sizeof(MyStruct), alignof(MyStruct)));
// No free: the allocator resets at the end of the frame.
Engine code avoids raw new/delete; prefer allocators or std::unique_ptr. The engine is built without RTTI (/GR-), so dynamic_cast and typeid are unavailable.
4.5 Logging
#include <motaware/core/logging/log.h>
MOTA_LOG_INFO(App, "Level loaded in {} ms", ms);
MOTA_LOG_WARN(Physics, "Body {} exceeded velocity limit", bodyID);
MOTA_LOG_ERROR(Rendering, "Failed to load texture {}", path);
MOTA_LOG_DEBUG(AI, "Agent {} -> state {}", entity.index, stateName);
Categories: Core, PAL, Memory, Config, Events, Rendering, Physics, Audio, Input, Animation, AI, Network, Scripting, Asset, Editor, App, ECS, Jobs, Reflection, Scene, RHI, Gameplay.
Severities: Verbose, Debug, Info, Warning, Error, Fatal. Verbose and Debug compile out of Shipping builds. Logs go to the console and to Saved/Logs/ next to the executable.
4.6 Events
#include <motaware/core/events/event_bus.h>
struct PlayerDiedEvent { // events must be trivially copyable
EntityID player;
EntityID killer;
};
auto& bus = core::EventBus::Instance();
auto id = bus.Subscribe<PlayerDiedEvent>([](const PlayerDiedEvent& e) {
MOTA_LOG_INFO(Gameplay, "Player {} killed by {}", e.player.index, e.killer.index);
});
bus.PublishImmediate(PlayerDiedEvent{player, enemy}); // dispatched now
bus.PublishDeferred(PlayerDiedEvent{player, enemy}); // dispatched at end of frame
bus.Unsubscribe(id);
5. Entity Component System
The ECS (<motaware/core/ecs/>) stores components in archetypes for cache-friendly iteration. Your module gets the live world as ctx.world.
5.1 Entities and components
Components must be trivially copyable and default constructible. Heavy runtime state (physics bodies, animation state machines, behavior trees) lives in manager classes outside the ECS, and components hold u32 indices into them.
struct Velocity {
Vec3 value{0.0f};
};
core::World& world = *ctx.world;
EntityID e = world.CreateEntity();
world.AddComponent<Velocity>(e, {Vec3(1, 0, 0)});
Velocity& vel = world.GetComponent<Velocity>(e);
if (world.HasComponent<Velocity>(e)) { /* ... */ }
world.RemoveComponent<Velocity>(e);
if (world.IsAlive(e)) world.DestroyEntity(e);
5.2 Queries
// All entities with Transform AND Velocity, but NOT StaticTag
world.ForEach<core::Query<core::All<Transform, Velocity>, core::None<StaticTag>>>(
[dt](EntityID id, Transform& tf, Velocity& vel) {
tf.position += vel.value * static_cast<f32>(dt);
});
Do not add or remove components, or create or destroy entities, inside ForEach. Collect the changes and apply them after the loop.
5.3 Systems
world.RegisterSystem({
.name = "Movement",
.phase = core::SystemPhase::Update,
.orderWithinPhase = 0, // lower runs first within a phase
.execute = [](core::World& w, f64 dt) {
w.ForEach<core::Query<core::All<Transform, Velocity>>>(
[dt](EntityID, Transform& tf, Velocity& vel) {
tf.position += vel.value * static_cast<f32>(dt);
});
}
});
Phases run in order: PreUpdate, Update, PostUpdate, PreRender. The engine runs registered systems every fixed simulation step; you do not call RunSystems yourself.
5.4 Singletons
struct MatchState { f64 timeLeft = 300.0; u32 round = 1; };
world.SetSingleton(MatchState{});
MatchState& match = world.GetSingleton<MatchState>();
5.5 Hierarchy
#include <motaware/core/ecs/hierarchy.h>
core::hierarchy::SetParent(world, child, parent);
const auto& children = core::hierarchy::GetChildren(world, parent);
core::hierarchy::DestroyRecursive(world, root); // entity and all descendants
The engine registers the transform system, which computes each entity's core::WorldTransform from its local Transform and its parent chain.
5.6 Reflection
#include <motaware/core/reflection/reflect_macros.h>
struct Turret {
f32 turnSpeed = 90.0f;
f32 range = 25.0f;
bool active = true;
};
// In a .cpp file:
MOTA_REFLECT_TYPE_BEGIN(Turret)
MOTA_REFLECT_PROPERTY(Turret, turnSpeed)
MOTA_REFLECT_PROPERTY(Turret, range)
MOTA_REFLECT_PROPERTY(Turret, active)
MOTA_REFLECT_TYPE_END(Turret)
Reflected types register in core::TypeRegistry, which the serialization code uses to read and write component properties. MOTA_REFLECT_PROPERTY_EX adds a display name, tooltip, and range; MOTA_REFLECT_ENUM_BEGIN/VALUE/END reflects enums.
6. Input
6.1 Actions and axes
#include <motaware/input/input_system.h>
#include <motaware/input/input_context.h>
auto ctx = std::make_shared<input::InputContext>("Gameplay");
input::ActionBinding jump;
jump.name = "Jump";
jump.sources.push_back(input::InputSource::Key(input::KeyCode::Space));
jump.sources.push_back(input::InputSource::Pad(input::GamepadButton::A));
ctx->AddAction(jump);
input::AxisBinding forward;
forward.name = "MoveForward";
forward.sources.push_back(input::InputSource::KeyAxis(input::KeyCode::W, 1.0f));
forward.sources.push_back(input::InputSource::KeyAxis(input::KeyCode::S, -1.0f));
forward.sources.push_back(input::InputSource::PadAxis(input::GamepadAxis::LeftY));
ctx->AddAxis(forward);
input::InputSystem::Instance().PushContext(ctx);
Action sources are OR-ed; axis sources are summed. InputSource::Mouse(MouseButton) binds mouse buttons.
6.2 Querying input
auto& in = input::InputSystem::Instance();
if (in.IsActionTriggered("Jump")) { /* pressed this frame */ }
if (in.IsActionPressed("Fire")) { /* held */ }
if (in.IsActionCompleted("Fire")) { /* released this frame */ }
f32 fwd = in.GetAxisValue("MoveForward"); // -1..1
if (in.IsKeyDown(input::KeyCode::Escape)) { /* raw key state */ }
f32 lx = in.GetGamepadAxisValue(input::GamepadAxis::LeftX);
6.3 Context stack
Contexts stack; the topmost gets input first:
in.PushContext(gameplayContext);
in.PushContext(menuContext); // menu takes priority
in.PopContext(); // back to gameplay
6.4 Enhanced input
<motaware/input/enhanced_input.h> adds an Unreal-style layer on top: InputMappingContext, modifiers (dead zone, negate, swizzle, and so on), and triggers (pressed, released, held, tap, hold-and-release) evaluated by EnhancedInputSystem.
On Android and iOS, touches arrive as pointer emulation plus an optional on-screen virtual gamepad (--touch-controls) that drives Pad/PadAxis bindings; see sdk-reference.md.
7. Physics
Physics uses Jolt and is stepped by the engine on the fixed tick. Your module gets the world as ctx.physicsWorld.
7.1 Rigid bodies
#include <motaware/physics/rigid_body.h>
EntityID box = world.CreateEntity();
world.AddComponent<Transform>(box, {.position = Vec3(0, 10, 0)});
physics::RigidBody rb;
rb.motionType = physics::MotionType::Dynamic;
rb.shape = physics::CollisionShapeDesc::Box(0.5f, 0.5f, 0.5f); // half-extents
rb.mass = 1.0f;
world.AddComponent<physics::RigidBody>(box, rb);
world.AddComponent<physics::PhysicsBodyPending>(box); // the physics system creates the body
Once created, RigidBody::bodyID holds the Jolt body index and the entity's Transform follows the simulation. Other RigidBody fields: layer, material, linearDamping, angularDamping, gravityScale, initialLinearVelocity, isTrigger, enableCCD.
7.2 Motion types and shapes
- Static: never moves (floors, walls).
- Kinematic: moved by code (doors, platforms); not affected by forces.
- Dynamic: affected by gravity, forces, and collisions.
auto sphere = physics::CollisionShapeDesc::Sphere(0.5f);
auto box = physics::CollisionShapeDesc::Box(1.0f, 0.5f, 2.0f); // half-extents
auto capsule = physics::CollisionShapeDesc::Capsule(0.4f, 1.0f); // radius, half-height
7.3 Raycasts, forces, velocities
physics::PhysicsWorld& phys = *ctx.physicsWorld;
physics::RaycastParams params;
params.origin = Vec3(0, 10, 0);
params.direction = Vec3(0, -1, 0);
params.maxDistance = 100.0f;
physics::RaycastHit hit = phys.Raycast(params);
if (hit.hit) {
MOTA_LOG_INFO(Physics, "Hit entity {} at {} m", hit.bodyEntity.index, hit.distance);
}
phys.AddForce(rb.bodyID, Vec3(0, 100, 0)); // applied over the step
phys.AddImpulse(rb.bodyID, Vec3(0, 10, 0)); // instantaneous
phys.SetLinearVelocity(rb.bodyID, Vec3(5, 0, 0));
7.4 Constraints
#include <motaware/physics/constraints.h>
physics::ConstraintDesc hinge;
hinge.type = physics::ConstraintType::Hinge;
hinge.bodyA = doorFrameBody;
hinge.bodyB = doorBody;
hinge.axisA = Vec3(0, 1, 0);
hinge.hasLimits = true;
hinge.minLimit = -1.57f;
hinge.maxLimit = 1.57f;
u32 constraintID = phys.CreateConstraint(hinge);
Constraint types: Fixed, Hinge, BallSocket, Cone, Slider, Distance. Set frequency and damping for a soft (spring) constraint.
7.5 Cloth, rope, and soft bodies
The engine owns a cloth and a rope simulator (ctx.clothSimulator, ctx.ropeSimulator, null in headless mode). Soft bodies use a SoftBodySimulator you own.
#include <motaware/physics/cloth.h>
#include <motaware/physics/rope.h>
// 16x16 cloth, 2 m x 2 m, 1 kg, top edge pinned
u32 flag = ctx.clothSimulator->CreateCloth(16, 16, 2.0f, 2.0f, 1.0f, /*pinTop=*/true);
ctx.clothSimulator->AddCollisionSphere(Vec3(0), 0.5f);
// 16-segment, 5 m rope pinned at the start
u32 rope = ctx.ropeSimulator->CreateRope(16, 5.0f, Vec3(0, 10, 0), Vec3(5, 10, 0),
/*pinStart=*/true, /*pinEnd=*/false);
7.6 Vehicles, characters, ragdolls, destruction
| Header | Main types | Purpose |
|---|---|---|
physics/vehicle.h | VehicleComponent, WheelConfig, VehicleManager | Wheeled vehicles: engine torque, steering, driven wheels (VehicleManager::SetInput(index, forward, right, brake, handBrake)) |
physics/character_controller.h | CharacterControllerComponent, CharacterControllerManager | Capsule character movement |
physics/ragdoll.h | RagdollComponent, RagdollManager | Ragdolls driven by constraints |
physics/fracture.h | DestructibleComponent, FracturePattern | Break an object into fragments when its health reaches zero |
physics/soft_body.h | SoftBodySimulator | Mass-spring soft bodies |
8. Rendering
The renderer is Vulkan 1.3 with physically based materials, cascaded shadows, image-based lighting, a post-processing stack, sky and volumetric clouds, terrain, particles and VFX, and dynamic global illumination. You describe the scene with ECS components; the engine renders it. In headless mode there is no renderer (ctx.renderer is null) but the same components are harmless.
8.1 Meshes and materials
An entity is drawn when it has a Transform, an rhi::MeshComponent, and an rhi::MaterialParams:
#include <motaware/rhi/mesh.h>
#include <motaware/rhi/material.h>
#include <motaware/rhi/renderer.h>
rhi::MeshComponent mesh;
mesh.meshID = rhi::Renderer::kMeshSphere; // kMeshCube (default), kMeshPlane, kMeshSphere, kMeshCylinder, ...
world.AddComponent<rhi::MeshComponent>(e, mesh);
rhi::MaterialParams mat;
mat.baseColor = Vec4(0.8f, 0.2f, 0.2f, 1.0f);
mat.metallic = 0.0f;
mat.roughness = 0.5f;
world.AddComponent<rhi::MaterialParams>(e, mat);
For your own geometry, build an rhi::MeshData (or use a generator: MeshData::CreateCube, CreateSphere, CreatePlane, CreateCylinder, CreateRock, CreateTree) and upload it:
if (ctx.renderer) {
rhi::MeshData data = rhi::MeshData::CreatePlane(10.0f, 10.0f, 4, 4);
mesh.meshID = ctx.renderer->AllocateRuntimeMesh(data);
}
<motaware/rhi/mesh_simplify.h> builds LOD chains from a runtime mesh; see "Mesh LODs" in sdk-reference.md.
8.2 Models and textures
#include <motaware/rhi/model_loader.h>
rhi::LoadedModel model = rhi::LoadModel("assets/models/crate.glb"); // glTF, FBX, OBJ, ...
if (model.valid && ctx.renderer) {
for (const rhi::LoadedMesh& m : model.meshes) {
u32 meshID = ctx.renderer->AllocateRuntimeMesh(m.meshData);
// ... create an entity with MeshComponent{meshID} and the matching material
}
}
u32 albedo = ctx.renderer->LoadTexture("assets/textures/crate_albedo.png"); // sRGB
u32 orm = ctx.renderer->LoadTexture("assets/textures/crate_orm.png", /*srgb=*/false);
mat.albedoTexture = albedo;
mat.ormTexture = orm;
LoadedModel also carries materials, a skeleton, and animation clips when the file has them. Packaged builds load the cooked binary form of each model and the block-compressed form of each texture automatically (see section 18); rhi::ModelExists(path) checks for either form.
8.3 Lights
Lights are components. With a Transform, the light takes its position from the entity:
#include <motaware/rhi/light.h>
#include <motaware/rhi/area_light.h>
rhi::DirectionalLight sun;
sun.direction = glm::normalize(Vec3(-1, -1, -1));
sun.color = Vec3(1.0f, 0.95f, 0.9f);
sun.intensity = 1.0f;
sun.castShadows = true;
world.AddComponent<rhi::DirectionalLight>(world.CreateEntity(), sun);
rhi::PointLight lamp;
lamp.color = Vec3(1.0f, 0.8f, 0.6f);
lamp.radius = 10.0f;
EntityID lampEntity = world.CreateEntity();
world.AddComponent<Transform>(lampEntity, {.position = Vec3(5, 3, 0)});
world.AddComponent<rhi::PointLight>(lampEntity, lamp);
rhi::AreaLightComponent panel; // Rect, Disc, Sphere, or Tube
panel.shape = rhi::AreaLightShape::Rect;
panel.width = 2.0f;
panel.height = 1.0f;
panel.intensity = 10.0f;
world.AddComponent<rhi::AreaLightComponent>(lampEntity, panel);
rhi::SpotLight adds direction and range.
8.4 Camera
The engine renders through ctx.cameraManager (gameplay::CameraManager), which blends between prioritized virtual cameras and adds camera shake. The engine updates it every frame.
#include <motaware/gameplay/camera_system.h>
gameplay::VirtualCamera follow;
follow.name = "Follow";
follow.priority = 10; // highest priority wins (with a blend)
follow.followTarget = player;
follow.followOffset = Vec3(0, 8, -12);
follow.lookAtTarget = player;
u32 camIndex = ctx.cameraManager->AddCamera(follow);
ctx.cameraManager->TriggerShake(0.5f, 0.3f); // intensity, duration
Use GetCamera(index) to drive a camera's pose yourself each frame. rhi::CameraComponent plus rhi::ComputeViewMatrix / ComputeProjectionMatrix (<motaware/rhi/camera.h>) are available for your own view math.
8.5 Weather and time of day
#include <motaware/rhi/weather.h>
ctx.weatherSystem->TransitionTo(rhi::WeatherPreset::Storm, 5.0f); // 5 s blend
ctx.weatherSystem->GetTimeOfDay().hours = 18.0f; // sunset
Presets: Clear, Cloudy, Overcast, Rain, HeavyRain, Snow, Fog, Storm. The engine updates the weather system each frame and feeds wind into cloth and particles.
8.6 Render settings
Renderer features are controlled with r.* console commands. Run them from the developer console, from --exec="..." on the command line, or from code with core::ConsoleSystem::Instance().Execute("r.bloom 1"). A few examples:
| Command | Controls |
|---|---|
r.vsync, r.renderScale, r.upscale | presentation and resolution scaling |
r.taa, r.fxaa | anti-aliasing |
r.bloom, r.bloom.intensity, r.exposure, r.tonemap, r.vignette | post-processing |
r.grade.contrast, r.grade.saturation | color grading |
r.ssr, r.fog, r.fog.density, r.fog.godrays, r.clouds, r.aerial | reflections, fog, sky |
r.shadow.cascades, r.vsm | shadows |
r.lumen.global, r.ddgi, r.rt.shadows, r.rt.reflections | global illumination and ray tracing (where supported) |
r.sun.elevation, r.sun.azimuth | sun direction |
r.wireframe, r.profilegpu | debugging |
Type help in the console for the full list.
8.7 Particles, VFX, and decals
ctx.particleManager, ctx.vfxManager, and ctx.decalManager expose particle emitters, data-driven VFX assets, and projected decals (<motaware/rhi/particle.h>, vfx_manager.h, decal.h). They are null in headless mode.
9. Audio
Audio uses miniaudio with 3D spatialization and bus mixing. ctx.audioEngine is null on dedicated servers; in plain --headless it runs on a null output device.
9.1 Playing sounds
#include <motaware/audio/audio_engine.h>
audio::AudioEngine& snd = *ctx.audioEngine;
audio::AudioClipHandle clip = snd.LoadClip({"assets/audio/explosion.wav"});
audio::PlayParams params;
params.volume = 0.8f;
params.spatialized = true;
params.posX = 10.0f; params.posY = 0.0f; params.posZ = 5.0f;
audio::SoundHandle h = snd.Play(clip, params);
// snd.Stop(h);
AudioClipDesc also takes streaming = true to stream long files (music) from disk. PlayParams covers bus, pitch, looping, minDistance, maxDistance, and the attenuation model.
9.2 Buses
snd.SetBusVolume(audio::AudioBusType::Music, 0.5f);
snd.SetBusVolume(audio::AudioBusType::SFX, 1.0f); // also Dialogue
snd.SetMasterVolume(0.8f);
9.3 ECS integration
To play sounds from components, register the audio systems once, then add AudioSource components and mark one listener:
#include <motaware/audio/audio_system.h>
#include <motaware/audio/audio_source.h>
if (ctx.audioEngine) audio::audio_system::Register(world, *ctx.audioEngine);
audio::AudioSource src;
src.clip = clip;
src.volume = 0.7f;
src.spatialized = true;
src.playOnAdd = true;
world.AddComponent<audio::AudioSource>(emitter, src);
world.AddComponent<audio::AudioListener>(cameraEntity);
9.4 More audio
ctx.musicSystem (MusicSystem) layers and crossfades music with stingers. Other headers in <motaware/audio/> cover DSP effects, reverb zones, occlusion, procedural synthesis (AudioGraph), sound cues, beat-synchronized scheduling (quartz.h), voice chat, and Steam Audio HRTF spatialization.
10. Animation
10.1 Skeletons and clips
Skeletons (anim::Skeleton, up to 256 bones) and clips (anim::AnimationClip) usually come from a model file through rhi::LoadModel (LoadedModel::skeleton and LoadedModel::animations).
10.2 State machines
#include <motaware/anim/animation_state_machine.h>
anim::AnimationStateMachine sm;
anim::AnimationState& idle = sm.AddState("Idle", &idleClip);
idle.transitions.push_back({
.targetState = "Run",
.crossfadeDuration = 0.2f,
.conditions = {{.paramName = "Speed", .type = anim::TransitionConditionType::GreaterThan, .value = 0.5f}}
});
anim::AnimationState& run = sm.AddState("Run", &runClip);
run.transitions.push_back({
.targetState = "Idle",
.crossfadeDuration = 0.2f,
.conditions = {{.paramName = "Speed", .type = anim::TransitionConditionType::LessThan, .value = 0.1f}}
});
sm.SetEntryState("Idle");
anim::AnimStateMachineParams params;
params.Set("Speed", currentSpeed);
sm.Update(dt, params);
10.3 Driving animation through the ECS
anim::AnimationRuntime stores skeletons, state machines, and parameter sets; the anim::Animator component points at them by index, and the animation system writes the final bone matrices into each entity's anim::SkinningData component:
#include <motaware/anim/animation_system.h>
#include <motaware/anim/animator.h>
// m_animRuntime is a member of your module (it must outlive the world's systems)
anim::animation_system::Register(world, m_animRuntime);
u32 skel = m_animRuntime.AddSkeleton(model.skeleton);
u32 smIx = m_animRuntime.AddStateMachine(sm);
world.AddComponent<anim::Animator>(character, {.skeletonIndex = skel, .stateMachineIndex = smIx});
world.AddComponent<anim::SkinningData>(character, {});
// Each tick:
m_animRuntime.GetParams(smIx).Set("Speed", speed);
10.4 Blend spaces, montages, motion matching
#include <motaware/anim/blend_space_2d.h>
#include <motaware/anim/montage.h>
anim::BlendSpace2D locomotion;
locomotion.AddEntry(&idleClip, Vec2(0, 0));
locomotion.AddEntry(&walkClip, Vec2(1, 0));
locomotion.AddEntry(&runClip, Vec2(2, 0));
locomotion.SetParameters(speed, direction);
locomotion.Evaluate(dt, outPose);
anim::AnimMontage attack;
attack.clip = &attackClip;
attack.blendInTime = 0.1f;
attack.blendOutTime = 0.2f;
anim::MontagePlayer montage;
montage.Play(&attack);
montage.Update(dt);
f32 weight = montage.GetWeight(); // blend weight over the base pose
Other animation headers in <motaware/anim/>: motion_matching.h (MotionMatcher), ik.h (two-bone and other IK), retarget.h, additive_animation.h, anim_layer.h, sync_group.h, morph_target.h, procedural_animation.h, control_rig.h, animation_event.h (notifies).
11. Scripting with Lua
The engine embeds LuaJIT for gameplay scripting. Scripts attach to entities and receive lifecycle callbacks; all engine bindings live on the global mota table. The full binding list is in lua-api.md.
11.1 A script
assets/scripts/mover.lua:
return {
OnCreate = function(entity)
mota.log("mover created: " .. entity)
end,
OnUpdate = function(entity, dt)
local x, y, z = mota.getPosition(entity)
local fwd = mota.getAxis("MoveForward")
mota.setPosition(entity, x, y, z + fwd * 5.0 * dt)
end,
OnDestroy = function(entity)
mota.log("mover destroyed")
end,
}
A script returns a table; OnCreate(entity), OnUpdate(entity, dt), and OnDestroy(entity) are optional. Entities are passed as opaque number handles that stay valid only while the entity lives.
11.2 Attaching scripts to entities
The engine hosts the Lua runtime: at startup it creates the scripting::ScriptEngine, connects it to the world, physics, input and audio, and registers the script systems, on clients, headless runs and dedicated servers alike. An entity runs a script by carrying a ScriptComponent:
#include <motaware/scripting/script_component.h>
scripting::ScriptComponent sc;
std::strncpy(sc.scriptPath, "projects/MyGame/Scripts/mover.lua", sizeof(sc.scriptPath) - 1);
world.AddComponent<scripting::ScriptComponent>(entity, sc);
The script systems load each new script and call OnCreate in PreUpdate, call OnUpdate every fixed tick, and call OnDestroy once the entity is destroyed or loses its ScriptComponent (and for every script at engine shutdown). Scripts are read through the virtual file system, so a packaged game loads them from its pak. In Debug and Development builds a changed script file is reloaded while the game runs. Errors are logged with the script's path and line.
Your module reaches the runtime through ctx.scriptEngine to run a chunk (LoadScriptFromString), read globals (GetGlobalNumber), or register its own Lua functions on GetLuaState(). The full scripting reference, including every mota function, is lua-api.md.
11.3 Sandboxing
os, io, loadfile, dofile, require, package and debug are removed from the Lua state, so scripts have no file system, process or native-code access. Still treat scripts as trusted game content rather than a security boundary for untrusted user code.
12. AI
12.1 Behavior trees
#include <motaware/ai/behavior_tree.h>
#include <motaware/ai/behavior_tree_nodes.h>
auto root = std::make_unique<ai::SelectorNode>("Root");
auto patrol = std::make_unique<ai::SequenceNode>("Patrol");
patrol->AddChild(std::make_unique<ai::CustomTaskNode>(
[](ai::BTContext& ctx) {
ctx.blackboard->Set<Vec3>("PatrolTarget", Vec3(10, 0, 5));
return ai::NodeStatus::Success;
}, "PickPatrolPoint"));
patrol->AddChild(std::make_unique<ai::WaitNode>(2.0f, "Wait"));
root->AddChild(std::move(patrol));
ai::BehaviorTree tree;
tree.SetRoot(std::move(root));
ai::Blackboard bb;
ai::BTContext btCtx{.blackboard = &bb, .entity = agent, .deltaTime = static_cast<f32>(dt)};
ai::NodeStatus status = tree.Tick(btCtx);
Node types: SelectorNode, SequenceNode, ParallelNode, InverterNode, RepeatNode, WaitNode, CustomTaskNode. Results are Success, Failure, or Running.
12.2 Blackboard
#include <motaware/ai/blackboard.h>
ai::Blackboard bb;
bb.Set<Vec3>("TargetPosition", Vec3(10, 0, 5));
bb.Set<bool>("PlayerDetected", true);
std::optional<Vec3> target = bb.Get<Vec3>("TargetPosition"); // nullopt if missing or wrong type
12.3 Pathfinding
#include <motaware/ai/pathfinding.h>
#include <motaware/ai/navmesh.h>
// Grid A*
ai::GridNavMesh grid;
grid.SetSize(100, 100, 1.0f); // 100 x 100 cells, 1 m each
grid.SetWalkable(50, 50, false); // block a cell
std::vector<Vec3> path = grid.FindPath(Vec3(0, 0, 0), Vec3(90, 0, 90)); // empty if unreachable
// Navigation mesh with smoothed paths
ai::NavMesh navmesh;
navmesh.BuildGrid(Vec3(-50, 0, -50), Vec3(50, 0, 50), 1.0f);
std::vector<Vec3> waypoints = navmesh.FindSmoothPath(startPos, endPos);
12.4 Goal-oriented planning (GOAP)
#include <motaware/ai/goap.h>
ai::GOAPPlanner planner;
ai::GOAPAction getWeapon;
getWeapon.name = "GetWeapon";
getWeapon.effects = {{"hasWeapon", 1.0f}};
planner.AddAction(getWeapon);
ai::GOAPGoal killEnemy;
killEnemy.name = "KillEnemy";
killEnemy.desiredState = {{"enemyDead", 1.0f}};
planner.AddGoal(killEnemy);
ai::GOAPPlan plan = planner.Plan(currentWorldState); // ai::WorldState = map<string, f32>
12.5 More AI
ai/perception.h (PerceptionComponent, sight and hearing), ai/crowd.h (CrowdManager, local avoidance), ai/utility_ai.h (UtilityEvaluator, response curves), and gameplay/state_tree.h (StateTree) complement behavior trees.
13. Networking
The engine has a real UDP transport with reliability, fragmentation, and encryption (libhydrogen). Every datagram after the handshake is encrypted and authenticated.
13.1 Network roles
A process gets a network role from the command line:
MyGame.exe --server --project=MyGame.motaproj --port=27015 # listen (implies --headless)
MyGame.exe --game --project=MyGame.motaproj --connect=127.0.0.1:27015 # join
With a role, the engine creates a net::NetworkManager, ticks it every frame, and hands it to your module as ctx.networkManager together with ctx.replicationManager and ctx.rpcManager. Without one, all three are null. The net.status console command prints the live session.
Useful flags: --max-connections=N (default 32), --snapshot-rate=N (default 30 per second), --server-key=<64 hex> (pin the server's public key on the client), and --sim-loss, --sim-latency, --sim-jitter to test bad connections. Clients that lose their server reconnect automatically. The full flag list, the security model, and connection lifetime events are in sdk-reference.md.
13.2 Replication
The engine replicates Transform and sends each client a reliable delta of what changed since that client's last update. To replicate an entity, give it a ReplicatedComponent on the server:
#include <motaware/net/replication.h>
if (ctx.networkManager && ctx.networkManager->GetRole() == net::NetworkRole::Server) {
world.AddComponent<net::ReplicatedComponent>(
e, net::ReplicatedComponent{ctx.replicationManager->AssignNetworkID()});
}
The entity and its registered components then appear in every client's world. Register your own component types with ctx.replicationManager->RegisterType<MyComp>("MyComp"), in the same order on server and client: the wire identifies types by registration order.
13.3 RPCs
#include <motaware/net/rpc.h>
#include <motaware/net/packet.h>
// Both sides register, in the same order:
ctx.rpcManager->RegisterRPC("Explode", net::RPCType::MulticastRPC,
[](core::BinaryReader& args) {
u32 netID = args.ReadU32();
// play the explosion for netID
});
// Server sends it to every client:
core::BinaryWriter args; args.WriteU32(entityNetID);
core::BinaryWriter body; ctx.rpcManager->CallRPC("Explode", args, body);
net::Packet p(net::PacketType::RPC);
p.GetWriter().WriteBytes(body.GetBuffer().data(), body.GetSize());
p.Finalize();
ctx.networkManager->BroadcastPacket(p, /*reliable=*/true);
RPC types: ServerRPC (client to server), ClientRPC (server to one client), MulticastRPC (server to all). Incoming RPC packets are dispatched to the registered handlers automatically.
13.4 Lag compensation and client prediction
#include <motaware/net/lag_compensation.h>
// Server: record hitboxes every tick, then test shots at the shooter's time
net::LagCompensation lagComp;
lagComp.RecordState(netID, position, rotation, halfExtents, serverTime);
auto hit = lagComp.RaycastRewound(rayOrigin, rayDir, maxDist, clientTime);
if (hit.hit) { /* damage the entity with network id hit.entityNetID */ }
// Client: predict local movement, reconcile with the server
net::ClientPrediction prediction;
prediction.moveSpeed = 5.0f;
prediction.RecordInput(frame, movementInput, jumpPressed);
prediction.Reconcile(lastAckedFrame, serverPosition); // true if a correction was applied
Vec3 displayPos = prediction.predictedPosition;
<motaware/net/> also contains snapshot interpolation (SnapshotInterpolator), interest management (ReplicationGraph), replays, and a WebRTC transport. Client code for online services (accounts, matchmaking, leaderboards, cloud saves) is described in online-services.md.
14. Headless Mode and Dedicated Servers
14.1 Headless mode
--headless boots with no window, renderer, UI, or runtime screens. The main loop ticks input state, the fixed-timestep simulation, and your game module, paced to the tick rate. It skips particles, VFX, decals, cloth, and rope; audio uses a null device. Use it for CI, automated tests, and bots:
mota run --headless --frames=120
MyGame.exe --headless --project=MyGame.motaproj --frames=600 --exec="net.status"
--frames=N quits after N frames; --exec="cmd1;cmd2" runs console commands before the first frame; --tickrate=N (1 to 240) sets the simulation rate.
14.2 Dedicated servers
--server implies --headless, listens on UDP --port (default 27015), skips audio and music, and caps the job pool at 4 workers (--workers=N, 1 to 64, overrides). Failing to bind the port aborts boot. Your game's own executable becomes a server with --server; the engine checkout also builds a generic MotaServer binary that always runs as a server.
MyGame.exe --server --project=MyGame.motaproj --port=27015 --tickrate=30 --max-connections=64
The server's identity key is created on first boot in Saved/Net/server_identity.key; back it up and keep it private. Its public key is logged at startup, and clients pin it with --server-key=<64 hex>.
14.3 Health endpoint
--health-port=N serves HTTP for orchestrators and dashboards (--health-bind=ADDR picks the interface, default 0.0.0.0):
| Route | Returns |
|---|---|
GET /health | 200 while the main loop is healthy, 503 when stalled, not listening, or draining at shutdown |
GET /status | JSON: engine, game, loop timing, network, and memory |
GET /metrics | Prometheus text format |
The endpoint is unauthenticated, so bind it to a management interface or firewall it. Servers shut down cleanly on Ctrl+C and SIGTERM.
14.4 Writing server-safe modules
- Check
ctx.renderer,ctx.screenManager,ctx.audioEngine, and the effects managers for null (see section 3.4). - Branch on
ctx.networkManager->GetRole()for authority logic. - Register replicated types and RPCs in the same order on both sides.
14.5 Linux dedicated servers
Games build Linux servers with the linux-server preset (source checkout, Clang, no editor) or the linux-sdk preset (prebuilt Linux SDK). Every scaffolded project has both. With the Linux SDK:
export MOTA_ENGINE_ROOT=/opt/MotaEngineSDK-<version>-linux-x64
cmake --preset linux-sdk
cmake --build --preset linux-sdk-shipping
From a source checkout (requires clang, CMake 3.25+, ninja, libx11-dev libxext-dev libvulkan-dev glslc, and vcpkg at $VCPKG_ROOT):
export MOTA_ENGINE_ROOT=~/MotaEngine VCPKG_ROOT=~/vcpkg
cmake --preset linux-server
cmake --build --preset linux-server-shipping
~/mota-build-MyGame/Shipping/MyGame --server --project=MyGame.motaproj
The resulting server needs no GPU. A Windows client connects to it unchanged. The source tree's root Dockerfile builds a server container image with a HEALTHCHECK on the health endpoint; sdk-reference.md covers it and the SDK's Dockerfile.sdk.
15. UI and Screens
The engine's runtime UI is its own framework (<motaware/ui/>) rendered by the Vulkan renderer. On top of it, the screen manager (<motaware/screens/>) runs a stack of full-screen states.
15.1 Screen manager
The engine creates the screen manager (ctx.screenManager, null in headless mode) and registers the built-in screens: splash, main menu, loading, pause, settings, HUD, game over, credits, save/load, inventory, dialogue, and the developer console.
#include <motaware/screens/screen_manager.h>
if (ctx.screenManager) {
ctx.screenManager->SwitchTo(screens::ScreenID::HUD); // replace the stack
ctx.screenManager->PushScreen(screens::ScreenID::Pause); // overlay
ctx.screenManager->PopScreen();
}
15.2 Custom screens
Subclass screens::Screen and draw with ui::UIDrawContext. Registering a screen with an existing ScreenID replaces the built-in one; ScreenID::Shop and ScreenID::HowToPlay are reserved for game-owned screens.
#include <motaware/screens/screen.h>
#include <motaware/ui/ui_draw_context.h>
class TitleScreen : public screens::Screen {
public:
TitleScreen() : screens::Screen(screens::ScreenID::MainMenu) {}
void OnDraw(ui::UIDrawContext& dc) override {
dc.DrawRect({0.0f, 0.0f, 1920.0f, 1080.0f}, ui::Color4{0.03f, 0.04f, 0.07f, 0.85f});
dc.DrawText("MY GAME", {760.0f, 240.0f}, 76.0f, ui::Color4{0.96f, 0.84f, 0.36f, 1.0f});
}
};
ctx.screenManager->RegisterScreen(std::make_unique<TitleScreen>());
Screen also has OnEnter, OnExit, and OnUpdate(f64). Use IGameModule::UpdateHUD() to push game state into the HUD each frame. StormForge's src/menu_screen.h is a complete keyboard- and mouse-driven menu built this way.
15.3 Widgets
For retained-mode layouts, the widget classes in <motaware/ui/> build a tree:
#include <motaware/ui/layout_widgets.h>
#include <motaware/ui/input_widgets.h>
auto panel = std::make_unique<ui::PanelWidget>("MainPanel");
panel->SetBackgroundColor({0.1f, 0.1f, 0.1f, 0.9f});
auto play = std::make_unique<ui::ButtonWidget>("Start Game", "PlayButton"); // text, name
play->SetOnClick([] { /* start the match */ });
panel->AddChild(std::move(play));
Widgets: PanelWidget, BoxWidget, GridWidget (layout), ButtonWidget, CheckboxWidget, SliderWidget, TextInputWidget (input), TextWidget, ProgressBarWidget, ImageWidget (display). ScreenID::Widget hosts a data-driven widget layout loaded from a .motawidget asset.
16. Gameplay Framework
16.1 Characters and damage
#include <motaware/gameplay/character.h>
#include <motaware/gameplay/damage.h>
world.AddComponent<gameplay::CharacterComponent>(enemy, {.health = 100.0f, .maxHealth = 100.0f});
world.AddComponent<gameplay::CharacterStateComponent>(enemy, {});
world.RegisterSystem(gameplay::MakeCharacterSystem()); // once: death detection + respawn timer
// React to damage: ApplyDamage only publishes the event, your code decides what it does.
core::EventBus::Instance().Subscribe<gameplay::DamageEvent>(
[w = ctx.world](const gameplay::DamageEvent& e) {
if (w->IsAlive(e.target) && w->HasComponent<gameplay::CharacterComponent>(e.target))
w->GetComponent<gameplay::CharacterComponent>(e.target).health -= e.amount;
});
gameplay::ApplyDamage(world, attacker, enemy, 25.0f, gameplay::DamageType::Physical);
ApplyDamage publishes a gameplay::DamageEvent (source, target, amount, type, hit position and normal) on the EventBus. The character system publishes a gameplay::DeathEvent when a character's health reaches zero. gameplay::MakePlayerControllerSystem() with a PlayerController component gives a simple controller driven by the MoveForward, MoveRight, Jump, and Look input bindings.
16.2 Inventory
#include <motaware/gameplay/inventory.h>
gameplay::InventoryComponent inv;
inv.slotCount = 16;
gameplay::Inventory::AddItem(inv, healthPotionID, 5); // returns the count added
bool hasKey = gameplay::Inventory::HasItem(inv, keyItemID);
world.AddComponent<gameplay::InventoryComponent>(player, inv);
16.3 Quests
#include <motaware/gameplay/quest.h>
gameplay::Quest quest;
std::snprintf(quest.name, sizeof(quest.name), "Find the Sword");
gameplay::QuestObjective obj;
std::snprintf(obj.description, sizeof(obj.description), "Collect the ancient sword");
obj.type = gameplay::ObjectiveType::Collect;
obj.targetCount = 1;
quest.objectives.push_back(obj);
auto& log = gameplay::QuestLog::Instance();
log.AddQuest(quest);
log.UpdateObjective(questID, objectiveID, 1); // fires QuestProgressEvent / QuestCompleteEvent
16.4 Prefabs
#include <motaware/gameplay/prefab.h>
auto& prefabs = gameplay::PrefabRegistry::Instance();
gameplay::PrefabID id = prefabs.RegisterPrefab(
gameplay::SaveEntityAsPrefab(world, templateEntity, "Crate"));
EntityID crate = prefabs.Instantiate(world, id, Vec3(10, 0, 5));
16.5 Save and load
The engine points the save system at Saved/SaveGames and installs a full component serializer:
#include <motaware/gameplay/save_system.h>
auto& saves = gameplay::SaveSystem::Instance();
saves.SaveGame(world, "QuickSave");
saves.LoadGame(world, "QuickSave");
saves.DeleteSave("QuickSave");
The save, load, and saves console commands and the built-in save/load screen use the same slots.
16.6 Abilities
#include <motaware/gameplay/ability_system.h>
gameplay::AbilitySystemComponent asc; // side storage: holds strings and callbacks
asc.AddAttribute("Health", 100.0f, 0.0f, 200.0f);
asc.AddAttribute("Mana", 50.0f, 0.0f, 100.0f);
gameplay::AbilitySpec fireball;
fireball.name = "Fireball";
fireball.abilityID = 1;
fireball.cooldownDuration = 2.0f;
fireball.cost = 10.0f;
fireball.costAttribute = "Mana";
fireball.onActivate = [](EntityID caster) { /* spawn the projectile */ };
asc.GrantAbility(fireball);
asc.TryActivateAbility(1, player);
asc.Update(player, dt); // ticks cooldowns and active effects
Abilities support gameplay tags (required, blocked-by, granted), explicit commit, and cancellation. gameplay_effect.h (GameplayEffectManager) applies timed and stacking attribute modifiers.
16.7 Dialogue and fog of war
#include <motaware/gameplay/dialogue.h>
#include <motaware/gameplay/fog_of_war.h>
gameplay::DialogueTree tree;
gameplay::DialogueNode greeting;
greeting.speaker = "Merchant";
greeting.text = "Want to trade?";
greeting.choices.push_back({.text = "Yes", .targetNodeID = 1});
greeting.choices.push_back({.text = "No", .targetNodeID = 2});
tree.AddNode(greeting);
gameplay::DialogueRunner runner;
runner.Start(tree);
runner.SelectChoice(0);
gameplay::FogOfWar fow;
fow.Initialize(128, 128, 1.0f); // cells, cell size
fow.RevealCircle(Vec2(px, pz), visionRadius, team);
f32 vis = fow.GetVisibility(Vec2(tx, tz), team); // 0 hidden, 0.5 explored, 1 visible
16.8 Other gameplay systems
Header (<motaware/gameplay/...>) | Provides |
|---|---|
game_mode.h | GameMode, GameState, GameModeManager |
projectile.h, interaction.h, spawn.h | Projectiles, interactables, spawn points (projectile and interaction systems are registered by the engine) |
state_tree.h | StateTree hierarchical state machines |
smart_object.h | SmartObjectRegistry: claimable world interaction slots |
mass_entity.h | Mass-style processors for large crowds |
achievements.h | AchievementManager, stat counters |
sequencer.h, sequence_player.h | Cinematic sequences (seq.* console commands) |
replay.h | Replay recording |
common_ui.h | Activatable widget stacks in priority layers, with input routed to the top one |
17. Data, Localization, and Files
17.1 Data tables
At boot, the engine loads every CSV in Data/Tables/ (next to the executable) into core::DataTableRegistry, keyed by file name. The first row is the header; the first column is the row key.
#include <motaware/core/data_table.h>
if (const core::DataTable* weapons = core::DataTableRegistry::Instance().Find("weapons")) {
if (const core::DataRow* rifle = weapons->FindRow("rifle")) {
f32 damage = rifle->GetFloat("damage");
bool automatic = rifle->GetBool("automatic");
}
}
Lua reads the same tables with mota.dataTableGetFloat and friends; the datatable console command inspects them.
17.2 Localization
The engine's own strings live in a core::StringTable registered with core::LocaleSystem. Register your game's tables the same way:
#include <motaware/core/localization/string_table.h>
m_strings.SetName("MyGame"); // m_strings: core::StringTable member
m_strings.ImportCSV(csvText); // header: key,<locale>,<locale>,...
core::LocaleSystem::Instance().RegisterTable(&m_strings);
std::string play = core::LocaleSystem::Instance().Lookup("menu.play"); // the key itself if missing
The active locale is en-US unless --locale=<tag> is given (or LocaleSystem::SetActiveLocale is called). <motaware/core/localization/> also has locale-aware number and date formatting, bidirectional text, and font fallback.
17.3 Files and paks
Reads should go through the virtual file system, which looks in mounted paks first and then in loose files. The engine mounts the game's pak when launched with --pak=<file> (the packaged run.bat does this).
#include <motaware/asset/pak_file.h>
auto bytes = asset::VirtualFileSystem::Instance().ReadFile("assets/config/waves.json");
if (bytes) { /* std::vector<u8> */ }
Write save data and other runtime files under ctx.savedDirectory, never next to the executable. Downloadable content paks can be mounted at runtime (dlc.mount, dlc.list console commands).
18. Packaging and Asset Cooking
18.1 Packaging a game
mota package builds the Shipping configuration and runs the game executable with --package=<project>. The result is a self-contained folder dist/<Game>/:
| File | Contents |
|---|---|
<Game>.exe + runtime DLLs | The game executable |
<Game>.pak | All cooked assets, LZ4-compressed and signed |
run.bat | Launches --game --project=... --pak=<Game>.pak (double-clicking the exe also works) |
THIRD_PARTY_NOTICES.md | Attributions for the engine's third-party components |
MOTAENGINE_LICENSE.md | The engine license |
The two legal files are embedded in the engine at build time, and a package that cannot write them fails. Your shipped game must carry them. Source files, build output, and docs never go into the pak. --out=<dir> and --exe-name=<name> adjust the output.
18.2 Texture cooking
Packaging (or --cook=<dir> with --cook-out=<dir>, a headless cook without packaging) writes block-compressed siblings next to every source texture:
| Sibling | Format | Used by |
|---|---|---|
<tex>.dds | BC7 with pre-built mips | Desktop GPUs |
<tex>.astc.ktx2 | ASTC 6x6 (4x4 for normal maps) | Mobile GPUs |
<tex>.etc2.ktx2 | ETC2 | Older Android devices (opt-in) |
--cook-textures=<list> picks the families: a comma list of bc7, astc, etc2, or all, none, default (default bc7,astc). At runtime Renderer::LoadTexture picks the first format the GPU supports and falls back to the source image, so game code always loads the source path. Normal maps are detected by file name (_n, _nrm, _norm, _normal). Cooking runs on a Windows host; the runtime reads the cooked formats on every platform.
18.3 Model cooking
Models are cooked to a binary runtime format (<model>.mmesh) so shipped builds load them without Assimp. rhi::LoadModel uses the cooked form automatically when it is present.
19. Configuration and the Console
19.1 Console variables
#include <motaware/core/config/cvar.h>
static core::CVar<f32> s_moveSpeed("player.moveSpeed", 5.0f, "Player movement speed");
static core::CVar<bool> s_godMode("player.god", false, "Invulnerability", core::CVarFlags::Cheat);
f32 speed = s_moveSpeed.Get();
s_moveSpeed.Set(10.0f);
s_moveSpeed.OnChanged([](const f32& oldVal, const f32& newVal) {
MOTA_LOG_INFO(Gameplay, "moveSpeed {} -> {}", oldVal, newVal);
});
CVars support i32, f32, bool, and std::string. Flags: ReadOnly, Cheat, RequiresRestart. In the console, set <name> <value>, get <name>, and list_cvars work on every registered CVar.
19.2 engine.ini and the command line
At boot the engine reads Config/engine.ini next to the executable. Each Key=Value under [Section] sets the CVar named Section.Key:
[player]
moveSpeed=7.5
god=false
After that, --<cvar name>=<value> on the command line overrides it, for example MyGame.exe --game --project=MyGame.motaproj --player.moveSpeed=10.
19.3 Console commands
#include <motaware/core/console/console_command.h>
auto& console = core::ConsoleSystem::Instance();
console.RegisterCommand("spawn_wave", "Spawn the next enemy wave",
[this](const std::vector<std::string>& args) -> std::string {
SpawnWave(args.empty() ? 1 : std::stoi(args[0]));
return "wave spawned";
});
console.Execute("spawn_wave 3");
Built-in commands include help, quit, set, get, list_cvars, echo, stat.fps, stat.memory, stat.physics, stat.entities, save, load, saves, net.status, datatable, and the r.* render settings.
19.4 Command-line summary
| Flag | Meaning |
|---|---|
--project=<file.motaproj> | Open a project and load its game module |
--game | Play the game (fullscreen, front-end screens, HUD) |
--pak=<file.pak> | Mount a pak before the project opens |
--headless | No window, GPU, or UI; simulation only |
--server | Dedicated server; implies --headless |
--port=N, --connect=<host[:port]> | Listen port / join a server |
--tickrate=N, --workers=N | Headless simulation rate / job worker count |
--health-port=N, --health-bind=ADDR | HTTP health endpoint |
--frames=N, --exec="cmd1;cmd2" | Auto-quit / run console commands at startup |
--locale=<tag> | Active locale |
--screenshot=<file.png> | Save the last frame of a --frames run as PNG (windowed only) |
--package=<proj>, --cook=<dir>, --cook-textures=<list> | Packaging and cooking |
The complete list, including networking test flags, is in sdk-reference.md.
20. Debugging and Profiling
20.1 Developer console
Press the backtick key (`) in a windowed game to open the developer console. It runs every registered console command and CVar.
20.2 Debug lines
Implement CollectDebugLines in your module to draw lines over the scene each frame:
void CollectDebugLines(std::vector<app::DebugLine>& out) override {
out.push_back({Vec3(0, 0, 0), Vec3(0, 5, 0), Vec4(1, 0, 0, 1)}); // start, end, RGBA
}
20.3 CPU and GPU profiling
#include <motaware/core/diagnostics/profiler.h>
void UpdateAI() {
MOTA_PROFILE_FUNCTION();
{
MOTA_PROFILE_ZONE("Pathfinding");
// ...
}
}
Zones compile out of Shipping builds. core::Profiler::Instance() holds the recorded zones and frame history, and SaveTraceToFile writes them to disk. r.profilegpu prints per-pass GPU timings; stat.fps and stat.memory give quick numbers. On a server, --net-profile with the net.profile command times the UDP path.
20.4 Crash reports
The engine installs a crash handler at boot that writes crash dumps to Saved/Crashes/. crash.list lists them.
20.5 Headless test runs
--headless --frames=N boots the full engine and your module without a GPU and exits with a status code, which makes it a good smoke test in CI. Combine it with --exec to run console commands.
21. Building the Engine from Source
This section applies to a source checkout. All Windows commands run through build_helper.bat from the engine root.
21.1 Configurations and presets
| Configuration | Build preset | Define | Use |
|---|---|---|---|
| Debug | windows-debug | MOTA_DEBUG | Full debug info, assertions, verbose logging |
| Development | windows-development | MOTA_DEVELOPMENT | Optimized with debug info |
| Shipping | windows-shipping | MOTA_SHIPPING | Fully optimized; Verbose/Debug logs and profiler zones compiled out |
All three share the windows-default configure preset (Ninja Multi-Config, output in build/). windows-ship configures a separate build-ship/ tree with the editor and tests turned off.
build_helper.bat cmake --preset windows-default
build_helper.bat cmake --build --preset windows-development
build_helper.bat build\engine\app\Debug\MotaEngine.exe --headless --frames=120
21.2 Tests
build_helper.bat ctest --preset windows-debug --output-on-failure
The suite has about 3,600 Catch2 test cases, plus smoke tests that boot the real MotaEngine and MotaServer binaries headless (no GPU required).
21.3 Linux
export VCPKG_ROOT=$HOME/vcpkg
cmake --preset linux-server # first run builds the vcpkg ports
cmake --build --preset linux-server-debug
ctest --preset linux-server-debug -L smoke
cmake --build --preset linux-server-shipping # -O3 + LTO
Build on a native Linux file system; building on a mounted Windows drive (for example /mnt/e under WSL2) is much slower.
21.4 Dependencies
Installed through vcpkg from vcpkg.json:
| Library | Purpose |
|---|---|
| SDL3 | Windowing, input, Vulkan surface |
| spdlog | Logging |
| GLM | Math |
| nlohmann-json | JSON |
| Jolt Physics | Physics |
| LuaJIT | Scripting |
| stb | Image loading |
| Assimp | Model import (cook time and development) |
| LZ4 | Pak compression |
| libhydrogen | UDP encryption |
| cpp-httplib | Health endpoint, crash upload, online services |
| libdatachannel | WebRTC transport |
| DirectXTex, KTX | Texture cooking (Windows host only) |
| Catch2 | Tests |
miniaudio is bundled in the source. Licenses for every component are in THIRD_PARTY_NOTICES.md.
21.5 Engine source layout
| Directory | Module |
|---|---|
engine/pal | Platform abstraction (SDL3, Win32, POSIX) |
engine/core | ECS, reflection, jobs, events, memory, logging, serialization, config, localization |
engine/input | Input |
engine/physics | Jolt integration and simulators |
engine/rhi | Vulkan renderer |
engine/audio | Audio |
engine/animation | Animation |
engine/scripting | Lua scripting |
engine/asset | Paks, VFS, cook pipeline |
engine/ai | AI |
engine/networking | Networking |
engine/ui | UI framework |
engine/gameplay | Gameplay framework |
engine/runtime_screens | Runtime screens |
engine/app | Engine lifecycle, main loop, packager, health endpoint |
22. Module and Header Reference
Public headers live under <motaware/<module>/>. The main types per module:
| Module | Include path | Key types |
|---|---|---|
| App | <motaware/app/> | IGameModule, GameModuleContext, GameModuleRegistry, RunEngineMain, DebugLine |
| PAL | <motaware/pal/> | Window, Timer, FileSystem, EntityID, fixed-width types |
| ECS | <motaware/core/ecs/> | World, Query, All, None, SystemDescriptor, SystemPhase, hierarchy::*, WorldTransform |
| Events | <motaware/core/events/> | EventBus |
| Memory | <motaware/core/memory/> | FrameAllocator, LinearAllocator, PoolAllocator, ObjectPool |
| Math | <motaware/core/math/> | Vec3, Mat4, Quat, Transform, splines, frustum |
| Jobs | <motaware/core/job/> | JobSystem |
| Config | <motaware/core/config/> | CVar, CVarSystem, ConfigFile |
| Console | <motaware/core/console/> | ConsoleSystem |
| Diagnostics | <motaware/core/diagnostics/> | Profiler, CrashReporter |
| Data | <motaware/core/> | DataTable, DataTableRegistry (data_table.h) |
| Localization | <motaware/core/localization/> | StringTable, LocaleSystem |
| Serialization | <motaware/core/serialization/> | BinaryWriter, BinaryReader |
| Input | <motaware/input/> | InputSystem, InputContext, ActionBinding, AxisBinding, EnhancedInputSystem |
| Physics | <motaware/physics/> | PhysicsWorld, RigidBody, CollisionShapeDesc, ConstraintDesc, ClothSimulator, RopeSimulator, SoftBodySimulator, VehicleManager, CharacterControllerManager, RagdollManager, DestructibleComponent |
| Rendering | <motaware/rhi/> | Renderer, MeshComponent, MeshData, MaterialParams, DirectionalLight, PointLight, SpotLight, AreaLightComponent, CameraComponent, WeatherSystem, ParticleManager, VFXManager, DecalManager, LoadModel |
| Audio | <motaware/audio/> | AudioEngine, AudioSource, AudioListener, MusicSystem, AudioGraph |
| Animation | <motaware/anim/> | Skeleton, AnimationClip, AnimationStateMachine, AnimationRuntime, Animator, BlendSpace2D, MontagePlayer, MotionMatcher |
| Scripting | <motaware/scripting/> | ScriptEngine, ScriptComponent |
| AI | <motaware/ai/> | BehaviorTree, Blackboard, GridNavMesh, NavMesh, GOAPPlanner, CrowdManager, UtilityEvaluator, PerceptionComponent |
| Networking | <motaware/net/> | NetworkManager, ReplicationManager, ReplicatedComponent, RPCManager, Packet, LagCompensation, ClientPrediction, SnapshotInterpolator, ReplicationGraph |
| Assets | <motaware/asset/> | VirtualFileSystem, PakFileReader, PakFileWriter |
| UI | <motaware/ui/> | Widget, PanelWidget, ButtonWidget, UIDrawContext |
| Screens | <motaware/screens/> | ScreenManager, Screen, ScreenID |
| Gameplay | <motaware/gameplay/> | CharacterComponent, PlayerController, InventoryComponent, QuestLog, PrefabRegistry, SaveSystem, AbilitySystemComponent, GameplayEffectManager, DialogueTree, FogOfWar, CameraManager, GameModeManager, StateTree |
For game-project details see sdk-reference.md; for scripting see lua-api.md.
