Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export

This commit is contained in:
Emil
2026-09-18 03:40:15 +03:00
parent 5c6b24d34d
commit 999686a896
125 changed files with 16086 additions and 1714 deletions
+54 -3
View File
@@ -28,9 +28,60 @@ Observed validation on Linux:
- MkDocs strict build passed with MkDocs 1.6.1 and Material 9.7.7.
The full editor, user-project build pipeline, MCP integration, standalone exports,
and Windows acceptance are still being implemented. This checkpoint is not the MVP release.
and Windows acceptance were still being implemented at that checkpoint. Later progress is recorded below.
Known intermediate constraints include box-only physics colliders, root-level physics
objects, static glTF triangles/UV0, a conservative serial renderer, and unfinished
world-preserving authoring reparent operations. These remain implementation work or
objects, static glTF triangles/UV0, a conservative serial renderer, and then-unfinished
world-preserving authoring reparent operations (completed in checkpoint 2). These remain implementation work or
explicit profile limits to review during final acceptance.
## Checkpoint 2 — integrated Editor, gameplay iteration and export
Implemented and integrated:
- Separate Player and SchemaExporter executables; statically linked project gameplay,
configurable simulation settings, real contact-based grounded queries, and four
compiled scripting tutorials embedded directly into the English MkDocs manual.
- Shared authoring commands and a real stdio MCP server, optimistic revisions,
transactional retries, jobs/cancellation, recovery of unsaved documents, and
viewport capture in graphical sessions only.
- Cancellable native subprocess execution, incremental project builds, schema export,
binary scene/resource packaging, distinct Debug development and Release export
directories, immutable published generations and retained last-good builds.
- A native retained UI with pinned FreeType/HarfBuzz and bundled Noto Sans:
scene tree, schema Inspector, viewport camera/picking/gizmos, assets, diagnostics,
build/play controls, commands, recovery and startup-loaded extension action panels.
- Exact-build native Editor SDK, dependency validation, owner-bound registrations,
example Beacon runtime component/editor command/panel, and unknown-data preservation.
- Full world-preserving TRS reparent with explicit rejection of unsupported shear.
- Read-only cooked asset target for Player. Import/build/editor/MCP services remain
outside the shipping runtime dependency graph.
Observed validation:
- Integrated Linux Clang 21 build: 19 CTests pass, including GPU rendering, retained
widgets, Editor authoring interaction, actual plugin loading, real MCP stdio,
process/cook contracts, and all four compiled gameplay tutorials.
- Real 2D and imported-glTF 3D standalone exports render with zero Vulkan validation
errors on NVIDIA RTX 2080 Ti. Changing gameplay rebuilds metadata; failed C++
compilation preserves the last successful published build.
- Runtime/tutorial checks also pass with Clang 18; runtime ASan/UBSan checks pass.
- Unmodified Blender 4.5.3 exports through the optional add-on. Real Editor imports
preserve output IDs after rename/geometry edits, report deleted-output conflicts,
retain the last generation on failure, and preserve separate scene placement/color.
Reproduce with `tools/verify_blender_roundtrip.py --blender PATH --editor PATH`.
- Strict MkDocs build passes. Tutorial source snippets are compiled by CTest.
- Previous checkpoint headless Linux and Windows GitHub CI passed after portability
fixes. New checkpoint and full Windows graphical/export checks are separate work;
Linux validation does not imply Windows validation.
This is an implementation checkpoint, not an MVP release. Finished playable sample
projects, complete Windows graphical/export acceptance, fresh-install checks,
performance measurements, and final UX review remain. Standalone image import is
integrated but its dedicated PNG/JPEG edge-case checks are the next asset task.
The baseline profile currently uses box colliders, root-level rigid bodies, static
triangle glTF meshes/UV0, basic PBR/directional shadows and a conservative serial
Vulkan renderer. Advanced rendering and broader content profiles remain later work.
The generated UI reference determines visual direction only; architecture, behavior
and acceptance criteria remain authoritative.
+58
View File
@@ -0,0 +1,58 @@
# Native Editor extensions
Editor extensions are startup-loaded `.so`/`.dll` modules. They are trusted native
code inside the Editor process. Gameplay remains statically linked into the separate
Player; an Editor extension is never required by the exported game.
The initial SDK registers commands and small action panels. Inspector fields for
runtime components come from the separate gameplay SchemaExporter. Rich custom
widgets and a general marketplace/package manager are later work.
## Example: Beacon
`examples/extensions/beacon` contains:
- `Beacon.hpp`: a runtime component schema and rotating-object behavior.
- `Editor.cpp`: an Editor command and a panel that creates a Beacon in one transaction.
Include `Beacon.hpp` from your project's `Scripts/Gameplay.cpp`. Call
`beacon::register_behavior(runtime)` from `registerGameplay`, and append
`beacon::schema()` to the array returned by `schema()`. Build gameplay so the Editor
can load the new metadata. Read [the first behavior tutorial](../scripting/first-behavior.md)
for the complete gameplay registration convention.
The normal engine build produces `example-plugin/` inside its build directory.
Copy its `beacon.faset-plugin.json` and native library into your project's
`Plugins/` directory, then restart the Editor. The **Beacon tools** panel offers
**Add Beacon**. Its command is also discoverable through MCP as
`plugin_example_beacon_create` and requires a document ID.
Disabling the Editor extension removes its panel and command after restart. It does
not erase its saved component data. Removing the runtime registration leaves an
unknown component preserved by authoring; exporting that scene fails until the
runtime dependency is restored or the component is deliberately removed.
## Compatibility and ownership
A manifest includes module ID/version, `kind: editor`, API version, native library,
build fingerprint and dependencies with exact versions. The loader validates the
complete graph for missing dependencies and cycles before calling entry points.
A failed dependency prevents loading its dependents.
The fingerprint includes the SDK sources, dependency lock, platform, architecture,
compiler version, configuration and CRT settings. Rebuild a plugin for the exact
Editor SDK. Compatibility across arbitrary C++ builds is not promised. Reloading an
updated native module requires restarting the Editor.
`include/faset/editor/plugin_api.h` defines a small C interface. Borrowed UTF-8 JSON
strings are valid during a call; responses are copied through a receiving callback.
Each side frees its own allocations. Register only during startup and invoke the SDK
on the Editor thread. Commands must use their owning module's name prefix. Panels
can invoke owned commands; a `$document` argument resolves to the active authoring
document. No EnTT registry, live runtime world, or arbitrary C++ object pointer is
exposed as a scripting API.
The native plugin test loads the actual example module, creates a component,
checks Undo, runs its separate runtime behavior, unloads command registrations,
opens the saved scene without the package, and rejects incompatible/cyclic/missing
plugin dependencies.
+85
View File
@@ -0,0 +1,85 @@
# Use the Editor through MCP
Faset exposes the same authoring commands to its native interface and to MCP. MCP
runs in the **Editor**. It never provides access to a running game's entities, and
is not linked into the Player or SchemaExporter.
Start a headless server after building Faset:
```sh
build/linux-debug/faset_editor --project /absolute/path/to/game --mcp
```
Configure your MCP client to launch that executable with those arguments using a
stdio transport. On Windows select `build/windows-debug/faset_editor.exe`. Use
absolute paths, including the project path. Add `--gui` when the same process
should display the native Editor; a graphical session and supported GPU are then
required. A separately launched Editor process owns a separate in-memory session.
The transport uses newline-delimited JSON-RPC and the MCP `2025-06-18` lifecycle.
Initialize, send `notifications/initialized`, then discover commands with
`tools/list`. Standard output is reserved for protocol messages. See the official
[MCP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
and [tool contracts](https://modelcontextprotocol.io/specification/2025-06-18/server/tools).
## Authoring workflow
1. Use `faset_documents` or create/open a document.
2. Query `faset_schema` for stable type and field IDs and their constraints.
3. Query the document for its persistent ID and current revision.
4. Submit one `faset_scene_edit` batch with that revision.
5. Save with `faset_document_save`. Use Undo/Redo for authoring changes.
For example, the arguments to `faset_scene_edit` can be:
```json
{
"document": "REPLACE_WITH_DOCUMENT_ID",
"revision": 0,
"idempotency_key": "create-first-object",
"operations": [{"op": "entity.create", "name": "Player"}]
}
```
The command returns the new revision and scene with generated IDs. Repeating the
same batch and retry key in the same session returns its previous result. Reusing
the key for a different payload is an error. An outdated revision produces
`revision.conflict`; query the new state and decide how to apply the intended change.
Do not blindly retry a write using a fresh revision.
Each successful batch is one Undo step. A failed operation rejects the whole batch.
Manual Inspector edits use this same service, including revision checks.
## Jobs and capabilities
`faset_import`, `faset_build`, `faset_export`, and `faset_play` return job IDs.
Use `faset_job` or `faset_jobs` for progress, diagnostics and results.
`faset_job_cancel` requests cancellation; cancelling a build is separate from
undoing a document change. Failed compilation/import preserves the last successful
published generation.
`faset_capabilities` reports available services. `faset_editor_capture` is present
only in a graphical Editor. It returns an MCP image and can save a project-relative
PNG. A headless server has no screenshot tool. `faset_play_control` provides pause,
resume and single-step process controls; it cannot read or modify game entities.
`faset_recovery_list` and `faset_recovery_restore` expose crash recovery, including
scenes that have never been saved. Restoring an already open document requires its
current revision. Recovery refuses to overwrite an externally changed scene file.
## Single-command CLI
The CLI is useful for scripts that do not need an MCP session:
```sh
build/linux-debug/faset_editor --project /absolute/path/to/game \
--command '{"name":"faset_import","arguments":{"path":"Assets/door/manifest.json"}}' --wait
```
`--wait` follows a returned job until completion. Failed, cancelled and conflicting
jobs produce a nonzero exit code. This shell quoting example targets Bash; use the
appropriate argument quoting for your Windows shell.
Validation: unit tests cover protocol errors and shared authoring semantics.
`editor_mcp_stdio` launches the real Editor and verifies initialization, clean JSON
stdout, conflicts, retry behavior, Undo/Redo, saving, reopening, and orderly EOF.
+15 -5
View File
@@ -1,14 +1,14 @@
# Build from source
!!! warning "Foundation checkpoint"
These instructions initially cover the build foundation. The integrated editor,
sample projects, and packaging steps are being added and verified during MVP implementation.
!!! note "Implementation checkpoint"
Linux Editor, Player and export integration are tested. Final Windows graphics/export
acceptance is tracked separately in the implementation report.
## Linux prerequisites
The selected toolchain is C++20, CMake 3.25 or later, Ninja, and Clang.
Graphical builds need Vulkan 1.3 headers/loader and a compatible driver.
SDL3 is built from a pinned source archive.
SDL3, FreeType and HarfBuzz are built from pinned source archives.
On Ubuntu, install the native build tools before configuring:
@@ -16,7 +16,7 @@ On Ubuntu, install the native build tools before configuring:
sudo apt install clang ninja-build cmake python3 python3-venv pkg-config \
libvulkan-dev vulkan-validationlayers libx11-dev libxext-dev libxrandr-dev \
libxcursor-dev libxi-dev libxfixes-dev libxkbcommon-dev libwayland-dev \
libfreetype-dev libharfbuzz-dev xvfb
xvfb
```
`xvfb` is used for automated window tests. A normal desktop session does not need it.
@@ -30,6 +30,16 @@ cmake --build --preset linux-debug --parallel
ctest --preset linux-debug
```
Create a project and open the native Editor:
```sh
build/linux-debug/faset_editor --project "$PWD/MyGame" --new MyGame --dimension 3
```
Use **Build C++** after changing `MyGame/Scripts/Gameplay.cpp`, then **Play**.
The Player runs separately. Stop it before changing and rebuilding C++ gameplay.
See [MCP and CLI](../editor/mcp.md) for headless authoring and automation.
For an optimized build use `linux-release`. The `linux-sanitize` preset enables
AddressSanitizer and UndefinedBehaviorSanitizer for tests without the graphics backend.
+1 -1
View File
@@ -7,7 +7,7 @@ and how those functions interact with scenes, physics, and the editor.
!!! warning "Development status"
MVP implementation is in progress. A planned feature is not a working feature.
Individual guides state their prerequisites and validation status. The current
foundation can be built and tested; a complete editor and game export are not yet available.
Editor, gameplay tutorials and Linux export can be built and tested. Final Windows graphics/export acceptance and complete sample games are still in progress.
Start with [how C++ gameplay works](scripting/index.md), then read
[frame and physics updates](scripting/lifecycle.md). See
+70
View File
@@ -0,0 +1,70 @@
# 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, registration during a callback, and registration after entities have loaded are rejected.
`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.
These methods require a valid handle and a physics body. 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.
`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**.
+50
View File
@@ -0,0 +1,50 @@
# More complete examples
Each folder below is a separate, buildable gameplay module with `Gameplay.hpp`, `Gameplay.cpp`, and `scene.json`. Select one using `FASET_GAMEPLAY_SOURCE_DIR`, as shown in [the first tutorial](first-behavior.md). The Player links that module statically.
## Follow an interpolated object
`examples/tutorials/following` moves an object during fixed updates. A camera follows its presentation pose during `lateUpdate`:
```cpp
--8<-- "examples/tutorials/following/Gameplay.cpp"
```
The camera resolves the saved target ID on each frame, handles its disappearance, reads `presentation(target)`, and writes only its own presentation pose. In this small scene both objects are roots, so their coordinate frames match. For objects under different parents, transform between coordinate frames explicitly; adding local positions from unrelated parents is incorrect.
The `tutorial_following` test advances one and a half fixed intervals. It checks that the camera follows the halfway presentation position, while its simulation pose is unchanged. It also removes the target to exercise missing-handle behavior.
## Spawn and destroy on safe boundaries
`examples/tutorials/spawning` creates a temporary sprite and removes it after its lifetime:
```cpp
--8<-- "examples/tutorials/spawning/Gameplay.cpp"
```
The spawner's `onStart` queues creation. The new object's `onStart` runs only when that command is applied. Its elapsed time is ordinary C++ state owned by the callbacks. A key contains all three handle fields, so a recycled slot or restarted session cannot accidentally reuse an older object's timer.
When the timer expires, `destroy` queues removal; the handle remains valid until the next barrier. `onDestroy` removes the stored timer entry. The example's `spawned_id` must be unique in the runtime scene: using the same value on several spawners produces a duplicate-ID diagnostic. A production spawner should choose an appropriate runtime ID policy.
This does not save the spawned object into the authoring document. The test checks deferred creation, eventual invalidation, and a fresh spawn after reloading the scene.
## Use a contact-based jump
`examples/tutorials/physics` contains the [complete physics controller](physics.md). Its test checks movement, a supported jump, rejection of a jump at the apex, and landing. The default example gameplay module also uses the same grounded query.
The query's slope/separation thresholds are intentionally small and explicit. Extend the gameplay controller when your game needs coyote time, jump buffering, climbing steps, or moving platforms; these are not automatically provided by naming a component “character”.
## Run the tutorial checks
After configuring a normal build with `BUILD_TESTING=ON`:
```bash
cmake --build build/linux-debug --target \
faset_tutorial_moving_tests faset_tutorial_following_tests \
faset_tutorial_spawning_tests faset_tutorial_physics_tests
ctest --test-dir build/linux-debug -R '^tutorial_' --output-on-failure
```
On Windows use the chosen Windows build directory. The four tests compile separate gameplay libraries and execute their actual callbacks. They require neither a graphics window nor the Editor. Running a tutorial through `faset_player` additionally checks rendering and platform integration and requires the documented Vulkan setup.
Schema declarations are tested for stable map-key/FieldId matching and defaults. They remain ordinary C++ source; the engine does not scan arbitrary C++ classes to create this API automatically.
+67
View File
@@ -0,0 +1,67 @@
# Your first behavior
This example moves a sprite along the X axis at two metres per second. It has no rigid body: the behavior owns its simulation pose.
## Build and run the complete example
First complete [the build setup](../getting-started/build.md), including the renderer dependencies. From the repository root on Linux:
```bash
cmake -S . -B build/tutorial-moving -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DFASET_GAMEPLAY_SOURCE_DIR="$PWD/examples/tutorials/moving"
cmake --build build/tutorial-moving --target faset_player faset_schema_exporter
build/tutorial-moving/faset_schema_exporter --output build/tutorial-moving/schema.json
build/tutorial-moving/faset_player --scene examples/tutorials/moving/scene.json
```
On Windows, use a Developer shell with `clang-cl`, the Windows SDK and the documented dependencies. Use `clang-cl` for both compiler options and an absolute path for `FASET_GAMEPLAY_SOURCE_DIR`; run `faset_player.exe` from the selected build directory.
The same directory contract is used by a project's `Scripts` folder. These commands select a complete gameplay module; they do not add its behavior to an unrelated module automatically.
## The module interface
```cpp
--8<-- "examples/tutorials/moving/Gameplay.hpp"
```
The header declares the two entry functions. `Runtime` is the game world API; its implementation and EnTT storage remain inside the engine.
## The implementation
```cpp
--8<-- "examples/tutorials/moving/Gameplay.cpp"
```
Read the callback from top to bottom:
1. `game` is the current runtime, and `self` is the object carrying `tutorial.move_x`.
2. `delta` is this frame's elapsed time in **seconds**.
3. `settings.value("speed", 2.0f)` reads configuration and supplies a fallback if the field is absent.
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 `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.
## Attach the behavior
The example scene is a complete, loadable document:
```json
--8<-- "examples/tutorials/moving/scene.json"
```
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.
## Make a change and verify it
Change the scene's `speed` to `-2`: the object moves left. Change the C++ callback or schema: stop the Player, rebuild, regenerate the schema, then launch a new session. There is no automatic C++ hot reload.
The `tutorial_moving` CTest checks that both 30 Hz and 60 Hz frame sequences move the object two metres in one second. It checks the resulting pose, rather than only checking that the program starts.
Common mistakes are forgetting `setTransform` after editing the copy, writing the wrong component type string, and attaching a rigid body while still using `setTransform`. The last case produces a runtime diagnostic: physics owns that body's pose. Continue with [physics movement](physics.md) for the correct API.
+28 -29
View File
@@ -1,41 +1,40 @@
# C++ gameplay
In the first version of Faset, a "script" is C++ gameplay code compiled into your game.
It is not an interpreted text file. The gameplay library is statically linked into
a separate Player executable.
In Faset, a gameplay script is **C++ compiled into the Player**. You write ordinary functions and register the callbacks an object needs. There is no C++ interpreter or live replacement of compiled classes. Stop Play, rebuild, export the schema, and start a new Player session.
The intended iteration cycle is:
Lua is planned for a later stage. The APIs and tutorials in this section describe the C++ implementation available now.
1. Stop Play.
2. Edit your C++ behavior or system.
3. Build the changed code and export its property schema.
4. Start a new Player session.
## Start here
The Editor reads a schema generated by a separate SchemaExporter. It does not load
your gameplay library into its own process. A gameplay crash therefore does not
automatically crash the Editor. Editor native extensions have a different lifecycle
and run inside the Editor process.
1. Read [Your first behavior](first-behavior.md) and run the moving-object example.
2. Learn [when callbacks run](lifecycle.md) before mixing frame updates and physics.
3. Build a [physics character](physics.md) that can move and jump from the floor.
4. Try [following, spawning, and timed destruction](examples.md).
5. Keep the [runtime API reference](api.md) nearby while writing code.
!!! note "API examples are added with implementation"
This page describes the accepted execution model. Exact function signatures and
complete examples will be documented alongside compiling runtime examples, rather
than presenting proposed APIs as available functions.
The complete tutorial modules are compiled and executed by CTest. The code blocks include those source files directly, so the manual does not maintain separate, untested copies.
## Behaviors and systems
## What belongs to your module
A behavior gives an individual object lifecycle callbacks. A system operates on a
set of objects with matching components. Both use the same runtime state; the visual
scene and Inspector are the authoring view of that state.
A gameplay directory contains `Gameplay.hpp` and `Gameplay.cpp`. It provides two functions in `faset::gameplay`:
Persistent scene IDs and runtime handles are different. A scene ID survives saving
and reopening. A runtime handle belongs to a particular world/session and can become
invalid after an object is removed. Do not store raw component pointers across
structural changes or treat a runtime handle as a save-file ID.
- `registerGameplay(runtime::Runtime&)` registers executable behavior callbacks.
- `schema()` returns a JSON array of component descriptions: stable type and field IDs, versions, defaults, constraints, and Inspector hints.
## Physics ownership
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.
Physics owns the position of a dynamic rigid body. Move it with the supported physics
commands instead of writing its presentation transform. A camera or other visual-only
object can follow the interpolated result without modifying the simulation.
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.
Continue with [Frame and physics updates](lifecycle.md).
## Data, poses, and state
`fields(self, type)` returns a **copy of component configuration**. Editing that copy changes neither the saved scene nor the runtime configuration. `transform(self)` returns a copy of the current simulation pose; pass the changed copy to `setTransform` for a non-physical object. A rigid body uses `setVelocity`, `applyImpulse`, or an explicit `teleport` instead.
Ordinary C++ state can be captured by callbacks. The spawning tutorial shows state indexed by the full runtime handle and cleaned up in `onDestroy`. Do not capture a reference to a local variable that will disappear after `registerGameplay` returns. Use owned state, or ensure the referenced object outlives the runtime.
Scene IDs are saved strings. `EntityHandle` is a temporary reference containing a session, slot, and generation. It must not be written into a save file. Resolve a scene ID with `find`, check `valid`, and expect old handles to stop working after removal or a new Play session.
## Current boundaries
These examples use per-object callbacks and the implemented typed pose/physics API. They do not provide a universal binding for arbitrary C++ classes, a public EnTT registry, or a general parallel-system scheduler. The runtime is sequential and has one owning thread.
MCP belongs to the Editor's authoring, import, build, and process-control services. It does not invoke runtime methods or inspect the live game world. A C++ gameplay change requires the same rebuild whether a person or an agent edited the source.
+47 -33
View File
@@ -1,47 +1,61 @@
# Frame and physics updates
!!! note "Execution contract"
This page describes the accepted runtime contract. The runnable callback examples
and test results are added as the runtime implementation becomes available.
The C++ member names are `onStart`, `fixedUpdate`, `update`, `lateUpdate`, and `onDestroy`. Design discussions may call the corresponding phases OnStart, FixedUpdate, Update, LateUpdate, and OnDestroy. Use the **camelCase member names** in code.
## Choose the right callback
Each ordinary callback receives `(Runtime& game, EntityHandle self, double delta)`. Register only the callbacks you need. `onStart` and `onDestroy` receive a zero `delta`; update callbacks receive seconds. `onCollision` has a separate event signature described in the [API reference](api.md).
- `OnStart`: initialize a behavior once its object and components exist.
- `FixedUpdate`: update simulation logic before a physics step.
- `Update`: run frame-based gameplay once per rendered frame.
- `LateUpdate`: update cameras and dependent visual objects after presentation interpolation.
- `OnDestroy`: release subscriptions and other behavior-owned state before its handle is invalidated.
## Object lifetime
The default simulation interval is 1/60 second. A rendered frame may contain zero,
one, or several fixed ticks. Frame rate and physics rate are not the same quantity.
`onStart` runs once after an object and its components exist. All objects in the initial scene are created before their initial callbacks run. A spawn queued by `onStart` becomes visible at the next fixed-tick barrier, not during the callback that requested it.
## Fixed tick order
`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.
1. Apply structural commands queued by earlier work.
2. Deliver tick input and call `FixedUpdate`.
3. Apply physics commands and step the 2D and 3D worlds.
4. Read back transforms and queue collision events.
5. Run reactions after physics.
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.
Object creation/removal and component addition/removal are deferred to the beginning
of the next fixed tick. This prevents a callback from invalidating the collection
currently being processed. New objects follow the same initialization rules as objects
loaded from a scene.
## One fixed tick
After the fixed ticks, the frame runs `Update`, prepares interpolated presentation
transforms, calls `LateUpdate`, and produces the render snapshot.
The default interval is 1/60 second. A rendered frame may contain zero, one, or several fixed ticks. For each tick the runtime:
## Avoid frame-rate-dependent movement
1. Applies structural commands queued by earlier work, in FIFO order.
2. Makes tick input available and calls `fixedUpdate`.
3. Steps the scene's Box2D or Box3D world with its configured substeps.
4. Reads physical poses back and delivers collision events to `onCollision` callbacks.
A speed is a distance per second. Multiply it by the callback's elapsed seconds when
calculating a displacement. Do not multiply a velocity by elapsed time before assigning
it to a physics velocity API; the physics step performs that integration.
`spawn`, `destroy`, `addComponent`, and `removeComponent` queue structural changes. A command queued while this barrier or a callback runs waits until the **next** tick. This avoids invalidating the entity collection currently being visited. Runtime structural commands do not create an Editor Undo action or modify a saved scene.
## Overload and pause
A velocity is metres per second: assign it directly. A manually calculated displacement is speed multiplied by `delta`. The [physics controller](physics.md) demonstrates this distinction.
The initial catch-up limit is four fixed ticks per frame. Excess whole intervals are
dropped with a diagnostic rather than making the physics step arbitrarily large.
This is a local-game policy, not a guarantee of deterministic network simulation.
## One rendered frame
Pausing clears accumulated time. Single-step advances exactly one simulation tick.
Interpolation history is reset for a new session, spawn, or teleport.
After its fixed ticks, the runtime calls `update` once. It then prepares presentation transforms, calls `lateUpdate`, and makes the final snapshot available to rendering.
For interpolation, the runtime blends the previous and current completed simulation poses using the accumulator fraction. Position and scale are interpolated linearly; rotation follows the shortest quaternion path. This normally displays a pose up to one fixed tick behind the latest simulated state. It is not prediction of a future physics pose.
Use `presentation(target)` in `lateUpdate` when a camera follows a physical object. Following `transform(target)` instead would follow the discrete simulation pose and can cause visible judder. Use `setPresentation` for the camera's visual pose; this method is permitted only during `lateUpdate` and does not write back into physics.
A non-physical pose changed in `update` is presented directly for that frame. Physical bodies reject ordinary `setTransform`; use an explicit teleport when discontinuous motion is intended. Spawn, teleport, scene replacement, and pause transitions reset the relevant interpolation history.
## Input, pause, and overload
`input()` supplies held horizontal/vertical axes and one-shot jump/interact edges. In `fixedUpdate`, an edge survives a rendered frame with no fixed tick and is consumed once, even when the next frame catches up several ticks. In `update`, input is the current frame's input. Consume a gameplay action in one chosen phase so your own code does not apply it twice.
The default catch-up limit is four ticks per frame. Excess whole intervals are dropped and reported as `dropped_time`; the fractional remainder is retained. Physics `delta` is not enlarged to compensate. This is a local-game policy, not a lockstep or rollback guarantee.
Player keys `P` and `N` pause and single-step. Pause clears accumulated wall time. One step advances one fixed tick and produces a current presentation pose. Resuming does not simulate the time spent paused.
## Configure simulation
The Player reads this optional object from the scene document. The settings are used by ordinary Play and `--validate`:
```json
{
"simulation": {
"fixed_delta": 0.016666666666666666,
"max_catch_up_ticks": 4,
"physics_substeps": 4,
"gravity": [0, -9.81, 0]
}
}
```
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.
+49
View File
@@ -0,0 +1,49 @@
# Move and jump with physics
A physical object's final pose belongs to Box2D or Box3D. Your behavior supplies intent through velocity, impulse, or an explicit teleport. It must not write a presentation position back into a rigid body each frame.
## Run the controller
Select `examples/tutorials/physics` as `FASET_GAMEPLAY_SOURCE_DIR`, build `faset_player`, and launch it with `--scene examples/tutorials/physics/scene.json`. Use the commands from [the first tutorial](first-behavior.md), changing the folder and build directory.
Press **A/D** or the left/right arrows to move, and **Space** to jump. The built-in Player supplies W/S as a vertical input axis too; this particular 2D controller deliberately uses only the horizontal axis. `P` pauses, `N` advances one tick while paused, and Escape closes the Player.
## Complete controller code
```cpp
--8<-- "examples/tutorials/physics/Gameplay.cpp"
```
The callback starts with the current velocity so it preserves the solver's vertical motion. It replaces only the horizontal component. An accepted jump replaces vertical velocity with `jump_speed`.
Do **not** multiply the assigned velocity by `delta`. The solver integrates metres per second over the fixed interval. `applyImpulse` is different: it applies a momentum impulse and its effect depends on mass. The adapter uses the body's centre and wakes it.
The collision callback is delivered after the physics step on the runtime's owning thread. It receives copied handles and a begin/end flag, not pointers into the native solver. This example prints a message when contact begins. Both objects can receive their own callback if both have registered behaviors.
## What grounded means
`grounded(self)` examines contact manifolds from the last completed physics step. A contact counts as support when its normal points sufficiently against gravity (dot product greater than 0.6) and at least one contact point is within 0.02 metres. With zero gravity, the query uses Y-up. The query supports both physics adapters.
This distinguishes a floor from a wall and from the top of a jump. **Zero vertical speed is not a ground test**: vertical speed is also near zero at the apex. The tests explicitly try to jump there and check that another upward impulse is not created.
New bodies have no support result before a physics step. Teleporting invalidates the old contact result until the next step. The query is a small support test; it is not a full character motor with step climbing, coyote time, jump buffering, moving-platform attachment, or a capsule controller.
## Scene components and units
The controller's scene includes a static floor and a dynamic box:
```json
--8<-- "examples/tutorials/physics/scene.json"
```
Use `faset.rigid_body_2d` in a 2D scene and `faset.rigid_body_3d` in a 3D scene. Each currently creates a box collider. `body_type` accepts `static`, `dynamic`, or `kinematic`. `half_extents` contains half the box dimensions in metres: two values for 2D, three for 3D. The object's absolute scale multiplies those extents at creation.
Density must be positive; friction is nonnegative; restitution is between zero and one. `linear_velocity` uses metres per second, `gravity_scale` scales world gravity for that body, and `category_bits`/`mask_bits` filter collisions. Rotation uses radians; a 2D rigid body rotates only around Z.
Initial adapters require physical bodies to be **root scene objects**. A parent transform is not silently baked into a rigid body's simulation frame. Collider scale cannot be changed through teleport; remove/re-add the body through the deferred component API when rebuilding its shape. More collider types and articulated character motors are separate work.
## Teleport and failure handling
For an intentional discontinuity, get a pose copy, change its position, and call `teleport(self, pose)`. It resets interpolation and wakes the body. It preserves the body's velocity; call `setVelocity(self, {0, 0, 0})` as well when resetting motion is intended.
A missing body or stale handle makes the physics accessor throw. Exceptions raised inside gameplay callbacks are recorded in `Runtime::diagnostics()` and printed by the Player; later phases continue. Fix the error instead of using exceptions as a normal ground test. A behavior that controls physics should be attached only to an object with the matching rigid-body component.