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
Enginelibrary owns the application loop, the ECS, the built-in systems, assets, rendering, input, audio, networking, and Lua. TheSerializationlibrary owns scene and prefab files. - Lucky is the editor.
RTypeEditoris an ImGui application that creates a normalEngine::Application, points itscustomRenderandonUIcallbacks 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:
- an absolute path that exists is used as-is;
- otherwise the path is joined onto the project folder;
- if that file is missing, the project folder plus the file name is tried;
- 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- worldx,y, draw layerz, previous-frame values, amovedflag, and arelativeflag 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
IPluginEditorper 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:
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.
MultiplayerPluginregistersPlayerInformation,PacketRegistryResource,PacketRouterResource,PlayerInputState,PlayerSlotState, andMultiplayerSystem. It starts a host from--server [--port N]or--client [--host H] [--port N]. With no flag, the game stays single-player.ReplicationPluginregistersNetworkComponentandReplicationSystem.
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:
- World pass - inside the active camera.
- 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- drawsSpriteComponentatPositionplus the sprite offset, applying rotation, scale, flip, and effective opacity.ShapeSystem- draws rectangles, circles, polygons, and lines, filled or outlined, with rotation.TextSystem- drawsTextwith 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, andPlayerSlotState- multiplayer state.PrefabSpawner- runtime prefab hook installed by serialization.State<T>,NextState<T>, andStateTransitionEvent<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:
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:
Parentlinks point to the new parent IDs.SignalConnectionstargets point to the new target IDs.CameraComponent::followTargetpoints to the new entity.Positionprevious-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 themovedflag.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>- appliesNextState<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.