Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export
This commit is contained in:
@@ -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**.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user