MotaEngine

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

  1. Introduction
  2. Getting the Engine
  3. Your First Game Project
  4. Core Concepts
  5. Entity Component System
  6. Input
  7. Physics
  8. Rendering
  9. Audio
  10. Animation
  11. Scripting with Lua
  12. AI
  13. Networking
  14. Headless Mode and Dedicated Servers
  15. UI and Screens
  16. Gameplay Framework
  17. Data, Localization, and Files
  18. Packaging and Asset Cooking
  19. Configuration and the Console
  20. Debugging and Profiling
  21. Building the Engine from Source
  22. Module and Header Reference

1. Introduction

1.1 Supported platforms

TargetStatus
Windows 10/11 x64 game clientSupported (Vulkan 1.3 GPU required)
Windows x64 headless / dedicated serverSupported
Linux x64 dedicated server (Ubuntu 24.04, Clang)Supported (headless only, no GPU needed)
Android, iOSExperimental; not part of this release's support scope
macOS, Linux game clientNot 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 is mota.
  • 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 SDKSource checkout
What you getHeaders, static libraries (Debug, Development, Shipping), the dependency tree, the mota CLI, a sample game, docsThe full engine source tree
First buildConfigures in seconds, links in about a minuteBuilds all vcpkg dependencies and the engine once (can take tens of minutes)
Modify the engineNoYes
Mobile packagingNoYes (experimental)

2.1 Requirements

ComponentRequirement
OSWindows 10 or 11, x64
CompilerVisual Studio 2026 (version 18) or later with the "Desktop development with C++" workload. Visual Studio 2022 is not supported
CMake3.25 or later
Vulkan SDK1.3 or later, from vulkan.lunarg.com
vcpkgSource checkout only. Visual Studio includes one; set VCPKG_ROOT to it
GPUVulkan 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\
CommandWhat 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 packageShipping 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:

FieldNull when
worldnever
renderer--headless and --server (no GPU)
physicsWorldphysics failed to initialize
screenManager--headless
audioEngine, musicSystem--server
particleManager, vfxManager, decalManager, clothSimulator, ropeSimulator--headless
weatherSystem, cameraManagernever
scriptEngineLua failed to initialize (it runs every ScriptComponent script; see section 11)
networkManager, replicationManager, rpcManagerthe process has no network role (no --server or --connect)
savedDirectorynever empty in a running game: the writable Saved/ directory for profiles and save games
requestQuitcallable 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, identity Quat(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

HeaderMain typesPurpose
physics/vehicle.hVehicleComponent, WheelConfig, VehicleManagerWheeled vehicles: engine torque, steering, driven wheels (VehicleManager::SetInput(index, forward, right, brake, handBrake))
physics/character_controller.hCharacterControllerComponent, CharacterControllerManagerCapsule character movement
physics/ragdoll.hRagdollComponent, RagdollManagerRagdolls driven by constraints
physics/fracture.hDestructibleComponent, FracturePatternBreak an object into fragments when its health reaches zero
physics/soft_body.hSoftBodySimulatorMass-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:

CommandControls
r.vsync, r.renderScale, r.upscalepresentation and resolution scaling
r.taa, r.fxaaanti-aliasing
r.bloom, r.bloom.intensity, r.exposure, r.tonemap, r.vignettepost-processing
r.grade.contrast, r.grade.saturationcolor grading
r.ssr, r.fog, r.fog.density, r.fog.godrays, r.clouds, r.aerialreflections, fog, sky
r.shadow.cascades, r.vsmshadows
r.lumen.global, r.ddgi, r.rt.shadows, r.rt.reflectionsglobal illumination and ray tracing (where supported)
r.sun.elevation, r.sun.azimuthsun direction
r.wireframe, r.profilegpudebugging

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):

RouteReturns
GET /health200 while the main loop is healthy, 503 when stalled, not listening, or draining at shutdown
GET /statusJSON: engine, game, loop timing, network, and memory
GET /metricsPrometheus 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.hGameMode, GameState, GameModeManager
projectile.h, interaction.h, spawn.hProjectiles, interactables, spawn points (projectile and interaction systems are registered by the engine)
state_tree.hStateTree hierarchical state machines
smart_object.hSmartObjectRegistry: claimable world interaction slots
mass_entity.hMass-style processors for large crowds
achievements.hAchievementManager, stat counters
sequencer.h, sequence_player.hCinematic sequences (seq.* console commands)
replay.hReplay recording
common_ui.hActivatable 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>/:

FileContents
<Game>.exe + runtime DLLsThe game executable
<Game>.pakAll cooked assets, LZ4-compressed and signed
run.batLaunches --game --project=... --pak=<Game>.pak (double-clicking the exe also works)
THIRD_PARTY_NOTICES.mdAttributions for the engine's third-party components
MOTAENGINE_LICENSE.mdThe 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:

SiblingFormatUsed by
<tex>.ddsBC7 with pre-built mipsDesktop GPUs
<tex>.astc.ktx2ASTC 6x6 (4x4 for normal maps)Mobile GPUs
<tex>.etc2.ktx2ETC2Older 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

FlagMeaning
--project=<file.motaproj>Open a project and load its game module
--gamePlay the game (fullscreen, front-end screens, HUD)
--pak=<file.pak>Mount a pak before the project opens
--headlessNo window, GPU, or UI; simulation only
--serverDedicated server; implies --headless
--port=N, --connect=<host[:port]>Listen port / join a server
--tickrate=N, --workers=NHeadless simulation rate / job worker count
--health-port=N, --health-bind=ADDRHTTP 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

ConfigurationBuild presetDefineUse
Debugwindows-debugMOTA_DEBUGFull debug info, assertions, verbose logging
Developmentwindows-developmentMOTA_DEVELOPMENTOptimized with debug info
Shippingwindows-shippingMOTA_SHIPPINGFully 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:

LibraryPurpose
SDL3Windowing, input, Vulkan surface
spdlogLogging
GLMMath
nlohmann-jsonJSON
Jolt PhysicsPhysics
LuaJITScripting
stbImage loading
AssimpModel import (cook time and development)
LZ4Pak compression
libhydrogenUDP encryption
cpp-httplibHealth endpoint, crash upload, online services
libdatachannelWebRTC transport
DirectXTex, KTXTexture cooking (Windows host only)
Catch2Tests

miniaudio is bundled in the source. Licenses for every component are in THIRD_PARTY_NOTICES.md.

21.5 Engine source layout

DirectoryModule
engine/palPlatform abstraction (SDL3, Win32, POSIX)
engine/coreECS, reflection, jobs, events, memory, logging, serialization, config, localization
engine/inputInput
engine/physicsJolt integration and simulators
engine/rhiVulkan renderer
engine/audioAudio
engine/animationAnimation
engine/scriptingLua scripting
engine/assetPaks, VFS, cook pipeline
engine/aiAI
engine/networkingNetworking
engine/uiUI framework
engine/gameplayGameplay framework
engine/runtime_screensRuntime screens
engine/appEngine lifecycle, main loop, packager, health endpoint

22. Module and Header Reference

Public headers live under <motaware/<module>/>. The main types per module:

ModuleInclude pathKey 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.