174 lines
11 KiB
Markdown
174 lines
11 KiB
Markdown
# 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](lifecycle.md) and the complete [registration example](first-behavior.md).
|
|
|
|
## 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](physics.md) 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](examples.md#spawn-and-destroy-on-safe-boundaries) 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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](../editor/profiling.md)
|
|
for startup, CPU/GPU timing, percentiles, and their limits. These flags do not add MCP
|
|
to the Player.
|