Components are JSON data. Systems are C# scripts that run over them every frame. This page covers how to write both,
every function scripts can call, and the engine's built-in components. Press / to search.
You write components as JSON files and systems as C# scripts. The editor turns the JSON into C# types,
wires your systems into the engine and recompiles every time you save. You never write or see that plumbing.
You write
Where
What it is
Component
Components/Enemy.json
Data only: a list of named, typed fields. Becomes a C# struct called Enemy.
System
Scripts/EnemySystem.cs
A static class with [ECSSystem(id)]. Its Update runs every frame over every entity that has the component.
Editor tool
Editor/MyTool.cs
Optional windows, menu items and inspectors for the editor. Never shipped with the game.
Project layout
Everything lives in your project folder (the one with the .astge file), never in the engine folder.
MyGame/
MyGame.astge project file: open this in the editor
Scenes/ *.scene.json, made in the editor
Prototypes/ *.proto.json, reusable entities ("prefabs")
Components/ *.json, your components
Scripts/ *.cs, your systems (shipped with the game)
Editor/ *.cs, your editor tools (editor only)
Animations/ *.anim.json animation clips
Assets/ ... art and audio, any folder names you like
Resources/ files you only load by name from code (always exported)
StreamingAssets/ data files read with Assets.ReadText (always exported)
ProjectSettings/ Build.json (Build window settings)
Library/ editor cache and editor save data (do not commit)
Builds/ exported games (do not commit)
The frame, in order
Knowing the order explains most "why didn't this happen yet" questions.
Input and events. SDL events are polled, then queued events are delivered (scene loads, spawns, your events).
FixedUpdate. 0 to 5 steps at 60 Hz. Physics runs here.
Update. Systems in phase order PreUpdate → Update → PostUpdate, lower Order first. Commands are flushed between phases.
Animation, then LateUpdate (camera, UI, hierarchy).
Frame end. Commands flushed, events delivered again, timers fire, Session.FrameEnd runs, debug lines and UI are sent.
Audio and render. Queued sounds start, then the frame is drawn.
Start here
Your first component and system
This walkthrough makes a sprite spin, with a speed you can change in the Inspector. It touches every part of the workflow.
1. Define the data
Create Components/Spinner.json (or Scripts › New Component). Pick an id of 1000 or more that nobody else uses.
Create Scripts/SpinnerSystem.cs. The SpinnerCollection parameter tells the engine which entities this system runs over.
Scripts/SpinnerSystem.cs
using Engine.Core;
using GeneratedECS;
using static GeneratedECS.GeneratedECSRegistry;
namespace Game;
[ECSSystem(6200)] // unique system id
public static class SpinnerSystem {
public static void Update(float dt, SpinnerCollection spinners) {
for (int i = 0; i < spinners.Length; i++) {
var s = spinners[i];
if (!s.Enabled) continue;
if (Transform.TryGet(s.EntityId, out var t))
t.Rotation += s.Speed * dt; // writes straight into the entity
}
}
}
3. Use it
Save both files. The editor compiles automatically; errors appear in the Console with file and line.
Select an entity with a Sprite, click Add Component and pick Spinner.
Press Play. Change Speed in the Inspector while it runs.
Press Stop. Play-mode changes are discarded; save the scene (Ctrl+S) for changes made while stopped.
Start here
Everyday workflow
Action
How
New system
Scripts › New Script, or create a .cs in Scripts/
New component
Scripts › New Component, or create a .json in Components/
New editor window
Tools › New Editor Window Script
Compile
Automatic on save. Status in the Scripts menu.
Run
Play / Pause / Stop. Undo/redo (Ctrl+Z / Ctrl+Y) works outside Play mode.
Find an error
Double-click a Console line to open the file at that line. Error Pause pauses Play on the first exception.
Open the code in your IDE
The editor writes a .csproj for the project so IntelliSense knows the generated types.
IntelliSense is missing my component
Generated types such as EnemyCollection come from the component JSON. If your IDE doesn't know them yet,
save and let the editor compile once, then reload the project in the IDE. After changing the generator itself, restart Visual Studio, which caches it.
GameScripts.csproj and git
The editor writes GameScripts.csproj into the project folder whenever it opens the project. It is only for your IDE (the editor
compiles scripts itself) and it is committed with the project. The engine build folder is written relative to the project, so
everyone with the same folder layout gets the same file, whichever drive or folder the repository is in:
It still changes when you open the project with a Release editor (Release instead of Debug in that path) or keep the
project somewhere else relative to the build folder. Either version works for anyone who has that build folder, so the team commits the
Debug one and discards the other change. Don’t edit the file by hand: the editor rewrites it.
Components
Writing a component
A component is plain data. One JSON file per component, anywhere under a Components/ folder.
target's scene id; survives save/load and duplicate
TextureAnimationAudioFontScenePrototypeAsset
string path + <Field>Id int
Asset picker; drag from Project
"Assets/Images/hero.png"
string
string
Text box
text
List<string>
List<string>
Editable list
array of text
Aliases also accepted: Sprite/Image (Texture), Anim/AnimationClip, Sound/AudioClip.
Per-field keys
Key
Effect
default
Starting value. Numbers, true, "x, y", "#hex", an enum value name, or an asset path.
tooltip
Hover text in the Inspector, and the XML doc comment in C#.
header
Bold section title above the field.
hidden
Not shown in the Inspector (still saved).
readOnly
Shown but not editable (values the engine writes).
range
[min, max]: number field becomes a slider.
enumName
Name of the generated enum (default <Component><Field>). Two components can share one enum this way.
Mistakes
Bad JSON, an unknown type, a duplicate field, or two components with the same id or name are compile errors that name the file
(GEN001–GEN004). The bad component is left out; everything else still compiles.
Components
Using components from C#
Every component JSON produces a struct in GeneratedECS.GeneratedECSRegistry, which the using static at the top of
every script brings into scope. The struct is a view: it holds a pointer to the entity's data, so setting a property writes
straight into the engine. Copying it into a local variable does not copy the data.
// from anywhere: systems, event handlers, timers, UI callbacks
if (Enemy.TryGet(entity, out var en)) en.Health -= 10; // safe: checks first
var t = Transform.Get(entity); // throws if missing
bool armed = Weapon.Has(entity);
foreach (var e in Enemy.All()) Debug.Log(e.Health); // every Enemy in the world
// asset fields: path string, or the fast int id
var sprite = Sprite.Get(entity);
sprite.Texture = "Assets/Images/Boss.png";
if (sprite.TextureId == other.TextureId) { /* same image */ }
// enum fields use the generated enum
if (en.State == EnemyState.Chase) { /* ... */ }
Adding and removing
Call
When to use
Enemy.Create(e)
Adds now with JSON defaults and returns the view. Only outside system loops (OnStart, event handlers, timers).
Enemy.AddDeferred(e)
Inside a system loop. The component exists after the next flush.
cmd.Add(e, Enemy.TypeId, in data)
Inside a loop, with your own starting values. Build data from Enemy_Defaults().
Enemy.Remove(e)
Anywhere; deferred automatically while systems run.
public static void Update(EnemyCollection enemies, Commands cmd) {
for (int i = 0; i < enemies.Length; i++) {
var e = enemies[i];
if (e.Health <= 0 && !Burning.Has(e.EntityId)) {
var b = Burning_Defaults(); // raw struct with the JSON defaults
b.Duration = 3f;
cmd.Add(e.EntityId, Burning.TypeId, in b);
}
}
}
Components
Built-in components
The engine's own components (ids 100–127) work exactly like yours. Their fields are listed in the
component reference.
Component
Purpose
Transform
Position (world units), Rotation (degrees, counter-clockwise), Scale (1 = 100 units). World space.
Sprite
Texture, tint, flip, sprite-sheet Frame (-1 = whole image), draw Order (higher on top).
Added by the editor when you nest entities. Don't add it by hand; use Hierarchy.SetParent.
Systems
Writing a system
A system is a static class marked [ECSSystem(id)] with any of six lifecycle methods. Write only the ones you need.
Scripts/EnemySystem.cs
using Engine.Core;
using GeneratedECS;
using static GeneratedECS.GeneratedECSRegistry;
namespace Game;
[ECSSystem(6000)]
public static class EnemySystem {
public static void OnStart() {
Physics.OnTriggerEnter(Scenes.Find("Goal"), c => Scenes.Load("Level2"));
}
public static void Update(float dt, EnemyCollection enemies, Commands cmd) {
for (int i = 0; i < enemies.Length; i++) {
var e = enemies[i];
if (e.Health <= 0) { cmd.Destroy(e.EntityId); continue; }
if (e.State == EnemyState.Chase && World.IsAlive(e.Target)) { /* move towards it */ }
}
}
}
Lifecycle methods
Method
Runs
OnCreate
Once, when the system is registered (also after each script reload).
OnStart
Once, when Play starts. Subscribe to events and callbacks here.
Update
Every frame, in the system's phase.
OnFixedUpdate
0 to 5 times per frame at a fixed 60 Hz. Don't read KeyPressed here: it can be missed or seen twice.
OnLateUpdate
After all Updates and animation.
OnDestroy
When Play ends.
Parameters
Every method takes 0–3 parameters, matched by type in any order:
float: delta time (same as Time.DeltaTime).
One XCollectionor one Query<A, B>: the entities this system runs over. Use the same one in every method of the system.
Commands: deferred structural changes.
A system without a collection still runs once per frame. Use it for global logic (input, spawning waves, HUD).
Systems
Phases and order
[ECSSystem(6010, Phase = SystemPhase.PreUpdate, Order = 10)]
public static class ReadInputSystem { /* ... */ }
Phases run PreUpdate → Update (default) → PostUpdate. Inside a phase, lower Order runs first (default 0, negatives allowed).
Engine system
Id
Phase
Order
Does
HierarchySystem
900
PostUpdate
10000
Children follow parents
PhysicsSystem
910
PostUpdate
5000
Bodies, collisions (in OnFixedUpdate)
CameraSystem
920
PostUpdate
20000
Follow, bounds, shake (OnLateUpdate)
UISystem
930
PostUpdate
30000
Layout, input, drawing (OnLateUpdate)
So gameplay in Update sees last frame's physics result, and moves you make in Update are applied to children and the camera the same frame.
Systems
Several components: Query
When a system needs entities that have two or more components, take a Query instead of a collection.
[ECSSystem(6020)]
[Without(typeof(Frozen))] // optional: skip entities that have Frozen
public static class MoveSystem {
public static void Update(float dt, Query<Velocity, Transform> q) {
foreach (var row in q) {
var (v, t) = row; // or: var (entity, v, t) = row;
t.PosX += v.X * dt;
t.PosY += v.Y * dt;
}
}
}
Up to 4 components. The first is iterated; entities missing any other are skipped, so put the rarest first.
Query<A> yields A directly.
[Without(...)] applies to the system's Query parameters only.
Same rules as collections: the views write through; structural changes go through Commands.
Systems
Change tracking
Set "trackChanges": true in a component's JSON, then skip entities that didn't change since this system last ran:
public static void Update(HealthBarCollection bars) {
for (int i = 0; i < bars.Length; i++) {
if (!bars.Changed(i)) continue; // only bars added or modified since last time
Redraw(bars[i]);
}
}
Untracked components report every slot as changed. For a Query, q.Changed(row.Index) covers the first component.
Systems
Entities, handles and structural changes
An entity id is a ulonghandle: slot index plus a generation. When an entity dies its slot's generation changes, so an old handle you kept
(a target, a follow) never reaches a different entity that reused the slot. World.IsAlive(id) just returns false.
ulong target = enemy.Target;
if (World.IsAlive(target) && Transform.TryGet(target, out var tt)) { /* chase */ }
Structural changes
Creating or destroying entities and adding or removing components can move component memory. Inside a system loop, do them
through something deferred:
World.Destroy, X.Remove (these defer automatically, but prefer Commands for clarity)
X.AddDeferred(e)
X.Create(e)
Scenes.Spawn(...)
Scenes.Instantiate(...)
Event handlers, timers, physics and UI callbacks all run at the end of the frame, outside any loop, where anything is allowed.
Gameplay
Events
Events pass messages between systems without direct references. They go through the same queue C++ uses, so C++ and C# can talk both ways.
Declare a struct with a unique id. Only unmanaged fields: numbers, bools, ulong entities, Vec2, nested blittable structs. No strings or lists.
Subscribe in OnStart. Subscriptions are cleared when Play stops, so you never get duplicates.
Publish from anywhere. Delivery happens at the next dispatch (start of the frame or after LateUpdate).
[ECSEvent(9001)]
public struct DamageEvent { public ulong Target; public int Amount; }
[ECSSystem(6100)]
public static class HealthSystem {
public static void OnStart() {
Events.Subscribe<DamageEvent>(e => {
if (Health.TryGet(e.Target, out var h)) h.Value -= e.Amount;
});
}
}
// somewhere else:
Events.Publish(new DamageEvent { Target = enemy, Amount = 5 });
Keep the subscription to stop early: var sub = Events.Subscribe<T>(...); sub.Dispose();
Engine events you can subscribe to
Event
Id
Fields
CollisionEvent
0xFFFF0400
Entity, Other, Normal, Point, Depth, Phase (0 enter, 1 stay, 2 exit), Trigger. Easier: the Physics.On* callbacks.
For data that belongs to no entity (score, wave number, settings), use a plain class through Resources. Don't use static fields
for per-entity state: put that in components.
public class GameState { public int Score; public int Wave = 1; public bool GameOver; }
Resources.Get<GameState>().Score += 10; // created on first use
[PersistentResource] // survives Play/Stop (e.g. options)
public class Options { public float MusicVolume = 0.8f; }
Resources reset when Play stops, so every Play starts clean, unless marked [PersistentResource].
Gameplay
Scenes and prototypes
A prototype (.proto.json) is a reusable entity with its children, like a Unity prefab. Create one from the Project panel and edit it like a small scene.
Scenes.Load("Level2"); // Scenes/Level2.scene.json, at the next frame boundary
Scenes.Reload();
ulong boss = Scenes.Find("Boss"); // by editor name, 0 if none
// spawn: safe anywhere, appears next frame boundary
Scenes.Spawn("Prototypes/Bullet", pos, bullet => {
var rb = Rigidbody.Get(bullet);
rb.Velocity = dir * 600f;
});
// immediate: only outside system loops
ulong e = Scenes.Instantiate("Prototypes/Pickup", 100, 50);
Scenes.DontDestroyOnLoad(player); // survives scene changes
Scenes.Loaded += name => Debug.Log("loaded " + name);
Names are flexible: "Level2", "Scenes/Level2" and the full file name all work, and a bare prototype name is found anywhere in the project.
Gameplay
Parent and child
Nest entities in the Hierarchy panel, or at runtime:
Hierarchy.SetParent(sword, player); // keepWorld: true by default
Hierarchy.ClearParent(sword);
if (Hierarchy.TryGetParent(sword, out var owner)) { }
Transform is always world space. Move a parent by its Transform and children follow.
Move a child relative to its parent with its Parent.LocalX/LocalY/LocalRotation/LocalScaleX/LocalScaleY.
Destroying a parent destroys its children. Call ClearParent first to keep them.
The first SetParent on an entity adds a component immediately: call it from OnStart or a callback, not while looping over that entity.
Gameplay
Time, timers and cooldowns
Time.TimeScale = 0.5f; // slow motion; 0 = paused game
float real = Time.UnscaledDeltaTime; // for menus while paused
Timers.After(2f, () => Debug.Log("2 seconds later"));
var spawner = Timers.Every(0.5f, SpawnEnemy); // until cancelled
Timers.Every(1f, Tick, count: 3); // exactly 3 times
Timers.After(1f, Explode, owner: bomb); // skipped if bomb died first
Timers.DestroyAfter(bullet, 2f);
spawner.Cancel();
// a field in your system class: static Cooldown s_dash = new Cooldown(1.5f);
if (Input.KeyPressed(Key.Space) && s_dash.TryUse()) Dash();
UIDraw.Rect(10, 40, 100 * s_dash.Progress, 6, Color.Cyan); // cooldown bar
Timers use game time (TimeScale applies) unless unscaled: true. They fire at the end of the frame and are cleared when Play stops.
For a per-entity cooldown store the time in a component: if (Timers.Now >= w.NextShot) { w.NextShot = Timers.Now + w.FireRate; Shoot(); }
Gameplay
Input
Vec2 move = Input.MoveVector; // WASD / arrows, length <= 1
float h = Input.Axis(Key.A, Key.D);
if (Input.KeyPressed(Key.Space)) Jump();
if (Input.KeyDown(Key.LeftShift)) speed *= 2;
var p = Input.Pointer; // mouse-left or first finger
if (p.Pressed && !UI.PointerOverUI) Shoot(Camera.MouseWorld);
for (int i = 0; i < Input.TouchCount; i++) {
var t = Input.GetTouch(i);
if (t.WasPressed) Debug.Log($"finger {t.Id} at {t.X},{t.Y}");
}
if (Input.KeyPressed(Key.Back)) OpenPauseMenu(); // Android back
Read input in Update. Pressed and Released last exactly one frame.
Coordinates are screen pixels, origin top-left, y down. For the world position use Camera.MouseWorld.
In the editor the Game panel must be focused or hovered, and positions are relative to it.
Input.Pointer works on both desktop and phones; prefer it for taps and clicks.
Gameplay
Physics
Add a Collider (Box or Circle). Size 1,1 or Radius 0.5 covers the sprite quad.
Add a Rigidbody to things that move. A Collider without a Rigidbody is a static wall.
Tick IsTrigger for areas that detect overlaps but never push (pickups, goals).
React in code with the callbacks below.
public static void OnStart() {
ulong player = Tags.Find("Player");
Physics.OnCollisionEnter(player, c => {
if (Tags.Has(c.Other, "Enemy")) Events.Publish(new DamageEvent { Target = player, Amount = 1 });
});
foreach (var coin in Tags.FindAll("Coin"))
Physics.OnTriggerEnter(coin, c => {
if (!Tags.Has(c.Other, "Player")) return;
Resources.Get<GameState>().Score++;
World.Destroy(c.Entity);
});
Physics.NameLayer(3, "Enemy");
Physics.SetLayerCollision(3, 3, false); // enemies pass through each other
Physics.Gravity = new Vec2(0, -980); // platformer: also set GravityScale 1 on bodies
}
// queries
if (Physics.Raycast(origin, dir, 500f, out var hit)) DebugDraw.Line(origin, hit.Point, Color.Red);
var near = Physics.OverlapCircle(pos, 120f, Physics.Mask("Enemy"));
Physics.AddImpulse(e, new Vec2(0, 400));
Rigidbody GravityScale defaults to 0 (top-down). Set it to 1 for platformers.
Put Rigidbodies on root entities; a child's Transform is rewritten from its parent.
Rotation is not simulated. Colliders follow the Transform rotation for overlap tests.
Physics.DebugDraw = true outlines every collider.
Gameplay
Camera
Camera.Follow(player, speed: 6f); // 0 = locked on
Camera.Zoom = 2f;
Camera.Shake(8f, 0.3f);
Camera.SetBounds(new Vec2(-2000, -1000), new Vec2(2000, 1000));
Vec2 world = Camera.MouseWorld;
Vec2 screen = Camera.WorldToScreen(enemyPos); // e.g. a UI marker above an enemy
Add an entity with a Camera2D component to set the camera up in the editor (the Game view shows it even when stopped).
These functions then write into that component. Without one, they drive a default camera.
Gameplay
Animation
Make a clip
Select a sprite-sheet image in the Project panel.
Set Columns and Rows (saved in the image's .meta), then click Create Animation Clip.
Pick frames, fps and loop in the Inspector; the preview plays live.
Animator.Play(e, "Run"); // by name or path; adds an Animator if missing
Animator.Play(e, "Attack", restart: true);
if (!Animator.IsPlaying(e, "Attack")) Animator.Play(e, "Idle");
Animator.OnFinished(e, clip => Animator.Play(e, "Idle")); // non-looping clips
Gameplay
Audio
Each sound is an int handle to a native audio component. Create it once, then play it as often as you like.
static int s_jump, s_music;
public static void OnStart() {
s_jump = Audio.Create("Assets/Audio/sfx/jump.wav"); // SFX group by default
s_music = Audio.Create("Assets/Audio/theme.ogg", AudioGroup.BGM, 0.6f);
Audio.Play(s_music);
}
public static void Update() {
if (Input.KeyPressed(Key.Space)) Audio.Play(s_jump);
if (Input.KeyPressed(Key.M)) {
if (Audio.GetState(s_music).IsPaused) Audio.Resume(s_music); else Audio.Pause(s_music);
}
}
From a component field of type Audio: Audio.Create(c.HitId).
Topic
Detail
Formats
.wav, .mp3, .flac, .ogg. An .ogg must contain Vorbis audio; Opus or FLAC inside .ogg fails (the log says which). Re-encode: ffmpeg -i in.ogg -c:a libvorbis -q:a 5 out.ogg.
Memory
Clips are loaded fully into memory. Prefer .ogg for long music: a 3-minute .wav is about 30 MB.
Timing
Play queues the sound; it starts at the end of the frame.
Volume/pitch
SetVolume / SetPitch apply on the next Play.
Cleanup
Handles are released when Play stops. Audio.Release(h) frees one earlier.
Preloading
Audio.Register(clip) loads it ahead of time to avoid a hitch on first play.
Background (Android)
Sound pauses when the app leaves the screen and continues when it returns (engine app-state hook).
UI
Canvas UI
Build it in the editor
Entity › UI › Canvas. Elements live under a Canvas and are laid out in reference pixels (default 1280×720), scaled to the screen.
SaveData.SetInt("coins", coins);
SaveData.SetString("lastLevel", Scenes.Current);
SaveData.SetObject("settings", mySettings); // any plain class
SaveData.Save(); // also automatic on Stop / Quit
int coins = SaveData.GetInt("coins", 0);
var s = SaveData.GetObject("settings", new Settings());
SaveData.Slot = "slot2"; // switch save slot
Where
Location
Editor
<Project>/Library/SaveData/ (never touches real player saves)
Windows
%APPDATA%\<Company>\<Product>
Linux
~/.local/share/<Company>/<Product>
Android
App internal storage
Debugging
Logging and debug drawing
Debug.Log("spawned " + n);
Debug.LogWarning("low hp");
Debug.LogError("save corrupt"); // kept in release, written to log.txt
Debug.LogException(ex);
Debug.Assert(hp >= 0, "negative hp");
DebugDraw.Circle(pos, attackRange, Color.Red); // this frame
DebugDraw.Line(a, b, Color.Yellow, duration: 2f);
DebugDraw.Box(center, new Vec2(64, 32), Color.Green, angle: rot);
Physics.DebugDraw = true; // every collider
Build
What stays
Editor
Everything.
Export with Strip Debug Logs
Log, LogWarning, Assert and all DebugDraw calls are compiled out: they don't even run. LogError and LogException stay and go to log.txt in the save folder.
Export with Strip Debugger / Profiler
The profiler, hang watchdog, crash reports, debug console, log capture and test runner are switched off at start-up. Profiler.* calls return at once. See Exporting.
Finding problems
Double-click a Console line to open the file at that line.
Console › Error Pause pauses Play on the first exception.
The editor also writes everything to Editor.log in its app-data folder (useful after a crash).
Every run of the editor and of the development game also writes logs/<executable>.log in its working directory (exported games don't): every C++ and C# line with time and frame number, including start-up before the editor UI exists. See Running headless for the options.
On Android, use Build › Logcat; your messages appear with the SDL/APP tag.
Slow frame or a freeze? Open the Watchdog panel: it shows which system took the time, and a hang report names the system the game is stuck in.
Debugging
Profiler and the Watchdog panel
Every ECS system is timed automatically, per phase, with the names the generator registered. You don't add anything to get the
systems table. For your own code, time a block or record a value:
// or: using var _ = Profiler.Scope("Pathfinding"); (ends at the end of the block)
using (Profiler.Scope("Pathfinding")) {
FindPath(start, goal);
}
Profiler.Counter("Enemies alive", enemies.Length); // one value per frame, graphed
Profiler.Mark("Boss spawned"); // event list + trace marker
double ms = Profiler.Stat("system_ms:EnemySystem"); // any profiler value by name (NaN if unknown)
The Watchdog panel
View › Watchdog, Ctrl+Shift+W, or click the FPS counter in the toolbar. It docks next to the Console.
Tab
Shows
Overview
Graphs: frame time against the frame budget, CPU, memory, C# heap, allocations, entities, audio, GC. Pin any counter.
Systems
Sortable table: avg / recent / max / last ms, % of the frame, entities, calls. Click a row for the per-phase breakdown. The checkbox turns a system off.
Timers
The same for engine phases and your Profiler.Scope blocks.
Memory
Process memory, native heap, C# heap and collections, leak analysis (memory that keeps growing).
Counters / Events
Every counter; hitches, hangs, GCs, script errors, your Profiler.Mark lines.
The header has Record trace (open the file in ui.perfetto.dev for a timeline of every frame,
system and scope) and Save report (text report in <Project>/Profiler/).
Turning it off
Collecting costs a little per system and per C++ allocation, which shows when you mass-spawn thousands of entities.
Preferences › Debug › Profiler (also a combo in the Watchdog header) decides when it runs:
Mode
Collects
While Watchdog is open (default)
Everything, only while the panel is visible.
Always
Everything, all the time, so the history is there when you open the panel.
Off
Frame time and FPS only.
The hang watchdog and crash reports stay on in every mode; they cost nothing per frame.
Debugging
Debug console commands
One set of commands works everywhere: the Watchdog Console tab, a terminal (headless runs), test scripts and code.
help lists them all, including the ones scripts added.
Command
Does
systems [avg|max|name], system <name> on|off
Systems table; switch a system off and on
stats, mem, leaks, events
Frame times, memory, leak analysis, recent events
entities, components [filter], audio, gc
Live entities, component counts, FMOD channels, C# heap
scene load <name>, spawn <prototype> [x y]
Through Scenes
pause, step [n], resume, timescale <x>
Freeze the game logic, single-step, slow motion
trace start [file], trace stop, report [file]
Chrome trace for part of a run; full report
mark <text>, log level <lvl>, quit [code]
Your own commands
[ECSSystem(6400)]
public static class CheatSystem {
public static void OnCreate() {
DebugConsole.Register("heal", "heal <amount>", "heal the player", args => {
var hp = Health.Get(Scenes.Find("Player"));
hp.Value += int.Parse(args[1]); // args[0] is the command name
return "player hp " + hp.Value; // shown in the console; throw to report a failure
});
}
}
string table = DebugConsole.Execute("systems max"); // run any command from code
Commands from game scripts are removed automatically when the scripts reload, so registering in OnCreate never duplicates them.
Testing
Testing at a glance
The engine has four kinds of tests. One command builds everything and runs all of them, the same way on your
machine and on a build server, and exits non-zero when anything fails.
C++ checks against the running engine through GamTestApi.h.
The two commands
From a clean checkout (CMake 3.16+, a C++20 compiler, Python 3.9+, the .NET SDK):
python Tests/ci.py # build Debug + Release (+ ASan on Linux/macOS), run every test
python Tests/ci.py --quick # Debug only: unit tests + leak self-check, about a minute after a build
ECS internals, the event bus, the serializer, any C++ that runs without a window
A unit test. It runs in milliseconds and names the exact case that broke.
A system, a component or gameplay that needs a scene
A .gtest script, or a C# test if you need to inspect entities.
Performance (“must stay under 2 ms”)
An expect system_ms:YourSystem < 2 line in a .gtest. Measure in a Release build.
Something that only breaks after a while (a hang, a growing heap)
A .gtest with a long wait plus expect hangs == 0 and expect rss_mb < ....
Testing
Running headless
Any build of the game or the editor runs without a window. That is how tests run on a build server, but it is also the
quickest way to profile a scene. The engine runs normally: the scene loads, systems tick, audio plays (silently if there is no
sound device), and the profiler records.
# the start scene for 10 seconds: a summary line every second, a full report at the end
GAM200_Game --project MyGame/MyGame.astge --headless --seconds 10
# 600 frames with a Chrome trace and a report file
GAM200_Game --project MyGame/MyGame.astge --headless --frames 600 --trace trace.json --report report.txt
# pure simulation: no drawing, every frame advances exactly 1/60 s (repeatable results)
GAM200_Game --project MyGame/MyGame.astge --headless --no-render --fixed-dt 0.0166667 --frames 3600
# the editor executable without a window behaves like the game (no editor UI)
GAM200_Editor --project MyGame/MyGame.astge --headless --test Tests/smoke.gtest
The executables are in local_desktop_build/<Config>/ after setup_build, or build/ci-<config>/<Config>/ after Tests/ci.py.
--project picks the game; without it the engine also accepts a bare *.astge argument or the GAM_PROJECT environment variable.
What you see
First a start-up table: how long each step took (SDL, window and GL, C# runtime, assets, renderer, audio, OnCreate).
Then a summary line every second, with the three most expensive systems at the end:
[prof] fps 60.0 | ms avg 2.41 p99 4.80 max 6.12 | cpu 38% | rss 212.4MB | ent 214 | top EnemySystem 0.42ms, PhysicsSystem 0.31ms
When the run ends, a full report: frame-time percentiles, the systems and timers tables, memory, leak trends, hitches, hangs and test results.
Options
The most useful ones. GAM200_Game --debug-help prints all of them; the same list can also be set once in the
GAM_DEBUG_ARGS environment variable (the command line wins).
Option
Effect
--headless / --windowed
No visible window / force a window. Headless is automatic on Linux when there is no display.
--frames N, --seconds S
Quit after N frames or S seconds. With --test, the run ends when the last test finishes.
--fps N
Frame limiter (headless default 60, 0 = as fast as possible).
--fixed-dt S
Every frame advances exactly S seconds: the same input gives the same result every run.
--no-render
Keep the GL context (assets load) but skip drawing: pure simulation.
--trace file.json
Chrome trace of every frame, system and scope. Open it in ui.perfetto.dev.
--report file, --report-json file
Write the final report to a file (text / machine readable).
--log-file file, --no-log-file, --log-level lvl
See logs below.
--hang-timeout S, --hang-abort S
A frame longer than S seconds is a hang (default 5). With --hang-abort a hang ends the run with exit code 124 instead of blocking a build server forever.
--exec "cmd; cmd"
Console commands to run on the first frame, e.g. --exec "scene load Level2".
--console
Type console commands into the terminal while it runs (on by default in an interactive headless terminal).
Where it can run
Machine
What to know
Windows or Linux desktop
Works as is. --headless keeps the window hidden.
Linux server, no display
Headless switches on by itself. Install Mesa (sudo apt install libegl1 libgl1-mesa-dri) to render on the CPU, or run under xvfb-run with --video-driver x11.
No OpenGL at all
Headless runs still start: the engine prints one warning and runs everything except drawing. The stats gl and rendering are 0, so a test can check for it.
Logs
Every run writes its log to the terminal and to logs/<executable name>.log in the working directory
(for example logs/GAM200_Editor.log), rewritten on each start. It holds every line of the process: C++ LOGI / LOGE,
std::cout, C# Debug.Log and the debugger's own lines, with time and frame number. The first line of the
main loop says where the file is. --log-file path chooses another file, --no-log-file turns it off. On Android the log is logcat.
Exported games never write it, stripped or not: the game doesn't create logs/ in a player's install folder. --log-file still works there when a tester asks for it, and errors from scripts still go to log.txt in the save folder (Debug.LogError).
When start-up fails
EngineCore::TryInitialize returns false when the engine cannot run, and says why in one sentence. Instead of an
empty window you get that sentence in the log and, in a windowed run, an error dialog naming the log file. The process exits with code 1.
Message starts with
Usual cause
SDL could not start the video subsystem
No display or graphics driver. Use --headless on a server.
could not create an OpenGL 4.5 core context
An old or missing GPU driver. Update it.
the C# script runtime did not start
CSharpRuntime/ next to the executable is missing or stale, or .NET is not installed. Run build_csharp (CMake does it as part of the build). The lines above it in the log say which step failed.
shader 'Sprite' failed to compile or link
A broken Assets/Shader/*.vert / .frag. The ShaderHelper error above it has the GLSL compiler output.
Exit codes
Code
Meaning
0
Ran to the end; every test passed
1
Start-up failed (see above)
N
N tests failed (capped at 100)
2
Tests were requested from a build exported with Strip Debugger / Profiler
124
Stopped by --hang-abort after a hang (the report names the system it was stuck in)
134 / 139
Crashed; crash_<pid>.log has the stack trace
Testing
Engine test cases
Test cases given with --test run one after another while the real scene plays, so they see the real systems,
physics, audio and profiler. When the last one finishes the engine quits; the exit code is the number of failures.
GAM200_Game --project MyGame/MyGame.astge --headless --test Tests/smoke.gtest --junit results.xml --hang-abort 30
GAM200_Game --headless --test-dir Tests --junit results.xml # every test file in a folder, sorted
Tests/run_tests.sh [game executable] [project.astge] # run_tests.bat on Windows: the team's standard set
--test accepts
Runs as
*.gtest, *.txt
A test script (below).
*.sh, *.bat, *.cmd
With #gam: / REM gam: lines: those lines as a test script (a launcher can test itself, below). Without: run as a program for this platform, exit code 0 passes.
a .NET *.dll
C# tests (detected automatically).
a native *.dll / *.so
A native test module.
shell:<command line>
Any program; exit code 0 passes.
Test scripts (.gtest)
One command per line; # starts a comment. Any console command works, plus these:
Command
Does
name <text>
Name in the results and JUnit (default: the file name).
timeout <s>
Fail if the test takes longer (default 120, or --test-timeout).
wait <frames>, wait <s>s
Let the game run. wait 120 = 120 frames, wait 2s = 2 seconds.
expect <stat> <op> <value>
Compare a stat: < <= > >= == !=.
assert_no_errors
Fail if anything logged an error since the test started (C++ or C#, including script exceptions).
fail <message>, pass
End the test now.
shell <command>
Run a program; a non-zero exit code fails the test.
A script stops at its first failure. An unknown command is skipped with a warning; --strict-scripts makes it a failure.
Tests/smoke.gtest
# Smoke test: the start scene runs for a while without errors, hangs or a collapsing frame rate.
name Smoke: start scene runs cleanly
timeout 60
profile reset # any console command works in a script
wait 120 # frames; "wait 2s" = seconds
expect frame >= 120
expect fps_avg > 20 # < <= > >= == !=
expect hangs == 0
assert_no_errors # no error logged since the test started (C++ or C#)
mark "smoke: half way"
wait 2s
expect frame_ms_p99 < 100
expect rss_mb < 2048
Switching systems off, stepping and tracing
name Systems: toggle + timings
wait 30
systems # prints the systems table into the log
expect systems_run >= 1
system EnemySystem off # the game keeps running without it
wait 30
system EnemySystem on
pause
step 10 # exactly 10 frames, then paused again
resume
trace start systems_trace.json # 60 frames of timeline for ui.perfetto.dev
wait 60
trace stop
assert_no_errors
Stat names
The same names work in expect, Profiler.Stat, TestAssert.Stat and native modules' api->Stat.
Stat
Value
frame, fps, fps_avg, frame_ms, frame_ms_p99
Frame counter and frame times since the last profile reset.
system_ms:<System>, zone_ms:<Scope>
One system's or Profiler.Scope's time, by the name in the Systems table.
systems_run, entities
Systems that ran last frame; live entities.
rss_mb, native_mb, managed_mb
Process memory, C++ heap, C# heap.
errors, hitches, hangs
Counts since start.
audio_channels, gl, rendering
FMOD channels playing; 1 when OpenGL / drawing is available.
your Profiler.Counter names
Last value; add .avg, .min or .max for the history.
A launcher that is also a test
Lines starting with #gam: (shell) or REM gam: (batch) are comments to the shell and test commands to
the engine. Tests/run_tests.sh passes itself to --test this way, so the launcher checks that it works too.
#!/bin/sh
# Lines starting with "#gam:" are comments to the shell, test commands to the engine.
#gam: name Shell-launched smoke test
#gam: wait 60
#gam: expect fps_avg > 20
#gam: assert_no_errors
exec "$GAME" --headless --test "$0" ...
C# tests
Put tests in a class library that references EngineScripts.dll and pass the built .dll to --test.
Static [EngineTest] methods run first, then every IEngineTest class runs frame by frame until all report done.
public static class QuickChecks {
[EngineTest] public static void PlayerExists() => TestAssert.True(Scenes.Find("Player") != 0, "no Player");
}
public sealed class BoxFalls : IEngineTest { // runs across frames
ulong _box; float _y0, _t;
public void Start() { _box = Scenes.Find("Box"); _y0 = Transform.Get(_box).PosY; }
public bool Update(float dt) { // true = finished
if ((_t += dt) < 1f) return false;
TestAssert.True(Transform.Get(_box).PosY < _y0, "box did not fall");
return true;
}
}
Native test modules
A small shared library that only needs Src/Engine/Debug/GamTestApi.h. Every *.cpp in Tests/native/
becomes one module (cmake -S Tests/native -B build/native && cmake --build build/native; ci.py does this for you),
and run_tests picks up whatever is in Tests/native/build/. Keep CMake’s own files under build/ (git-ignored):
only the finished modules go to Tests/native/build/.
Tests/native/FramePacingTest.cpp
#include "GamTestApi.h"
static int s_frames = 0;
extern "C" GAM_TEST_EXPORT const char* GamTest_Name(void) { return "Frame pacing"; }
extern "C" GAM_TEST_EXPORT int GamTest_Init(const GamTestApi* api) {
GAM_CHECK(api, api->GetNative("Time.GetState") != nullptr); // any ENGINE_NATIVE, same table as C#
GAM_CHECK(api, api->Execute("profile reset") == 0); // any console command
return 0; // 0 = ok
}
extern "C" GAM_TEST_EXPORT int GamTest_Update(const GamTestApi* api, float dt) {
if (++s_frames < 120) return 0; // 0 = keep going
GAM_CHECK(api, api->Stat("fps_avg") > 20.0);
return 1; // 1 = done (failed if a check failed)
}
Run a console command (0 = ok) and read its text output.
GetNative(name)
Any ENGINE_NATIVE function, the same table C# uses.
Log(level, msg), Frame(), Time()
Log (0 info, 1 warning, 2 error), engine frame, seconds since the test started.
Results
--junit file.xml writes JUnit XML that any CI server can show. Tests/run_tests.* writes JUnit, the text and
JSON reports, a trace and the log to test-results/ (or the folder in GAM_TEST_OUT). The exit codes are in
Running headless.
Testing
Unit tests
GAM200_UnitTests tests the engine's core C++ with nothing else running: no window, no OpenGL, no C# runtime, and it
does not even link EngineCore. It builds in seconds, runs in about 50 ms, and names the exact case that broke.
Area
Covers
Sparse sets
SparseSet (set / get / remove / growth / iteration) and UntypedSparseSet (raw bytes, swap-remove, change ticks). SparseSetTests.cpp
Registry
Entity ids and recycling, generation handles, typed and C# components, the C# release hook on destroy. RegistryTests.cpp
Systems
Phase / order, lazy OnStart, enable toggles, deferred destroy and add, change tracking, profiler timing, script hot reload. SystemDispatchTests.cpp
Views
View<A, B> queries. ViewTests.cpp
Events
Typed events, raw C# channels, unsubscribing during dispatch, the managed sink and reset. EventSystemTests.cpp
cmake --build build --target GAM200_UnitTests # seconds: it does not need EngineCore or C#
build/Debug/GAM200_UnitTests # everything
build/Debug/GAM200_UnitTests Registry View # only cases whose name contains "Registry" or "View"
build/Debug/GAM200_UnitTests --list # names only
build/Debug/GAM200_UnitTests --junit unit.xml # JUnit XML for CI
ctest -C Debug --test-dir build # the same through CTest
A failing case shows the file, line, expression and both values
Running 47 unit test case(s) with per-test leak checking
[ RUN ] SparseSet: remove keeps every other key mapped to its own value
[ OK ] SparseSet: remove keeps every other key mapped to its own value (0.00 ms)
[ RUN ] Registry: destroyed ids are recycled and handles to the old entity go stale
[ FAILED ] Registry: destroyed ids are recycled and handles to the old entity go stale (1.23 ms)
RegistryTests.cpp:63: newHandle != handle (4294967297 != 4294967297)
...
46 of 47 unit test case(s) passed in 41.2 ms -- 1 FAILED
Exit code: the number of failed cases (0 = all passed). Some cases test error paths on purpose, so lines such as
[C++ ECS] Error: AddComponentRaw on dead entity 5 in the output are expected; only [ FAILED ] means something broke.
Writing a test
Add a .cpp file to Tests/unit/ (or a case to an existing one) and re-run CMake. Every .cpp in the folder is part of the executable.
Tests/unit/RegistryTests.cpp
#include "UnitTest.h"
#include "ECSRegistry.h"
#include <memory>
namespace { struct Health { int hp = 0; }; } // test-only component
TEST_CASE("Registry: removing a component keeps the others") {
auto reg = std::make_unique<ECS::Registry>(); // ~4 MB: keep it off the stack
const ECS::EntityID a = reg->createEntity(), b = reg->createEntity();
reg->addComponent(a, Health{ 10 });
reg->addComponent(b, Health{ 20 });
reg->removeComponent<Health>(a);
CHECK(!reg->hasComponent<Health>(a));
REQUIRE(reg->hasComponent<Health>(b)); // stops here if false: the next line would crash
CHECK_EQ(reg->getComponent<Health>(b)->hp, 20);
}
Macro
Does
TEST_CASE("Area: what it checks")
A test case. Start the name with the area so filters work.
CHECK(expr)
Records a failure and keeps going.
REQUIRE(expr)
Records a failure and leaves the test. Use it when the next line would crash otherwise.
Comparisons that print both values. Signed and unsigned integers compare by value.
FAIL("message")
Fails and leaves the test.
HIDDEN_TEST_CASE(...)
Only runs with --run-hidden <text> (used by the leak self-check).
Code that calls the engine
The ECS calls two engine services: ScriptEngineHost::GetExports() (so C# can free a component's strings and lists) and
the profiler's Begin/EndSystem. In the unit tests they are small doubles in Tests/unit/EngineSeams.cpp that a test can
observe or fill in:
namespace {
int g_released = 0;
void CDECL_CONV OnReleased(uint64_t, uint32_t, void*) { ++g_released; }
// puts the null exports table back even if a REQUIRE leaves the test early
struct ExportsGuard { ExportsGuard() { TestSeams::ResetExports(); } ~ExportsGuard() { TestSeams::ResetExports(); } };
}
TEST_CASE("Registry: destroying an entity lets C# release its component") {
ExportsGuard guard;
ScriptEngineHost::GetExports().OnEntityComponentDestroyed = &OnReleased; // what C# would install
// ... create an entity with a managed component, destroy it ...
CHECK_EQ(g_released, 1);
}
TestSeams::Profiler() counts system dispatches the same way. If the ECS starts calling another engine function, the unit
test link fails with an undefined symbol: add a double for it to EngineSeams.cpp.
Testing
Leak checking
Every unit test case is also a leak check: whatever a test allocates must be freed by the time it returns, or the case fails
with LEAK: n block(s) .... A leak fails the run exactly like a wrong value.
Check
Where
Catches
Reports
Per-test block count
Every build
new without delete in the test, or in engine code it calls
The test case that leaked
MSVC CRT debug heap
Windows, Debug
Also malloc leaks (rapidjson, SDL)
Each leaked block’s number and size; --break-alloc <n> stops the debugger on it
AddressSanitizer + LeakSanitizer
Linux / macOS, asan configuration
Any unreachable block in the whole run, plus memory errors (use after free, overflows)
A stack trace to the allocating line
Proof that it works
Tests/unit/LeakCheckSelfTest.cpp holds two hidden cases that leak on purpose. Tests/ci.py runs them every time and
only passes when the leak checker fails that run, so a broken leak checker can never go unnoticed.
$ build/Debug/GAM200_UnitTests --run-hidden leak-demo
[ FAILED ] leak-demo: a test case that forgets to delete one block (0.00 ms)
LEAK: 1 block(s) allocated with new in this test case were never deleted
[ FAILED ] leak-demo: a container that is never destroyed (0.00 ms)
LEAK: 2 block(s) allocated with new in this test case were never deleted
0 of 2 unit test case(s) passed -- 2 FAILED (exit code 2)
Finding a leak
Run only the failing case: GAM200_UnitTests "name of the case".
On Windows, run the Debug build. The CRT debug heap dump above the failure lists each leaked block as {1234} normal block at .... Run the same case under the Visual Studio debugger with --break-alloc 1234: it stops on the line that made that allocation.
On Linux, build the sanitizer configuration and run it. LeakSanitizer prints the stack of each leaked allocation.
==10727==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 64 byte(s) in 1 object(s) allocated from:
#0 0x7f... in malloc
#1 0x55... in CountedAlloc Tests/unit/UnitTestMain.cpp:37
#2 0x55... in operator new[](unsigned long) Tests/unit/UnitTestMain.cpp:65
#3 0x55... in LeakOneBlock Tests/unit/LeakCheckSelfTest.cpp:15 <- the line that leaked
SUMMARY: AddressSanitizer: 488 byte(s) leaked in 3 allocation(s).
The whole engine
The unit tests cover the core logic. For a run of the whole game, configure with -DGAM_SANITIZE=address,undefined (Linux / macOS) and run it
headless; Tests/sanitizers/lsan.supp silences leaks that belong to third-party libraries (SDL, FMOD, the .NET runtime, Mesa), never engine code.
The engine's own report also prints after shutdown: N more native allocations alive. That number includes statics and caches, so compare it between runs rather than expecting 0.
Testing
Build and test everything (ci.py)
Tests/ci.py builds every configuration and runs every test without asking anything, so a build server can run the exact command you run.
It exits with 0 only when every step passed and 1 otherwise.
python Tests/ci.py # everything
python Tests/ci.py --quick # Debug only: unit tests + leak self-check
python Tests/ci.py --configs debug asan # pick configurations
python Tests/ci.py --mutation-check # also prove the tests catch deliberate bugs
python Tests/ci.py --list # print the plan, run nothing
Configurations
Name
Builds
Runs
debug
Everything, Debug
Unit tests, leak self-check, native test modules, project export, headless engine tests (Tests/run_tests.*) on the exported game.
release
Everything, Release
Unit tests, project export, headless engine tests on the exported game.
asan
Unit tests with AddressSanitizer, LeakSanitizer and UBSan (Linux / macOS)
Unit tests, leak self-check.
The default is all three (Windows: debug and release; there the CRT debug heap does the leak checking). Each configuration builds into
build/ci-<name>/, so they never overwrite each other or your own build folder.
Options
Option
Effect
--quick
Debug only, no engine run.
--no-engine
Skip the project export and the headless engine tests.
--no-export
Run the engine tests on the build folder’s GAM200_Game with --project instead of exporting first. Faster, but the project’s scripts only load if an editor compiled them into that build folder (see below).
--project <path>
Game project for the engine tests: the project folder or its *.astge file, anywhere on disk. Relative paths count from where you run the command, then from the repository. Default: GAM_PROJECT, else the first project inside the repository (MyGame/, projects/DataTest/, ...). The summary says which one ran.
--mutation-check
See below.
--results dir
Output folder (default test-results).
--timeout s
A step running longer than this is killed and fails (default 3600).
--quiet
Only print the step results; full output is still in the logs.
How the engine tests get your scripts
A game runs the project’s compiled scripts from CSharpRuntime/Game/GameScripts.dllnext to the executable, and only an
editor writes that file. A fresh build folder such as build/ci-debug has never had an editor open a project, so a game started from it
knows no components and runs no systems (unknown component 'Transform' skipped, systems_run stays 0).
So for every engine configuration, ci.py first exports the project with that configuration’s editor, exactly like
File › Build (GAM200_Editor --build), into build/ci-<config>/export/<Project>/, and runs the engine tests on the
exported game from its own folder, the way a player starts it. The tests then check the project’s own scripts, compiled the way a player gets them.
Your project is left as it was. Opening a project makes the editor rewrite ProjectSettings/Build.json, GameScripts.csproj and
.gitignore; ci.py puts all three back byte for byte afterwards, also when the export fails, and warns if anything else in the project changed.
Strip Debugger / Profiler turns tests off in an exported game, so the test export always has it off (and keeps the console window).
The summary says when your Build Settings had it on. Your real exports are not affected.
The export runs headless (GAM200_Editor --headless --build): no window, no display, no OpenGL, so it works the same on a Linux build server.
An editor built before headless export existed is detected within a second; ci.py then exports with an editor window instead (xvfb-run on Linux).
Logs: test-results/<config>/export.log (what the editor printed) and export-editor.log (its full log).
Testing a project that lives somewhere else
python Tests/ci.py --project "D:/My Games/BossRush" # the project folder
python Tests/ci.py --project "D:/My Games/BossRush/BossRush.astge" # or its .astge file
# PowerShell: the same project for every run in this window
$env:GAM_PROJECT = "D:/My Games/BossRush"
python Tests/ci.py
Put paths with spaces in quotes. A wrong path, or a folder without a *.astge, stops ci.py straight away instead of running without a project.
Running it on Windows
You need Python 3.9+, CMake, Visual Studio with Desktop development with C++, and the .NET SDK, all on PATH. A normal
PowerShell or Command Prompt in the repository root is enough: CMake finds Visual Studio itself.
Skipped with a message: AddressSanitizer runs on the Linux server. Debug builds use the MSVC CRT debug heap for leak checking instead.
First run
Slow: it configures and builds build/ci-debug and build/ci-release, C# runtime included. Later runs only rebuild what changed.
Headless without Mesa warning
Normal on Windows: the engine tests run without the renderer. C# systems, tests and the profiler still run.
Debugging a unit test
Each build/ci-* folder has a .sln. Set GAM200_UnitTests as the startup project and put the test’s name in Debugging › Command Arguments.
Results
End of a run
==========================================================================================
PASS debug: configure 4.1 s
PASS debug: build everything 96.3 s
PASS debug: unit tests 0.2 s 47 of 47 unit test case(s) passed in 41.2 ms
PASS debug: leak checker self-check 0.1 s deliberate leak was reported and failed the run as expected
PASS debug: native test modules 2.0 s
FAIL debug: engine tests (headless) 64.8 s 1 of 4 test case(s) failed
...
==========================================================================================
[ci] 11/12 steps passed in 412 s. Logs and JUnit XML: test-results (exit code 1)
test-results/
summary.json every step: config, name, ok, seconds, detail
debug/
configure.log build.log
unit-tests.log unit-junit.xml
leak-selfcheck.log
native-configure.log native-build.log
export.log export-editor.log the project export (editor --build)
engine-tests.log
engine/ junit.xml report.txt report.json trace.json engine.log (crash / hang dumps)
release/ ...
asan/ ...
debug/mutations/ one build + test log per deliberate bug (--mutation-check)
build/ci-<config>/ build output, git-ignored (Windows: a <Config> folder with GAM200_Game.exe, CSharpRuntime, ...)
build/ci-<config>/native/ CMake files of the native test modules
build/ci-<config>/export/ the exported game the engine tests run
Tests/native/build/ the native test modules themselves (run_tests loads them from here)
Proving the tests catch regressions
--mutation-check breaks the engine on purpose, one bug at a time, from Tests/unit/mutations.json. For each one it patches the
source, rebuilds the unit tests, and requires the run to fail. A bug that survives means no test protects that behaviour yet.
The real source is always restored, including after Ctrl+C or a killed run.
One entry of Tests/unit/mutations.json
{
"name": "SparseSet::remove forgets to re-point the element it moved",
"file": "Src/ECS/SparseSet.h",
"find": "sparse[backEntityID] = di;",
"replace": "/* mutation: moved element keeps its old index */"
}
find must appear exactly once in file. When someone edits that line, the entry fails with a clear message: update it to match.
Keep it out of git
Everything a run creates is ignored by the repository’s .gitignore: build/, test-results/, logs/,
*.dmp and the *.ci-mutation-backup files an interrupted --mutation-check can leave. If git lists files from a test run,
the .gitignore is out of date: don’t commit them.
On a build server
Install the same tools, check out the repository, run python Tests/ci.py, and keep test-results/ as the build's artifacts
(the *junit*.xml files are what most CI servers display). Nothing else is needed: no editor, no display, no prompts.
On a Linux server add Mesa for rendering, or the engine tests run without drawing.
Editor
Editor tools
Put scripts in <Project>/Editor/. They compile with your scripts, only in the editor, reload on save, and are never shipped.
Draw with Dear ImGui through Engine.UI.ImGui.
Editor/LevelTools.cs
using System.Linq;
using Engine.Core;
using Engine.Editor;
using Engine.UI;
using GeneratedECS;
[EditorWindow("Tools/Level Stats")]
public class LevelStats : EditorWindow {
public override void OnGUI() {
ImGui.Text($"Entities: {Selection.All.Count()}");
if (ImGui.Button("Select player"))
Selection.Select(Selection.Find(n => n.Name == "Player"));
}
}
public static class MyMenus {
[MenuItem("Tools/Create Spawn Point")]
static void CreateSpawn() => Selection.CreateEntity("SpawnPoint");
}
[CustomInspector(typeof(GeneratedECSRegistry.Enemy))]
public class EnemyInspector : ComponentInspector {
public override bool OnInspectorGUI() {
bool changed = DrawDefault(); // the normal fields
if (ImGui.Button("Full health")) { SetFloat("Health", 100); changed = true; }
return changed; // true = undoable edit
}
}
A tool that throws is logged with its file and line, and the editor's UI state is repaired.
Shipping
Exporting
File › Build Settings (Ctrl+Shift+B): product name, company, version, icon, start scene, window size, fullscreen, vsync, platform, output folder, and the two strip options below.
What goes into a build
The start scene and every scene, with everything they reference.
Every .anim.json clip and its texture.
Files named as a path in a script string, e.g. Audio.Create("Assets/Audio/jump.wav").
Everything under Resources/ and StreamingAssets/.
With Include All Assets ticked: every project file.
Platform
Output
Windows / Linux
Builds/<Platform>/ with the game executable, engine libraries, CSharpRuntime/ (release scripts, no Editor code), Assets/ and Data/ (your files and the manifest). Cross-export needs the other OS's engine build.
Android
An APK built with Gradle, optionally installed on the connected phone. C# runs on the .NET runtime packaged in the APK. One-time setup: Build Settings › Android › Set up.
Release builds
Option
Removes
Keep it off when
Strip Debug Logs
Debug.Log, LogWarning, Assert and DebugDraw calls (compiled out). Errors are still logged.
You want log output from testers.
Strip Debugger / Profiler
At start-up: profiler, hang watchdog, crash reports, debug console, test runner. Profiler.* and DebugConsole.* do nothing.
The build is graded or tested from the command line (--test then exits with code 2).
Both are off by default. Tick both for a fully quiet release. Engine options such as --headless and --frames keep working.
Android checklist
Press Logcat in the Build window before starting the game to see its log.
Folder names that differ only in capitalisation from the engine's (Audio vs audio) are handled; file names must match exactly.
Audio .ogg files must be Vorbis.
Large GIFs are shrunk to fit phone memory (manifest key animatedTextureBudgetMB).
Command line
GAM200_Editor --project MyGame/MyGame.astge --build Linux [--output dir] # opens the editor, exports, closes
GAM200_Editor --project MyGame/MyGame.astge --build Linux [--output dir] --headless # no window: build servers / CI
Both use the project’s Build Settings, with the target and output folder from the command line. With --headless the editor UI
never starts and the export runs on its own (HeadlessBuild.cs), so no display or OpenGL is needed. The exit code is 0 when the
export succeeded and 1 when it failed; the reason is in the [Build] lines of the log.
Shipping
Ids and rules of thumb
Id ranges
Kind
Range
Example
Engine components
below 1000 (built-ins 100–127)
Transform = 100
Game components
1000 and up
Enemy = 1100
Engine systems
900–999, reserved
PhysicsSystem = 910
Game systems
5000 and up
EnemySystem = 6000
Events
agree with the team; 0xFFFF0000 and up are the engine's
DamageEvent = 9001
Ask before taking a range: two components or systems with the same id are a compile error.
Rules of thumb
Data in components, behaviour in systems. No static fields for per-entity state.
Subscribe to events and callbacks in OnStart.
Use Entity fields, not names, for links that must survive saving.
Structural changes inside loops go through Commands, AddDeferred or Scenes.Spawn.
Commit Scenes/ Prototypes/ Components/ Scripts/ Editor/ ProjectSettings/ and your art. Ignore Library/ Builds/ obj/ bin/.
Never put test content in the engine folder.
Shipping
Troubleshooting
Symptom
Cause and fix
My change does nothing
The script failed to compile and the old version is still running. Check the Console.
Entity #12 has no Enemy component
Enemy.Get throws when the component is missing. Use TryGet.
Cannot modify members of 'e' because it is a 'foreach iteration variable'
Copy it first (var en = e;) or use a for loop with the indexer.
Key presses are missed or doubled
You read KeyPressed in OnFixedUpdate. Read it in Update.
Event handler runs twice
You subscribed in OnCreate or Update. Subscribe in OnStart.
Odd crashes or wrong data after adding a component in a loop
Use AddDeferred / Commands inside loops.
Spawned entity isn't there yet
Scenes.Spawn creates it at the next frame boundary; use the done callback.
Bodies don't fall
Rigidbody GravityScale defaults to 0. Set it to 1.
Clicks hit the game through a button
Check UI.PointerOverUI before handling game clicks.
The .ogg holds Opus or FLAC audio. Re-encode it as Vorbis.
GEN002 Duplicate component id
Two JSON files share an id. Pick a free one.
IntelliSense doesn't know a component
Save so the editor compiles once, then reload the project in the IDE.
Editor slows down when spawning thousands of entities
The profiler is collecting. Close the Watchdog panel or set Preferences › Debug › Profiler to Off. Also check you are measuring a Release build.
Headless without Mesa (no OpenGL) warning
The server has no OpenGL. Everything except drawing still runs; install Mesa for rendering and Render timings.
Headless test run exits with code 2
The game was exported with Strip Debugger / Profiler. Export again with it off.
hang: frame took ... stuck in Frame > Update > X
System X did not return for 5 seconds (an endless loop?). The log line names it; add --hang-abort 30 so automated runs never block.
A start-up failed dialog, or exit code 1 before the first frame
The dialog and the log say why (window / OpenGL, the C# runtime, a shader). See When start-up fails.
LEAK: n block(s) allocated with new in this test case were never deleted
That unit test (or engine code it called) allocated without freeing. See Finding a leak.
engine tests (headless) fails with the C# script runtime did not start
That build folder has no CSharpRuntime/ next to the executable. Re-run CMake so the C# step builds into the same folder (CMakeLists.txt passes it to build_csharp), then run again.
export project (editor --build) fails
The step shows the editor’s Build FAILED reason, for example a start scene that doesn’t exist (set it in File › Build Settings) or a script compile error. Full output: test-results/<config>/export.log. Export the project once from the editor to see the same message there.
The game ran without the project’s compiled scripts: you used --no-export with a build folder no editor compiled into. Drop --no-export; see How the engine tests get your scripts.
GAM200_Editor --headless --build loads the start scene and never exports
That editor was built before headless export existed. Rebuild it (the C# side compiles Editor/HeadlessBuild.cs), or run the export without --headless.
ci.py: no *.astge project found
Pass the project with --project path/to/ProjectFolder, or set GAM_PROJECT. See ci.py.
Git shows GameScripts.csproj as changed
Someone opened the project with a Release editor, or from another folder layout. Discard the change; see GameScripts.csproj and git.
mutation ...: SURVIVED in ci.py --mutation-check
No unit test notices that deliberate bug. Add a test for the behaviour.
mutation ...: `find` occurs 0 times
Someone edited the line the mutation patches. Update its entry in Tests/unit/mutations.json.
[ci] missing tools: dotnet
The full build compiles the C# runtime: install the .NET SDK.
Unit tests fail to link: undefined reference to ...
The ECS started calling a new engine function. Add a test double for it to Tests/unit/EngineSeams.cpp.
Reference
API reference
Every public member scripts can use, grouped by area. Signatures are taken from the engine source. Namespaces: Engine.Core for the runtime, GeneratedECS for generated components, Engine.Editor and Engine.UI for editor tools.
ECS
World
static classEngine.Core
Entity-level access usable from any system or callback. Entity ids are generation-checked handles (see Entities & handles).
IsAlive
bool IsAlive(ulong entity)
True while the entity exists. A handle to a destroyed entity stays false even if its slot is reused.
Has
bool Has(ulong entity, uint componentTypeId)
Whether the entity has the component. Prefer the generated X.Has(e).
CreateEntity
ulong CreateEntity()
A new empty entity (no components). If you add components to it right away, do it outside a system loop, or use Scenes.Spawn / Commands.
Destroy
void Destroy(ulong entity)
Destroys the entity. Deferred automatically while systems run. Children (via Parent) are destroyed with it.
Removes a component (deferred while systems run). Prefer X.Remove(e).
GetRaw
void* GetRaw(ulong entity, uint componentTypeId)
Raw pointer to the component bytes, or null. Low level; the generated views wrap this.
IsDispatching
bool IsDispatching { get; }
True while the engine is running system callbacks (structural changes are deferred then).
Describe
string Describe(ulong entity)
"#12" or "#12 (dead)", for log messages.
IndexOf
uint IndexOf(ulong handle)
Slot index part of a handle.
GenerationOf
uint GenerationOf(ulong handle)
Generation part of a handle.
Handle
ulong Handle(ulong index)
Handle for a slot index (used by generated code; scripts rarely need it).
GetComponentArray
void* GetComponentArray(uint componentTypeId, out int count, out ulong* entities)
Dense array of every component of one type. Low level; use X.All().
Commands
structEngine.Core
Deferred structural changes, safe inside a system loop. Add a Commands cmd parameter to any lifecycle method. Nothing happens until the engine flushes the queue (between phases and at the end of the frame stage).
Destroy
void Destroy(ulong entity)
Queues destroying the entity.
Remove
void Remove(ulong entity, uint componentTypeId)
Queues removing a component: cmd.Remove(e, Enemy.TypeId).
Add
void Add<T>(ulong entity, uint componentTypeId, in T data) where T : unmanaged
Queues adding (or overwriting) a component from its raw storage struct, copied immediately: var d = Enemy_Defaults(); d.Health = 50; cmd.Add(e, Enemy.TypeId, in d); String and list fields can't be set this way (fill them next frame).
IsAlive
bool IsAlive(ulong entity)
Same as World.IsAlive.
<Component> (generated)
structGeneratedECS.GeneratedECSRegistry
Every component JSON becomes a struct with the JSON's name. It is a view that points into the engine's memory: setting a property writes straight into the entity's data, even on a copy in a local variable. Shown here for a component called Enemy.
TypeId
const uint TypeId
The component's id from its JSON. Used with Commands and World.
EntityId
ulong EntityId { get; }
The entity this component belongs to.
IsValid
bool IsValid { get; }
False for a default/empty view (e.g. after a failed TryGet).
Get
static Enemy Get(ulong entity)
The entity's component. Throws if it has none; use TryGet/Has when unsure.
TryGet
static bool TryGet(ulong entity, out Enemy component)
The component if the entity has it.
Has
static bool Has(ulong entity)
Whether the entity has the component.
All
static EnemyCollection All()
Every Enemy in the world, from anywhere (not only as a system parameter).
Create
static Enemy Create(ulong entity)
Adds the component now with its JSON defaults. Not inside a system loop (adding can move component memory); use AddDeferred there.
AddDeferred
static void AddDeferred(ulong entity)
Same as Create but queued: safe inside systems; the component exists after the next flush.
Remove
static void Remove(ulong entity)
Removes the component (deferred while systems run).
field (number/bool/Vec2/Color)
float Health { get; set; }
One property per JSON field, typed by the field type (see field types).
field (enum)
EnemyState State { get; set; }
Enum fields get a generated enum named <Component><Field> (or enumName).
Asset fields: the project path as a string, plus <Field>Id, the raw asset id (fast to compare; 0 = none).
field (Entity)
ulong Target { get; set; }
Entity handle (0 = none). Check World.IsAlive before using it.
Enemy_Defaults
static Enemy_RawStorage Enemy_Defaults()
The JSON defaults as a raw struct, for Commands.Add.
<Component>Collection (generated)
structGeneratedECS
What a system gets for its EnemyCollection parameter, and what Enemy.All() returns. Iterate with for + indexer or foreach.
Length
int Length { get; }
Number of components.
this[int]
Enemy this[int index] { get; }
The component in slot index. Writable through the view: var e = enemies[i]; e.Health -= 1;
EntityAt
ulong EntityAt(int index)
Entity id of slot index (same as enemies[i].EntityId).
Changed
bool Changed(int index)
True if slot index was added or modified since this system last ran. Always true unless the JSON sets "trackChanges": true.
IsTracked
bool IsTracked { get; }
Whether change tracking is on for this component.
GetEnumerator
Enumerator GetEnumerator()
foreach (var e in enemies). The loop variable is read-only: to write, copy it first (var en = e; en.Health = 0;) or use the indexer.
Query<A, B, C, D>
structEngine.Core
System parameter that iterates entities having all listed components (1 to 4). The first type drives the loop; the others are looked up per entity and entities missing any are skipped, so put the rarest first.
foreach
foreach (var row in q)
Query<A> yields A. 2-4 types yield a QueryRow: deconstruct with var (a, b) = row; or var (entity, a, b) = row;.
Length
int Length { get; }
Entities handed over by the engine, before the other components and [Without] are checked.
EntityAt
ulong EntityAt(int index)
Entity id of slot index.
Changed
bool Changed(int index)
Change detection for the first component; use with row.Index.
QueryRow<A, B, ...>
structEngine.Core
One entity of a multi-component query.
Entity
readonly ulong Entity
The entity.
Index
readonly int Index
Slot index (for q.Changed(row.Index)).
V0..V3
readonly A V0; readonly B V1; ...
The component views.
Deconstruct
void Deconstruct(out A a, out B b) / (out ulong entity, out A a, out B b)
Enables var (a, b) = row;.
System & event attributes
attributesEngine.Core
Attributes that turn classes and structs into systems, filters and events.
ECSSystem
[ECSSystem(uint systemId, Phase = SystemPhase.Update, Order = 0)]
Marks a static class as a system. The id must be unique (game systems 5000+, engine 900-999 reserved). Lower Order runs first inside a phase.
On a system class: its Query parameters skip entities that have any of these components. Not applied to Collection parameters.
ECSEvent
[ECSEvent(uint eventId)]
Marks a blittable struct as an event. Unique id; 0xFFFF0000 and above are reserved for the engine.
PersistentResource
[PersistentResource]
On a resource class: it survives Play/Stop instead of being reset.
Events & state
Events
static classEngine.Core
The shared C++/C# event bus. Events are queued and delivered in order when the engine dispatches (start of the frame and after LateUpdate), outside any system loop.
Publish
void Publish<T>(in T e) where T : unmanaged
Queues an event (copied immediately).
Subscribe
EventSubscription Subscribe<T>(Action<T> handler, bool persistent = false) where T : unmanaged
Listens for T. Cleared automatically when Play stops, so subscribe from OnStart. persistent: true survives Stop (editor tools).
Unsubscribe
void Unsubscribe(EventSubscription sub)
Stops listening (same as sub.Dispose()).
EventSubscription
structEngine.Core
Returned by Events.Subscribe.
IsValid
bool IsValid { get; }
False for a default value.
Dispose
void Dispose()
Unsubscribes.
Resources
static classEngine.Core
Global singletons that belong to no entity (score, settings, spawn tables). Plain classes, created on first use, reset when Play stops unless marked [PersistentResource].
Get
T Get<T>() where T : class, new()
The instance, created on first use: Resources.Get<GameState>().Score += 10;
TryGet
bool TryGet<T>(out T value) where T : class
The instance if it exists, without creating it.
Set
void Set<T>(T value) where T : class
Replaces or provides the instance (e.g. one loaded from a file).
Remove
bool Remove<T>() where T : class
Removes it.
Session
static classEngine.Core
Play-session plumbing.
FrameEnd
event Action<float> FrameEnd
End of every simulated frame, after LateUpdate, outside any system loop (safe for structural changes). Argument: delta time.
Ended
event Action Ended
The play session ended (editor Stop, scene teardown).
IsPlaying
bool IsPlaying { get; }
True while a play session runs (always true in an exported game).
FrameEndEvent
eventEngine.Core
[ECSEvent(0xFFFF0300)] published by the engine once per simulated frame.
fields
float DeltaTime, UnscaledDeltaTime
Scenes & app
Scenes
static classEngine.Core
Load scenes and spawn prototypes at runtime (exported game and editor Play mode). Names may be bare ("Level2"), with folders, with or without the extension.
Load
void Load(string scene)
Queues a scene change for the next frame boundary. "Level2" finds Scenes/Level2.scene.json.
Reload
void Reload()
Restarts the current scene.
Current
string Current { get; }
Loaded scene, e.g. "Scenes/Level1" ("" = none).
IsLoading
bool IsLoading { get; }
True during a load.
Find
ulong Find(string name)
First entity of the current scene with that editor name, or 0.
Spawn
void Spawn(string prototype, float? x = null, float? y = null, Action<ulong> done = null)
Spawns a .proto.json at the next frame boundary. Safe inside systems. done receives the new root entity (0 on failure).
Makes child a child of parent. keepWorld: the child stays where it is. Returns false for dead entities, missing Transforms or cycles. The first call on an entity adds a Parent component immediately: call it from OnStart/events, not inside a loop over that entity.
ClearParent
void ClearParent(ulong child)
Detaches; the child keeps its world transform.
TryGetParent
bool TryGetParent(ulong child, out ulong parent)
The parent if linked and alive.
Application
static classEngine.Core
The running game.
Quit
void Quit()
Exported game: closes it. Editor: stops Play mode. Raises Quitting and saves SaveData if AutoSave.
Quitting
event Action Quitting
Raised when Quit is called.
IsEditor
bool IsEditor { get; }
Running inside the editor.
IsExportedGame
bool IsExportedGame { get; }
IsMobile
bool IsMobile { get; }
Platform is Android.
Platform
string Platform { get; }
"Windows", "Linux", "Android", ...
ProductName
string ProductName { get; }
From Build Settings.
Company
string Company { get; }
Version
string Version { get; }
CommandLine
string[] CommandLine { get; }
The process command line (argv[0] first).
OpenURL
void OpenURL(string url)
Opens a web page or file with the OS (desktop).
Screen
static classEngine.Core
The game window. Ignored inside the editor; Android is always fullscreen.
Fullscreen
bool Fullscreen { get; set; }
Borderless fullscreen on/off.
ToggleFullscreen
void ToggleFullscreen()
What F11 / Alt+Enter do.
CursorVisible
bool CursorVisible { get; set; }
Show/hide the OS cursor.
Width / Height
int Width { get; } int Height { get; }
Game view size in pixels.
ProjectSettings
static classEngine.Core
The project manifest (.astge) written by the Build window.
Delays and repeats without countdown fields. Game time unless unscaled: true. Callbacks run at the end of the frame, outside system loops. All timers are cancelled when Play stops.
Scaled seconds since the session started (same clock as Cooldown).
Count
int Count { get; }
Active timers (debugging).
TimerHandle
structEngine.Core
Returned by Timers.After / Every.
IsActive
bool IsActive { get; }
Cancel
void Cancel()
None
static readonly TimerHandle None
Cooldown
structEngine.Core
Reusable cooldown on game time: if (cd.TryUse()) Fire();
ctor
Cooldown(float duration)
Duration
float Duration
TryUse
bool TryUse()
True (and restarts) if it was ready.
IsReady
bool IsReady { get; }
Remaining
float Remaining { get; }
Seconds until ready.
Progress
float Progress { get; }
0 right after use, 1 when ready (UI fill bars).
Trigger
void Trigger()
Starts the cooldown without checking.
Reset
void Reset()
Ready now.
Input
Input
static classEngine.Core
Polled input. Pressed/Released and deltas last one frame: read them in Update, not OnFixedUpdate. Coordinates are window pixels (the Game view in the editor), origin top-left, y down.
KeyDown
bool KeyDown(Key k) / KeyDown(int scancode)
Key is held.
KeyPressed
bool KeyPressed(Key k)
Went down this frame.
KeyReleased
bool KeyReleased(Key k)
Went up this frame.
KeyTyped
bool KeyTyped(Key k)
Pressed this frame including key repeat (text editing).
Axis
float Axis(Key negative, Key positive)
-1 / 0 / +1, e.g. Axis(Key.A, Key.D).
MoveVector
Vec2 MoveVector { get; }
WASD / arrows as a vector (x right, y up), length ≤ 1.
2D boxes and circles, rigidbodies, contacts, raycasts and overlaps. Bodies are simulated at 60 Hz in PostUpdate. Rotation is not simulated. Handlers run at the end of the frame (safe to destroy/spawn).
Gravity
static Vec2 Gravity = (0, -980)
World units/s². Multiplied by each body's GravityScale (default 0 = top-down).
Logging. Log / LogWarning / Assert are compiled out of exports with Strip Debug Logs on. LogError / LogException always stay and go to log.txt in the save folder. For timings use Profiler.
Every system callback, event handler, timer and UI callback is wrapped: an exception is logged with its file and line and the game keeps running.
ErrorRaised
event Action<Exception> ErrorRaised
Count
int Count { get; }
Errors this run.
Profiler
static classEngine.Core
Scoped timers, counters and markers for scripts. They show up in the Watchdog panel, the terminal summary of headless runs, the report and the Chrome trace. Systems are timed automatically; use this for your own code inside them.
Scope
Zone Scope(string name)
Times the code until the returned value is disposed: using (Profiler.Scope("Pathfinding")) { ... }. Nested scopes nest in the trace.
Begin / End
void Begin(string name) / void End()
Same without using; every Begin needs an End on the same frame.
Counter
void Counter(string name, double value)
A value per frame (last write wins): history, min, max and average are kept and graphed.
Mark
void Mark(string text)
An instant event in the Events tab and the trace.
Stat
double Stat(string name)
Any profiler value by name, NaN if unknown: fps_avg, frame_ms_p99, rss_mb, entities, system_ms:<System>, zone_ms:<Scope>, your counters (suffix .avg / .min / .max). Same names as test scripts' expect.
DebuggerEnabled
bool DebuggerEnabled { get; }
False in an export built with Strip Debugger / Profiler; every Profiler call then returns at once.
Zone
readonly struct Zone : IDisposable
Returned by Scope.
DebugConsole
static classEngine.Core
Debug console commands shared by the Watchdog Console tab, the terminal of headless runs, --exec and test scripts. See Debug console commands.
void Equal<T>(T expected, T actual, string what = "value") / void Near(double expected, double actual, double tolerance, string what = "value")
TestAssert.Fail
void Fail(string message)
TestAssert.Stat
double Stat(string name)
Same as Profiler.Stat.
DebuggerNative
static classEngine.Core
Read side of the native debugger, used by the Watchdog panel. Editor tools can use it for their own displays. Struct layouts mirror the engine's (DebugNatives.cpp).
Available
bool Available { get; }
False when the engine has no debugger natives.
Frame
FrameInfo Frame()
Frame number, frame / work ms, fps, avg / p99 / max ms, CPU %, hitch / hang / error / warning counts, paused, headless, rendering (1 drawing, 0 --no-render, -1 no OpenGL), tracing, table sizes.
Fields of the engine's own components, as defined in EngineComponents/*.json. Use them from C# exactly like your own: Transform.Get(e).PosX, Sprite.TryGet(e, out var s).
Transform
id 100Core
Field
Type
Default
Notes
PosX
float
0
World position (units)
PosY
float
0
Rotation
float
0
Degrees, counter-clockwise
ScaleX
float
1
1 = 100 world units
ScaleY
float
1
Sprite
id 101Core
Field
Type
Default
Notes
Texture
Texture
–
Image to draw. Empty = the engine's missing-texture image.
Color
Color
#FFFFFFFF
Tint multiplied with the texture (alpha = transparency)
FlipX
bool
false
FlipY
bool
false
Frame
int
-1
Sprite-sheet cell (-1 = whole image). Columns/rows come from the image .meta.
Order
int
0
Draw order: higher draws on top
Parent
id 102Core
Field
Type
Default
Notes
Entity
ulong
0
Parent entity (read-only; set by dragging in the Hierarchy)
Linked
int
0
Hidden
LocalX
float
0
Offset relative to the parent
LocalY
float
0
LocalRotation
float
0
LocalScaleX
float
1
LocalScaleY
float
1
Camera2D
id 103Core
Field
Type
Default
Notes
Zoom
float
1
Range 0.05..20. 2 = everything twice as big
Primary
bool
true
The camera the game renders with (first primary one wins)
BackgroundColor
Color
#1E1E26FF
Follow
Entity
0
Entity to follow
FollowSpeed
float
0
0 = snap; higher = tighter
Offset
Vec2
0, 0
Added to the followed position
UseBounds
bool
false
BoundsMin
Vec2
-1000, -1000
BoundsMax
Vec2
1000, 1000
Animator
id 104Core
Field
Type
Default
Notes
Clip
Animation
–
Clip to play (.anim.json). Code: Animator.Play(entity, "Run")
Speed
float
1
Range 0..5
Time
float
0
Read-only: seconds into the clip
Frame
int
0
Read-only: current frame
Playing
bool
true
Collider
id 110Physics
Field
Type
Default
Notes
Shape
enum ColliderShape
Box
Box, Circle
Size
Vec2
1, 1
Box size in units of the Transform scale (1, 1 = the sprite quad)
Radius
float
0.5
Circle radius relative to the Transform scale (0.5 = fits the quad)
Offset
Vec2
0, 0
Centre offset in world units
IsTrigger
bool
false
Reports overlaps but never pushes
Layer
int
0
Physics layer 0..31
Rigidbody
id 111Physics
Field
Type
Default
Notes
Type
enum BodyType
Dynamic
Dynamic: velocity, gravity and collisions. Kinematic: velocity only, pushes others. Static: never moves.
Velocity
Vec2
0, 0
World units per second
GravityScale
float
0
0 = top-down. Multiplies Physics.Gravity
Mass
float
1
Range 0.01..1000
Drag
float
0
Velocity damping per second (0..20)
Bounciness
float
0
0..1
FreezeRotation
bool
true
Hidden
Tag
id 112Physics
Field
Type
Default
Notes
Value
string
–
Find with Tags.Find("Player") / Tags.Has(e, "Enemy")
Canvas
id 120UI
Field
Type
Default
Notes
ReferenceWidth
float
1280
UI is laid out for this resolution and scaled to the window
ReferenceHeight
float
720
Match
float
0.5
0 = scale with width, 1 = with height
SortOrder
int
0
RenderMode
enum CanvasRenderMode
ScreenSpace
ScreenSpace or WorldSpace (placed at this entity's Transform)
WorldScale
float
1
WorldSpace only: world units per reference pixel
Layer
enum CanvasLayer
AboveSprites
AboveSprites (HUD) or BehindSprites (backgrounds)
UIRect
id 121UI
Field
Type
Default
Notes
AnchorMin
Vec2
0.5, 0.5
Lower-left anchor in the parent (0,0 bottom-left, 1,1 top-right)
AnchorMax
Vec2
0.5, 0.5
Upper-right anchor. Different from AnchorMin = stretch
Pivot
Vec2
0.5, 0.5
Position
Vec2
0, 0
Offset of the pivot from the anchors (reference pixels, y up)
Size
Vec2
160, 40
Reference pixels (added to the anchor span when stretching)
Visible
bool
true
UIImage
id 122UI
Field
Type
Default
Notes
Texture
Texture
–
Empty = plain colour rectangle
Color
Color
#FFFFFFFF
Fill
float
1
Horizontal fill 0..1 (health bars)
Frame
int
-1
Sprite-sheet cell (-1 = whole image)
Animate
bool
true
Play animated GIF / APNG
Speed
float
1
GIF playback speed
UIText
id 123UI
Field
Type
Default
Notes
Text
string
Text
Font
Font
–
Empty = engine default font
FontSize
float
24
4..200
Color
Color
#FFFFFFFF
Align
enum UITextAlign
Center
Left, Center, Right
VAlign
enum UITextVAlign
Middle
Top, Middle, Bottom
Wrap
bool
false
UIButton
id 124UI
Field
Type
Default
Notes
Interactable
bool
true
Normal
Color
#3A3F4BFF
Hover
Color
#4C5466FF
Pressed
Color
#2A2E37FF
Disabled
Color
#2A2E3780
Action
string
–
Name sent with the click: UI.OnClick("Play", ...)
NormalImage
Texture
–
Optional 9-slice image; brightened / darkened for hover / press