Checkpoint 4: harden native paths, authoring workflows and Player lifecycle

This commit is contained in:
Emil
2026-09-18 05:14:50 +03:00
parent 5ac4db438d
commit 45bc352d46
88 changed files with 12606 additions and 504 deletions
+8 -1
View File
@@ -4,7 +4,7 @@ Include `<faset/runtime/Runtime.hpp>` and use namespace `faset::runtime`. This i
## Register behavior
`void Runtime::registerBehavior(std::string componentType, Behavior behavior)` registers callbacks before scene loading. An empty or duplicate type, registration during a callback, and registration after entities have loaded are rejected.
`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&)`.
@@ -63,6 +63,13 @@ Immediate validation errors throw. Deferred failures are recorded in diagnostics
`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.
+5 -2
View File
@@ -42,7 +42,7 @@ Read the callback from top to bottom:
4. `transform` gives a pose copy. Multiplying metres per second by seconds gives a displacement in metres.
5. `setTransform` publishes the changed non-physical pose.
The `[](...) { ... }` expression is a C++ lambda: a function stored in `Behavior::update`. Empty brackets mean it captures no local variables. `registerBehavior` takes ownership of the callback object. Register before calling `load`; registration after a world has loaded is rejected.
The `[](...) { ... }` expression is a C++ lambda: a function stored in `Behavior::update`. Empty brackets mean it captures no local variables. `registerBehavior` takes ownership of the callback object. Register before calling `load`; registration while entities exist or a callback is running is rejected.
The `schema()` function describes editable configuration. It does not create a runtime object. `tutorial.move_x` is the stable `TypeId`; `speed` is a stable `FieldId` within that type. Keep these IDs when changing a display label. Changing a field's meaning or units needs an explicit data migration, not just a new label.
@@ -56,7 +56,10 @@ The example scene is a complete, loadable document:
The sprite is visible because it has `faset.sprite`. It moves because it also has `tutorial.move_x`. The configuration field is `speed`; the type string must match the registration exactly. `rotation` uses radians, and the default coordinate system is Y-up.
After your project's schema is exported and loaded by the Editor, the type can be described through the same authoring schema used by the Inspector. The direct Player command above is useful before building an Editor workflow around your component.
After **Build C++** succeeds in the Editor, choose **Add component** in the Inspector
and select the registered type. Its schema supplies editable fields, defaults and
constraints. The direct Player command above is useful for testing the same behavior
independently of an editor session.
## Make a change and verify it
+7
View File
@@ -23,6 +23,13 @@ A gameplay directory contains `Gameplay.hpp` and `Gameplay.cpp`. It provides two
The Player calls registration before loading a scene. The separate SchemaExporter calls `schema()` without creating a game world or running gameplay callbacks. The Editor reads the resulting declaration; it does not load the gameplay binary into the Editor process.
Every custom component in a played scene must have a matching TypeId and exact
positive version in the linked gameplay `schema()`. The Player checks this before
loading the world, including `--validate`. A data-only custom component still needs
a schema, even when it registers no callbacks. Built-in TypeIds are reserved. The
Editor preserves missing or future component data for recovery, but the Player
rejects it until the corresponding module/schema is available or the data is migrated.
A scene object receives a behavior by containing a component whose `type` matches the string passed to `registerBehavior`. Registering a behavior does not attach it to every object. A component can contain data without having any callbacks.
## Data, poses, and state
+7 -1
View File
@@ -10,6 +10,12 @@ Each ordinary callback receives `(Runtime& game, EntityHandle self, double delta
`onDestroy` runs while that object's handle and allowed component data are still valid. Clean up subscriptions or external C++ state there. After removal, `valid(oldHandle)` returns false. Removing a behavior component also runs that component's `onDestroy`. Clearing or replacing a scene runs destruction callbacks; a new scene uses a new session identity.
Normal Player shutdown and the Editor's **Stop** request also run destruction callbacks.
Stop gives the Player a bounded grace period to finish. An unresponsive native callback
can force the Editor to terminate the process; no application can guarantee cleanup
callbacks after forced termination or a crash. Such fallback is reported in Console.
Exceptions caught from callbacks, including shutdown callbacks, appear in Player logs.
Registering a callback does not make captured pointers safe. A lambda that stores a reference to a stack variable in `registerGameplay` will outlive that variable. The [timed-despawn example](examples.md#spawn-and-destroy-on-safe-boundaries) uses shared ownership for captured state and removes each object's entry on destruction.
## One fixed tick
@@ -58,4 +64,4 @@ The Player reads this optional object from the scene document. The settings are
}
```
This is an excerpt, not a complete scene. The tutorial scenes contain complete examples. `fixed_delta` is seconds, gravity is metres per second squared, and substeps are solver subdivisions inside one fixed tick. These do not create additional gameplay callbacks. Invalid configuration is rejected before the Player starts. Editing these JSON settings is implemented; an Editor settings panel should only be relied on where the current UI exposes it.
This is an excerpt, not a complete scene. The tutorial scenes contain complete examples. `fixed_delta` is seconds, gravity is metres per second squared, and substeps are solver subdivisions inside one fixed tick. These do not create additional gameplay callbacks. Invalid configuration is rejected before the Player starts. The Editor's **Simulation** dialog edits these scene settings through normal Undo transactions; **Project settings** edits the project's name, dimension and start scene separately.