Files

11 KiB

Runtime API reference

Include <faset/runtime/Runtime.hpp> and use namespace faset::runtime. This is the implemented C++ surface used by the compiled tutorials. The runtime is sequential; call it from its owning thread.

Register behavior

void Runtime::registerBehavior(std::string componentType, Behavior behavior) registers callbacks before scene loading. An empty or duplicate type and registration while entities are present or a callback is running are rejected. An empty or cleared world can register additional types.

Behavior::Callback is std::function<void(Runtime&, EntityHandle, double)>. Assign it to any of onStart, fixedUpdate, update, lateUpdate, and onDestroy. Unassigned members do nothing. Behavior::onCollision instead accepts (Runtime&, EntityHandle, const CollisionEvent&).

See callback order and the complete registration example.

Resolve identity

  • EntityHandle find(const std::string& persistentId) const returns a handle, or an empty handle when absent.
  • bool valid(EntityHandle) const noexcept checks session, entity validity, and generation.
  • std::uint64_t session() const noexcept identifies this runtime session, not the saved scene.

A handle contains session, slot, and generation. Its Boolean conversion says it is nonempty; it does not prove that the object is still alive. Call valid before using a retained handle. A handle from another runtime or an earlier load is rejected.

Read configuration and poses

  • nlohmann::json fields(EntityHandle, const std::string& componentType) const returns a configuration copy. It throws if the type is absent.
  • Transform transform(EntityHandle) const returns a simulation-pose copy.
  • Transform presentation(EntityHandle) const returns the pose prepared for display.
  • void setTransform(EntityHandle, const Transform&) updates a non-physical object.
  • void setPresentation(EntityHandle, const Transform&) updates presentation only, during lateUpdate.

Transform has position, rotation, and scale, each a std::array<float, 3>. Position is in metres; rotation is XYZ Euler radians with matrix composition Rz * Ry * Rx; scale is a multiplier. The pose is local to its scene parent. The renderer composes the hierarchy. The initial physics adapters require root objects.

These are values, not borrowed component pointers. Changing a returned copy has no effect until an appropriate setter is called. Presentation writes do not alter physics or the saved scene.

Control a rigid body

  • Vec3 velocity(EntityHandle) const reads metres per second. A 2D body returns Z = 0.
  • void setVelocity(EntityHandle, Vec3) sets linear velocity; it does not take a displacement.
  • void applyImpulse(EntityHandle, Vec3) applies an impulse at the centre and wakes the body.
  • void teleport(EntityHandle, const Transform&) changes the pose discontinuously, wakes the body, and resets interpolation/contact-query history. It does not zero velocity.
  • bool grounded(EntityHandle) const tests support using recent native contact normals.

All methods require a valid handle. Velocity, impulse, and grounded queries also require a physics body; teleport accepts physical and non-physical objects. Ordinary setTransform is rejected for physical objects, including static and kinematic bodies. See physics for dimensions, tolerances, and collider limits.

Input and collision events

InputState input() const noexcept returns horizontal, vertical, jumpPressed, and interactPressed. The Player maps A/D and left/right arrows to horizontal input, W/S and up/down arrows to vertical input, Space to jump, and E to interact. The gameplay module decides what these actions do.

CollisionEvent contains first, second, and began. onCollision receives an event during post-physics delivery. Its reference lasts for that callback; copy the event if you need to retain it, then recheck retained handles before later use.

const std::vector<CollisionEvent>& collisions() const noexcept exposes the most recently completed tick's events. The vector is replaced on a later tick or scene replacement. Polling only once per rendered frame can miss an earlier tick in a multi-tick frame; use callbacks when every delivered event matters. This is contact begin/end notification, not a general event bus or a contact-normal query.

Queue structural changes

  • void spawn(nlohmann::json entity) queues an entity record with id, name, parent, and components.
  • void destroy(EntityHandle) queues removal of the object and its descendants.
  • void addComponent(EntityHandle, nlohmann::json component) queues a full component record.
  • void removeComponent(EntityHandle, const std::string& componentType) queues removal by type.

Changes are applied in FIFO order at the next fixed-tick barrier. spawn does not return an immediately usable handle; use find(id) after application. A spawned child's parent must already exist when its command is applied. These commands are individual runtime operations, not an atomic authoring batch with Undo.

Immediate validation errors throw. Deferred failures are recorded in diagnostics; a stale command does not revive an object. IDs and component types must not collide. The spawning example demonstrates the timing.

Drive a world or a test

Runtime(RuntimeConfig = {}) constructs the controller. load(const nlohmann::json&) validates and prepares a scene, creates its entities, then calls initial callbacks. Invalid scene data leaves the preceding world intact. clear() destroys the current entities and invalidates their session handles.

Direct callers can include <faset/runtime/schema.hpp> and call validate_scene_schemas(scene, gameplaySchema) before load. The Player does this automatically: each custom TypeId/version must match its linked schema. Generic Runtime accepts opaque custom data with positive versions, including data-only components; built-ins use version 1. This helper verifies identity/version, without automatic migrations or authoring-style custom-field constraint validation.

FrameStats advance(double elapsedSeconds, InputState = {}) advances fixed ticks, frame callbacks, interpolation, and late callbacks. singleStep(InputState = {}) advances one fixed tick. setPaused(bool) clears accumulated time and resets presentation history; paused() reports this local state. Do not call load/clear/advance recursively from a callback.

RuntimeConfig defaults to fixedDelta = 1.0 / 60.0, maxCatchUpTicks = 4, physicsSubsteps = 4, and gravity = {0, -9.81f, 0}. FrameStats reports fixed ticks performed, dropped time, interpolation fraction, and total tick count.

snapshot() returns a value snapshot for rendering; snapshotJson() provides its JSON representation. diagnostics() returns a read-only vector of runtime messages. These are native C++ APIs for the Player and tests, not MCP endpoints.

Editor data migrations

Gameplay schema versions describe saved component data. When a field changes units or meaning, increase the type's version and include declarative migrations in that type returned by gameplay::schema(). For example, this type declaration converts version 1 speed values from centimetres per second to metres per second:

{
  "id": "game.mover",
  "version": 2,
  "fields": {
    "speed": {"type": "number", "default": 2.5, "min": 0, "max": 10},
    "enabled": {"type": "boolean", "default": true}
  },
  "migrations": [
    {
      "from_version": 1,
      "fields": {
        "speed": {"scale": 0.01},
        "enabled": {"default": true}
      }
    }
  ]
}

Build C++ to export and validate the declaration. The Editor reads these rules from the same schema manifest as field metadata; it does not load the gameplay library or execute a migration callback. Invalid metadata or migration rules fail the build before replacing the last published binary/schema generation.

Each step upgrades from_version to the next integer version. A component at version 1 needs both steps 1 and 2 to reach version 3. Empty fields explicitly allows a version step with no value conversion. Supported field operations are:

  • default: insert a value only when the field is absent.
  • scale: multiply an existing numeric field by a finite number.
  • require_manual: when true, stop if the field is present, so incompatible data requires an explicit manual conversion.

Rules preserve component/entity IDs and fields they do not mention. The final values must satisfy the current field schema. Unsupported operations, repeated steps, invalid version ranges, invalid rules and non-finite scale values are rejected when the schema is loaded. Arithmetic overflow during conversion also fails without applying the transaction.

Opening or recovering a scene preserves older component versions as opaque data; it never migrates them automatically. Missing rules therefore do not prevent opening the scene. Choose Migrate to v… in the Inspector after rebuilding the schema, or use the same editor's faset_scene_edit operation with the current document revision:

{
  "document": "document-id",
  "revision": 3,
  "operations": [
    {"op": "component.migrate", "entity": "entity-id", "component": "component-id"}
  ]
}

The whole batch is one Undo step and is written to the recovery journal. Save explicitly to update the scene file. Missing steps, a manual-conversion requirement or a validation error leave the document and revision unchanged. Future versions cannot be downgraded. The same operation accepts an instance ID when entity and component identify a top-level instance-local addition using its original stored IDs, rather than resolved preview IDs.

Inherited components belong to their source document: open that source to migrate them. Sparse field overrides in other instances are not automatically converted; review and explicitly update overrides when changing a field's units or meaning. The Player performs no migration and still requires the scene version to match its linked gameplay schema before Play or exported-game validation.

Inspect a running Player locally

The Player has local development controls in addition to gameplay input: P toggles pause, N steps one fixed tick while paused, and Escape closes the Player. F3 toggles physics-box outlines; --debug-physics enables them from startup. Static bodies are green, kinematic bodies orange, and dynamic bodies cyan.

These outlines use current physics poses and collider half-extents multiplied by the absolute transform scale. They can differ slightly from an interpolated visible mesh. The initial adapters support root-only boxes: four outline edges in 2D, twelve in 3D. This view does not show contact normals, broad-phase cells, or arbitrary mesh colliders.

To record measurements, run the Player with --profile measurements.json --frames 240. A profile requires an explicit count from 1 to 100,000. As with all bounded Player runs, simulation uses the configured fixed delta each frame; --headless renders through offscreen Vulkan. The report contains measured durations rather than treating that simulation delta as frame time. See Player profiling for startup, CPU/GPU timing, percentiles, and their limits. These flags do not add MCP to the Player.