Skip to content

Core concepts

These are the terms the rest of the documentation uses. The sections are alphabetical, so you can jump straight to a word when a guide uses one you do not know yet.

Architecture

The project is split into two products that share one runtime.

  • Pozzo is the engine. The Engine library owns the application loop, the ECS, the built-in systems, assets, rendering, input, audio, networking, and Lua. The Serialization library owns scene and prefab files.
  • Lucky is the editor. RTypeEditor is an ImGui application that creates a normal Engine::Application, points its customRender and onUI callbacks at the editor panels, and edits and renders the application registry rather than handing it to a separate game process.

The editor marks its registry with an EditorMode resource. ScriptSystem checks for that resource and stays idle, which is why scripts only run when you press Play and a separate game process starts.

A game generated by the editor has the opposite shape: a small main.cpp creates Engine::Application, initializes the engine plugins, loads the scene with Serialization::SceneSerializer, and calls app.Run(). There is no editor layer in that executable.

Plugin initialization order is the system update order. The generated game and the editor both register HierarchyPlugin before PositionPlugin, because parent motion must be propagated before PositionSystem records the previous-frame position.

Application::Run() uses a fixed simulation step:

PreUpdate          input polling, virtual-canvas scaling, mouse mapping
while lag >= dt    Update(dt) for every registered system
PostUpdate         rendering (world pass, then screen pass)
FlushQueues        deferred entity destruction

The engine renders into a fixed-size virtual canvas, then scales that canvas into the window with letterboxing. See the application overview and the contributor notes on engine architecture and editor architecture.

Assets

Assets are files on disk: textures, sounds, music, and fonts. They are not the same thing as resources. A resource is an in-memory singleton stored in the registry; an asset is loaded into an AssetManager, which is itself a resource.

AssetManager keeps aliases for loaded assets:

auto& assets = registry.GetResource<Engine::AssetManager>();
assets.LoadSprite("player", "assets/player.png");
auto sprite = assets.GetSprite("player");

The same pattern exists for sounds, music, and fonts. AudioPlugin creates the AssetManager resource if it is missing, so games that enable audio can load their files through one place.

Paths stored in scenes and prefabs are normally project-relative. The registry holds a ProjectContext with the local project folder, and ResolveAssetPath turns a stored path into a loadable one:

  1. an absolute path that exists is used as-is;
  2. otherwise the path is joined onto the project folder;
  3. if that file is missing, the project folder plus the file name is tried;
  4. otherwise the original path is returned so the caller can report it.

ToProjectRelativePath performs the reverse operation when the editor assigns an asset or script path. This is why a project can move between machines without rewriting every sprite path. See Assets.

Audio

Audio is data-driven. AudioSource is a component with an assetId (the alias used by AssetManager), an isMusic flag, the trigger flags shouldPlay and shouldStop, the current isPlaying state, and loop and volume fields.

AudioPlugin creates the AssetManager, calls InitAudioDevice, registers the component, and registers AudioSystem. AudioSystem updates playing music streams, reacts to shouldPlay by playing a sound or music asset, reacts to shouldStop, and removes finished sound effects from its active list. It only visits entities inserted into its m_Entities set; adding an AudioSource component is not enough by itself. The sound example inserts the entity explicitly after creating it.

Because the component is plain data, a scene can describe an emitter without containing any audio code. Gameplay code or Lua only flips the trigger flags. See Audio.

Collisions

A collider is a ColliderComponent. It can carry its own optional ShapeComponent; when it does not, the shape on the same entity is used. The component also has enabled, isTrigger, isStatic, a layer, and a bitmask mask.

Two colliders interact only when both masks accept the other collider's layer:

static bool CanCollide(const ColliderComponent& a, const ColliderComponent& b) {
    return a.Accepts(b) && b.Accepts(a);
}

There are 32 usable layers. A single restrictive mask is enough to silence the whole pair: detection, resolution, and enter/stay/exit events all use the same filter.

CollisionSystem turns each shape into vertices with ShapeComponent::GetShapeVectors(rotation, position) and runs a separating-axis test on the two outlines. For solid, non-line shapes it also computes the minimum translation vector and pushes the bodies apart. Static colliders do not move; when both are static, no resolution is applied. Triggers report contacts but skip resolution, and line shapes are skipped during resolution.

Each contact fires callbacks from CollisionEvents and Godot-style signals from SignalConnections on both entities: onCollisionEnter, onCollisionStay, onCollisionExit, plus the onTrigger* variants when either side is a trigger. Callbacks must not destroy entities synchronously; use Registry::QueueDestroyEntity so destruction happens at the end of the frame. See Collisions.

Components

Components are plain data structs in Engine/include/Components. They contain no game logic. The registry stores one ComponentArray<T> per registered type, and each array is an unordered_map<Entity, T>. Access goes through templates:

registry.RegisterComponent<Engine::Name>();
Engine::Entity player = registry.CreateEntity();
registry.AddComponent(player, Engine::Name::Create("Player"));
registry.GetComponent<Engine::Position>(player).x = 320.0f;

Registry::CreateEntity() always adds a Position(0, 0) and a Rotation(0) before returning the new ID. Other components are added by plugins, the editor, or gameplay code. Asking for a component type that was never registered throws an ErrorEcs; asking for a component the entity does not own also fails, so check with HasComponent<T>() first.

The built-in components are:

  • Position - world x, y, draw layer z, previous-frame values, a moved flag, and a relative flag used by hierarchies.
  • Rotation - angle in degrees.
  • Parent - parent entity ID, used for hierarchies and subtree destruction.
  • ScreenSpace - tag that moves rendering from the world pass to the HUD pass.
  • Name - optional human-readable name.
  • SpriteComponent, ShapeComponent, Text - renderable data.
  • ColliderComponent, CollisionEvents, SignalConnections - collision and signal data.
  • CameraComponent - camera zoom, follow target, shake, and bounds.
  • Input - player index and active flag for a controllable entity.
  • AudioSource - audio trigger data.
  • NetworkComponent - replication metadata.
  • Opacity - alpha multiplier with optional inheritance to children.
  • PrefabInstance - marks the root of a prefab subtree.

See Components and the component reference.

Editor

Lucky, the editor, is project-based. On launch it shows a gallery of projects stored in projects.json; choosing one opens that project's test.scene. The CLI can create projects without the gallery:

./build/RTypeEditor new MyGame
./build/RTypeEditor create system MySystem --path MyGame
./build/RTypeEditor create plugin MyFeature --path MyGame

A new project gets test.scene, an assets/ folder, and input.json. The scene records the project folder, window settings, build names, multiplayer settings, and the input map.

The editor is an ImGui docking layout. The main panels are:

  • Hierarchy - the entity tree, including parent/child structure.
  • Inspector - component editing, driven by one IPluginEditor per component type.
  • Scene - the live scene view, editor camera, selection, and move/rotate gizmos.
  • Assets - files in the project folder.
  • Console - engine and editor logs.
  • Connections - signal connections between entities.
  • Preview - isolated previews for assets and prefabs.
  • Input Map - action-to-key bindings stored in the scene.
  • Settings - scene, window, and build settings.
  • State - state groups used by the state tools.

File > Save Scene writes test.scene in the project folder. The play button saves first, finds the generated game executable, and launches it from the project folder. The hammer button generates main.cpp and a temporary CMake file, builds the game, and copies the executable back into the project folder. The multiplayer play button launches a --server process and the configured number of --client processes.

The editor registry is not a running game: it has EditorMode, so ScriptSystem does nothing there. Use Play when you need scripts, collisions, networking, or the real fixed-step loop. See the editor overview and interface.

Engine

Pozzo is a C++ library. Engine::Application owns a Registry, creates the window and virtual canvas, runs the fixed-step loop, and renders through raylib. ApplicationConfig controls the window size, virtual canvas size, title, target FPS, command-line arguments, callbacks, and headless mode.

A minimal embedding looks like this:

Engine::ApplicationConfig config;
config.windowTitle = "My Game";
Engine::Application app(config);
Engine::PositionPlugin position;
position.Init(app.GetRegistry());
app.Run();

GetRegistry() gives access to the ECS, Quit() stops the loop, and GetGameTexture() exposes the virtual canvas texture for custom rendering. headless = true skips the window and rendering while still ticking systems and networking; this is what the editor's headless server option uses.

Command-line arguments are stored in the LaunchOptions resource and published to Lua as the global arg, so scripts can branch on flags such as --server and --client.

The engine does not depend on the editor or on Serialization at runtime. Serialization depends on Engine, and the editor links both. See the engine overview and application.

Entities

An entity is an ID, not an object:

using Entity = std::size_t;

CreateEntity() returns IDs starting at 1 and never reuses them. Destroying an entity removes all of its components, but the numeric ID is not recycled. IsEntityAlive() checks whether any component is still attached.

New entities start with Position and Rotation. A Name is optional; when it is missing, GetEntityName() falls back to Entity <id>.

Entities can form a hierarchy through Parent. The child keeps its world position in Position; Parent only stores the link. When the child's Position::relative flag is set, HierarchySystem applies the parent's frame-to-frame delta to the child. Destroying a parent destroys its whole subtree.

Because systems often iterate component arrays, destroying an entity in the middle of a callback can invalidate the iteration. Use QueueDestroyEntity() during collision or signal callbacks and let FlushQueues() run at the end of the frame. See Registry and entities in the editor.

Input

Input is action-based. InputManager is a registry resource that maps a player ID to an IController. InputPlugin registers the Input component and creates the resource. The concrete Controller polls the keyboard, mouse, and gamepad; InputManager::Update() runs once per frame in Application::PreUpdate.

A Keybind maps a key or button to a string action:

auto& input = registry.GetResource<Engine::InputManager>();
if (auto* controller = input.GetController(1);
    controller != nullptr && controller->isActionActive("SHOOT")) {
    // fire
}

Each Keybind carries the key, an optional compose key, and a DeviceType (KEYBOARD, MOUSE, GAMEPAD, or TOUCH). The Input component marks which player controls an entity and whether that control is active.

The editor's Input Map panel writes action bindings into the scene configuration. The generated game applies them to player 1's controller. Lua can read the same actions through the Controller API. Mouse positions are mapped back into the virtual canvas, so gameplay code sees canvas coordinates rather than window pixels. See Input and input mapping.

Multiplayer

Multiplayer is split between a transport layer and a replication layer.

  • MultiplayerPlugin registers PlayerInformation, PacketRegistryResource, PacketRouterResource, PlayerInputState, PlayerSlotState, and MultiplayerSystem. It starts a host from --server [--port N] or --client [--host H] [--port N]. With no flag, the game stays single-player.
  • ReplicationPlugin registers NetworkComponent and ReplicationSystem.

NetworkHostResource wraps ENet. It creates a server or client, polls events, routes packets, and assigns network IDs. Packets are framed with a two-byte little-endian packet ID followed by a Glaze BEVE payload; CreatePacket can send a typed payload reliably or unreliably. Packet names are hashed deterministically so a Linux and a Windows build agree on IDs.

NetworkComponent makes an entity networked:

struct NetworkComponent {
    std::uint64_t networkId = 0;
    std::string authority = "Server"; // "Server", "Client", or "Shared"
    bool syncTransform = true;
    bool syncSprite = false;
    std::string prefabPath;
};

The authority model decides what happens at load and spawn:

  • Server entities already exist on every machine. The scene entity ID doubles as the network ID, so clients match their local copy without spawning.
  • Client entities are per-player templates. The server removes the template and spawns one prefab instance per connected client.
  • Shared entities are never replicated; every machine builds them from the same scene.

Runtime entities such as bullets call Net.replicate from Lua. The server assigns an ID, broadcasts a reliable spawn, then sends full state snapshots on the fixed simulation tick. Clients create the entity or prefab and apply position, rotation, and sprite data. There is no prediction or interpolation yet; the system is deliberately snapshot-based so it can be extended later.

The server supports up to five concurrent players. Each player receives a color slot from PlayerSlotState, and the slot travels with spawn and state messages. The server starts runtime network IDs at 1,000,000 so scene entities can keep their low IDs.

The editor's multiplayer play mode launches the server and the configured number of clients. See Networking and multiplayer in the editor.

Plugins

An engine plugin is a C++ class implementing Engine::IPlugin:

class MyPlugin : public Engine::IPlugin {
  public:
    void Init(Engine::Registry& registry) override {
        registry.RegisterComponent<MyComponent>();
        registry.RegisterSystem<MySystem>();
    }
};

Plugins are statically compiled and linked; they are not shared libraries loaded at runtime. A plugin usually bundles a component, a system, and any resource the pair needs. The built-in plugins include PositionPlugin, RotationPlugin, HierarchyPlugin, SpritePlugin, ShapePlugin, TextPlugin, ColliderPlugin, CameraPlugin, OpacityPlugin, SignalPlugin, InputPlugin, AudioPlugin, ScriptPlugin, ReplicationPlugin, and MultiplayerPlugin.

Initialization order matters because RegisterSystem appends to the update and render order. There is no dependency graph. HierarchyPlugin must run before PositionPlugin, for example.

The editor has a parallel plugin concept: an editor plugin implements Editor::IPluginEditor and tells the Inspector how to draw, add, and remove one component. PositionPluginEditor, SpritePluginEditor, and the others live in Editor/src/Plugins.

Project-local systems and plugins can be scaffolded with RTypeEditor create system and RTypeEditor create plugin. They are written under the project's system/ folder, collected in system/build.cmake, and registered through a generated rtype_scaffold_register.hpp header. See Plugins, engine plugins, and editor plugins.

Prefabs

A prefab is a reusable entity subtree saved as a .prefab file. The file uses the same EntitySnapshot format as a scene subtree, so it can contain the root, its children, and all reflected components.

The root carries PrefabInstance:

struct PrefabInstance {
    std::string prefabPath; // project-relative, e.g. "Prefabs/Player.prefab"
};

At runtime, serialization installs a PrefabSpawner resource when a scene is deserialized. Any system can then spawn a prefab without depending on the serialization library:

auto& spawner = registry.GetResource<Engine::PrefabSpawner>();
Engine::Entity bullet = spawner.Spawn(registry, "Prefabs/Bullet.prefab");

SpawnPrefabRuntime resolves the prefab path through ProjectContext, reads the snapshot, and spawns every entity with new IDs. It returns the new root, or (Entity)-1 on failure. Replication uses the same path when a NetworkComponent names a prefab for a client.

The editor's Preview panel can open a .prefab, edit it in an isolated registry, and write it back. Saving a prefab from the preview copies the sprite path, sprite scale, shape size, and script onto every instance; each instance keeps its own position. See Prefabs.

Rendering

Rendering is shared by the game loop and the editor scene view through Engine::RenderScene. It performs two passes:

  1. World pass - inside the active camera.
  2. Screen pass - in canvas pixels, for entities tagged with ScreenSpace (HUD, menus).

Within a pass, the engine collects every entity drawn by a LayeredRenderSystem and sorts draws by Position::z ascending, then by system registration order, then by entity ID. Higher z is drawn on top. After the layered draws, each system's Render() or RenderScreen() runs in registration order for systems that draw on their own.

The built-in renderers are:

  • SpriteSystem - draws SpriteComponent at Position plus the sprite offset, applying rotation, scale, flip, and effective opacity.
  • ShapeSystem - draws rectangles, circles, polygons, and lines, filled or outlined, with rotation.
  • TextSystem - draws Text with the default or a custom font.
  • CameraSystem - follows targets, decays shake, clamps to bounds, and exposes world-to-screen conversions.

A CameraComponent uses its entity's Position as the world point at the center of the viewport and its Rotation as the view angle. If several cameras are primary, the one with the lowest entity ID wins. If there is no primary camera, the world pass uses an identity camera.

VirtualResolution renders the world and HUD into an off-screen texture, then scales that texture uniformly into the window with letterbox or pillarbox bars. This keeps the game's visible area independent of the window size. See Rendering.

Resources

Resources are singleton objects stored in the registry next to entities and systems. They are type-erased and accessed by type:

auto& manager = registry.AddResource<Engine::AssetManager>();
Engine::AssetManager& same = registry.GetResource<Engine::AssetManager>();
bool present = registry.HasResource<Engine::AssetManager>();

GetResource<T>() throws ErrorEcs when the resource is missing. Resources are not serialized with the scene; they are recreated by plugins, the application, or scripts on each run.

Important built-in resources include:

  • VirtualResolution - the fixed game canvas and mouse mapping.
  • LaunchOptions - command-line arguments.
  • ProjectContext - the local project root used for asset resolution.
  • AssetManager - loaded sprites, sounds, music, and fonts.
  • InputManager - player ID to controller mapping.
  • NetworkHostResource, PacketRegistryResource, PacketRouterResource, PlayerInputState, and PlayerSlotState - multiplayer state.
  • PrefabSpawner - runtime prefab hook installed by serialization.
  • State<T>, NextState<T>, and StateTransitionEvent<T> - the state-machine pattern.

The editor adds editor-only resources such as EditorMode and EditorConfig, which gameplay code should not depend on. See Resources.

Scenes

A scene is a .scene file containing a SceneData value:

sceneName
config      -> SceneConfig
entities    -> SceneSnapshot

SceneConfig extends ApplicationConfig with the project folder, build executable name, build window title, initial scene path, multiplayer client count and headless-server flag, and the input map. SceneSnapshot is a list of EntitySnapshot values, one per entity, with each built-in component stored as an optional field.

SceneSerializer::Serialize captures the registry and writes the file. Deserialize restores the project context, optionally copies the scene configuration into the caller's config, spawns the entities with new IDs, remaps parent links, signal targets, and camera follow targets, and installs the runtime prefab spawner.

Saving a scene stores the project folder relative to the scene directory, so the project can move. When a config is passed, loading keeps the local project folder and initial scene path from the current machine rather than trusting the file. If the scene file is missing, deserialization starts with an empty scene and still registers a project context.

The editor always saves the open scene as test.scene in the project folder. The generated game loads the project's scene files after initializing the engine plugins. Scene loading does not initialize plugins or their component types; those must already be registered before deserialization. See Scenes and the scene format.

Scripting

Scripts are Lua tables attached to entities through ScriptComponent:

struct ScriptComponent {
    std::string scriptPath; // project-relative
    bool autoReload = true;
    bool loaded = false;
    sol::table scriptTable;
};

The script file must return a table. ScriptSystem creates one shared Lua state, opens the standard libraries, and registers the engine API on its first update. It then loads each script once and calls:

local Player = {}

function Player:onCreate(entity)
end

function Player:onUpdate(entity, dt)
end

return Player

onCreate runs when the script is loaded; onUpdate runs on every fixed simulation step. The module table is passed as self, so colon methods work. Lua errors are logged and do not stop the system.

The Lua API is split by plugin: Entity, Transform, Component, Input, Controller, Time, Net, and log/print. arg contains the command-line arguments. Scripts can create and queue-destroy entities, read and write components, check input actions, start a server or client, and send packets.

SignalConnections stores plain data, not Lua closures: a signal name, a target entity, and a method name. Dispatch resolves the target's script method at call time and invokes it as method(table, emitter, other). Because no closures are stored, connections survive scene saves, loads, and script reloads.

The editor registry has EditorMode, so ScriptSystem does nothing while you are editing. Press Play to run scripts in a real game process. See Scripting and the Lua API.

Serialization

Serialization is the mechanism behind scenes and prefabs. The project uses Glaze with two formats: JSON (the default, readable and diffable) and BEVE binary. SerializationFormat selects between them.

The serializer captures a fixed set of built-in components in an EntitySnapshot: name, position, rotation, sprite, shape, text, collider, opacity, signal connections, parent, input, audio source, camera, script, network, and prefab instance. Components not in that list are not written to a scene or prefab. Adding a new serializable component means adding it to the snapshot and the capture/spawn passes, not just registering it with the ECS.

On restore, SpawnEntities creates new entity IDs and remaps references inside the restored subtree:

  • Parent links point to the new parent IDs.
  • SignalConnections targets point to the new target IDs.
  • CameraComponent::followTarget points to the new entity.
  • Position previous-frame values are reset to the restored position so the first frame does not invent a delta.

References to entities outside the restored subtree are left unchanged. This is what makes pasting a subtree or loading a prefab preserve external connections.

Serialization also owns the project-relative path conversion used by assets and scripts. See the scene format.

Systems

A system is a class deriving from Engine::System:

class MySystem : public Engine::System {
  public:
    void Update(Engine::Registry& registry, float dt) override {
        auto positions = registry.GetComponentArray<Engine::Position>();
        for (auto& [entity, position] : *positions) {
            (void)entity;
            position.x += 60.0f * dt;
        }
    }
};

Register it with registry.RegisterSystem<MySystem>(). Systems are global: they are not attached to individual entities. A system decides for itself which entities it cares about, usually by iterating a component array and checking for the other components it needs.

System also provides Render() and RenderScreen() for systems that draw on their own, and an m_Entities set. Nothing fills that set automatically. Most built-in systems ignore it and query component arrays directly; TextPlugin inserts IDs into TextSystem::m_Entities, and CollisionSystem uses the set as an optional filter when it is non-empty.

Update receives the fixed timestep. It can run zero, one, or several times per rendered frame, depending on how far the accumulator has drifted. Use dt for movement and timers; do not assume one update per frame.

The built-in systems are:

  • HierarchySystem - propagates parent motion to relative children.
  • PositionSystem - snapshots previous positions and updates the moved flag.
  • SpriteSystem, ShapeSystem, TextSystem - layered rendering.
  • CameraSystem - follow, shake, bounds, and camera conversion.
  • CollisionSystem - SAT overlap tests, resolution, and contact events.
  • AudioSystem - consumes audio triggers and updates music streams.
  • ScriptSystem - loads and updates Lua scripts.
  • MultiplayerSystem - polls the network host each fixed tick.
  • ReplicationSystem - spawn, despawn, identity, input, and state snapshots.
  • StatesSystem<T> - applies NextState<T> transitions.

Systems that draw one entity at a time derive from LayeredRenderSystem instead of System and implement CanDraw and DrawEntity. That is what lets sprites, shapes, and text share one z-sorted draw list.

Use QueueDestroyEntity for destruction during a system update and let FlushQueues handle it after the frame. See Systems and the system reference.