Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Each SDK handles omitted callbacks automatically. Supported names are `on_create`,
`on_start`, `on_enable`, `fixed_update`, `update`, `on_disable`, `on_destroy`, and
`on_stop`. Only frame callbacks take dt. Luau also accepts the capitalized Lua
aliases. External collision callbacks remain unavailable. API 1.1 adds shared
aliases. Collision subscriptions are available through `Physics.onCollisionEnter/Stay/Exit`; see [Contacts and slopes](/docs/api/physics-actions/contacts). API 1.1 adds shared
`Events` to every supported script type, backed by the same scene services.
See [Shared gameplay actions](Scripting/gameplayActions.md) for coverage and setup. Use `rustic.log` for game diagnostics and run scripts through Play.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ Global scripts are instantiated first when the game starts. Scene scripts are ne
when their scene loads. Component scripts are instantiated with their object and are
disabled/unregistered with it. Only callbacks actually supplied by a script are
invoked. The canonical callbacks are `Start`, `Update`, `FixedUpdate`,
`OnCollisionEnter`, `OnCollisionStay`, `OnCollisionExit`, `OnEnable`, `OnDisable`, and
`OnDestroy`. Original snake-case callback names continue to work.
`OnEnable`, `OnDisable`, and `OnDestroy`. Original snake-case callback names continue to work. Collision notifications use explicit Physics event subscriptions.

The basic Play simulator runs gravity and primitive box collision response after
`FixedUpdate`, including in scenes with no scripts. `Anchored` prevents physics
movement; `CanCollide` must be enabled on both objects for solid response. Collision
callback names are reserved: the simulator does not dispatch collision or touch
events. See `PHYSICS.md` for floor setup, falling objects, and current limitations.
subscriptions use `Physics.onCollisionEnter/Stay/Exit`, delivered during Update after resolution. Merely declaring an OnCollisionEnter lifecycle function does not subscribe. See [Contacts and slopes](/docs/api/physics-actions/contacts).

Execution is deterministic: scope (global, scene, component), explicit execution
order, entity Asset ID, script Asset ID, then attachment order. Scripts communicate
Expand All @@ -40,7 +38,7 @@ The global `rustic` table exposes `entity_id`, `get_translation`, `set_translati
`find_entity`, `get_attribute`/`GetAttribute`, `edit_attribute`/`EditAttribute`,
`input`, `log`, `delta_time`, `fixed_delta_time`, `get_property`, `set_property`, and
`set_enabled`. The built-in attributes are `Name`, `Position`, `Size`, `Color`,
`CanTouch`, `CanCollide`, `Anchored`, and `Parent`.
`CanTouch`, `CanCollide`, `Anchored`, `Friction`, and `Parent`.
Lua also provides `print(...)` for an info Console entry and `warn(...)` for a
warning Console entry. Use `rustic.log(level, message)` to choose a different level.

Expand Down
57 changes: 46 additions & 11 deletions docs/PHYSICS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Basic physics
# Physics: forces, friction, contacts and slopes

Built-in primitives simulate automatically in Play, New Window, and Standalone.
No script or physics component is required. Edit mode never moves objects. Gravity
Expand Down Expand Up @@ -29,7 +29,8 @@ Rectangle2D or Circle2D above it. Gravity still acts along world Y.
| `Anchored = false` | The primitive falls, including when collision is disabled. Unanchoring starts with zero velocity after an anchored tick. |
| `CanCollide = true` | Solid response occurs only when both primitives have this flag enabled. Anchored primitives can be floors or walls. |
| `CanCollide = false` | The object passes through other objects; it still falls if unanchored. |
| `CanTouch` | Required for object clicks and touchscreen taps; it does not change solid response or enable collision callbacks. See [Touchscreen input and object clicks](/docs/api/touch-input). |
| `CanTouch` | Enables collision subscriptions when both bodies allow touch, plus object clicks and taps. Does not change solid response. |
| `Friction` | Finite coefficient from 0 to 10; default 0.5. Zero is frictionless. Set it in the Inspector or through `EditAttribute`. The contact coefficient is the geometric mean of both values. |
| `Size` | Local transform scale, multiplied by primitive dimensions for collision bounds. |

These flags are read every fixed tick, after all `FixedUpdate` script callbacks.
Expand Down Expand Up @@ -69,17 +70,52 @@ globalThis.behavior = {
Attach this version as a JavaScript Object Component instead of the Lua script.
Scene paths can also target other objects; see the **Edit scene objects** guide.

## Forces, friction and slopes

Use the falling-box scene above. Create a JavaScript **Object Component** in
Programming, attach it to `Box` with **+ Add Component**, and replace its source:

```javascript
globalThis.behavior = {
Start() {
rustic.EditAttribute("Friction", 0.6);
Physics.onCollisionEnter(rustic.entity_id(), (other, normal) => {
print("Contact", other, "normal", normal);
});
Physics.onCollisionExit(rustic.entity_id(), other => print("Left", other));
Physics.impulse(rustic.entity_id(), [2, 0, 0]);
},
FixedUpdate(dt) {
// Unit mass: an upward force of 9.81 cancels gravity over this interval.
// Physics.force(rustic.entity_id(), [0, 9.81, 0], dt);
const support = Physics.ground(rustic.entity_id(), 45);
if (support && support.walkable) {
// normal is world-space; angle is degrees from world up.
// Use support.normal to project controller movement along the surface.
}
}
};
```

Press Play. The box falls, emits one enter callback, and slows on the floor.
Rotate the anchored floor about Z to make a ramp. With zero friction on either
body the box slides down the slope; sufficiently high friction holds it. There
is no automatic character stair stepping or slope-angle movement clamp.
`Physics.ground` reads contacts from the most recent completed fixed step, so it
is null before the first physics tick and may lag a scripted teleport by a tick.

`Physics.force` adds `force * scaledDelta` to velocity. Call it once per
`FixedUpdate`, passing `dt` in languages requiring an explicit interval. Impulse
adds velocity immediately; launch replaces it. Applying force from every rendered
frame with a fixed interval produces frame-rate-dependent acceleration.

## Current limits

- Only built-in primitives participate. Imported mesh assets, cameras, lights,
folders, and empty nodes have no physics bodies. Runtime-created primitives join
the next fixed tick; removed primitives lose their velocity state.
- Collision uses world-aligned boxes enclosing primitive dimensions, transformed
by rotation, scale, and the hierarchy. Spheres and circles behave as boxes, torus
holes are solid, and rotated slopes do not produce true slope sliding. Thin planes
and 2D shapes have a minimum collision half-thickness of 0.025 units.
- Bodies have equal mass and translate without rotating, bouncing, friction,
joints or configurable gravity. API 1.1 adds live primitive raycasts, sphere casts, overlaps, forces, impulses and velocity changes; see [Physics queries and forces](/docs/api/physics-actions). These APIs do not replace the box collision model.
- Collision uses oriented boxes enclosing primitive dimensions, transformed by rotation, scale, and the hierarchy. Rotated boxes and planes provide slope normals. Spheres and circles still behave as boxes and torus holes are solid. Sheared parent transforms fall back to world-aligned boxes. Planes and 2D shapes have a minimum collision half-thickness of 0.025 units.
- Bodies have equal unit mass and translate without rotating, bouncing, joints or configurable gravity. Coulomb friction reduces relative tangential motion and can hold bodies on shallow slopes. API 1.1 adds live primitive raycasts, sphere casts, overlaps, forces, impulses and velocity changes; see [Physics queries and forces](/docs/api/physics-actions). These APIs do not replace the box collision model.
- Swept box checks reduce tunnelling during simulated falls. Script teleports and
script-moved anchored platforms are not swept. Fast multi-body impacts and large
stacks are approximate; use separated starting positions and modest scene sizes.
Expand All @@ -90,8 +126,7 @@ Scene paths can also target other objects; see the **Edit scene objects** guide.
- Child bodies receive world-space gravity and are converted back to local
positions. Anchored children still follow their parent's transform. For predictable
independent rigid bodies, place them at scene root or under an organizational folder.
- No `OnCollisionEnter`, `OnCollisionStay`, or `OnCollisionExit` events are dispatched
by this simulator, even if a language adapter accepts those callback names.
- Collision callbacks use explicit `Physics.onCollisionEnter`, `Physics.onCollisionStay`, and `Physics.onCollisionExit` subscriptions. Merely declaring lifecycle functions named `OnCollisionEnter` does not register a subscription. See [Contacts and slopes](/docs/api/physics-actions/contacts).

## Diagnose problems

Expand All @@ -102,7 +137,7 @@ Scene paths can also target other objects; see the **Edit scene objects** guide.
- **The box falls through:** enable Can collide on both the box and floor. Imported
models need a separate built-in primitive as a collision proxy for now.
- **Objects hover or collide too early:** check primitive dimensions and transform
scale. Rotated or curved shapes use enclosing boxes rather than their surface.
scale. Curved primitives use enclosing boxes; sheared transforms use world-aligned enclosing boxes.
- **The object vanishes:** check the camera view and floor placement. Without a
collidable floor, a body continues falling beyond the camera.
- **A script release fails:** verify attachment and language, and read Console
Expand Down
3 changes: 1 addition & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ loop is timed out/restarted without editor failure; no project-language setting
- Implement initial Rapier rigid/static/kinematic bodies, colliders, triggers, layers,
raycasts, fixed-step synchronization, and debug draw behind `physics-api`.
Basic Play gravity and enclosing-box primitive collision are already implemented
in `engine-play`; see `PHYSICS.md`. Full rigid-body rotation, triggers, queries,
collision callbacks, and imported-mesh physics remain future work.
in `engine-play`, along with forces, friction, raycasts, contact subscriptions and slope support queries; see `PHYSICS.md`. Full rigid-body rotation, triggers and imported-mesh physics remain future work.
- Implement WAV/MP3/AAC/FLAC/OGG 2D/3D playback, source/listener, attenuation, loop, bus, volume,
pitch, and streaming music behind `audio-api`.
- Implement native runtime labels, buttons, images, panels, text input, basic flex/grid
Expand Down
2 changes: 2 additions & 0 deletions docs/Scripting/API/physics-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ Choose the physics operation you need.

- [Physics queries](/docs/api/physics-actions/queries) — Find colliders with casts and overlaps.
- [Physics forces](/docs/api/physics-actions/forces) — Apply forces, impulses and velocity changes.

See [Collision callbacks and slope queries](/docs/api/physics-actions/contacts) for contact subscriptions, friction and ground support.
100 changes: 100 additions & 0 deletions docs/Scripting/API/physics-actions/contacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Collision callbacks and slope queries

[Back to Physics APIs](/docs/api/physics-actions).

## Setup

Create an anchored, collidable Rectangular Prism floor and an unanchored Cube at
`[0, 5, 0]`. Enable **Can touch** on both. In Programming create an Object Component,
then select the cube and attach it using **+ Add Component**. Physics runs in Play,
New Window and Standalone; edit mode does not simulate.

## Calls

- `Physics.onCollisionEnter(entity, callback)`
- `Physics.onCollisionStay(entity, callback)`
- `Physics.onCollisionExit(entity, callback)`
- `Physics.ground(entity, maxAngle = 45)`
- `rustic.EditAttribute("Friction", coefficient)` and `rustic.GetAttribute("Friction")`

Subscriptions return an Events connection; call `disconnect()` or
`Events.disconnect(connection)` to remove it. Owner disable, destruction, scene
unload and successful reload clean up subscriptions using the existing event rules.
Callbacks run during the next `Update` event dispatch, after fixed-step collision
resolution. Multiple fixed ticks can queue multiple stay notifications before a
rendered frame. Exceptions disable the behavior using the normal script error path.

Each callback receives `(otherEntityId, normal)`. The ID is a stable string. The
normal is a three-component world vector pointing from the other body toward the
subscribed body. Exit reports the last contact normal; the other ID may refer to
an object that was removed. There is one enter/stay notification per touching pair
per fixed tick, mirrored to both bodies; substeps and solver iterations do not
duplicate notifications. Leaving contact, removing a collider, anchoring both bodies,
or disabling collision/touch produces exit on the next fixed tick. At least one
body must be dynamic, both must enable CanCollide, and both must enable CanTouch.
These are solid-contact callbacks; noncollidable triggers are not implemented.
Collision event names are reserved and cannot be emitted by scripts.

`ground` returns null without an upward support contact, otherwise
`{entity, normal, angle, walkable}`. `angle` is degrees from world up; `walkable`
compares it with `maxAngle`, a finite number from 0 through 90. It chooses the
most upward-facing contact from the latest completed fixed step, independently
of CanTouch. It does not cast a ray, predict the next step, stop a body on a steep
slope, or implement stairs. Query after a fixed step has completed; startup has no
contacts yet. Friction and the solver produce sliding along rotated surfaces.

## JavaScript example

```javascript
globalThis.behavior = {
Start() {
rustic.EditAttribute("Friction", 0.5);
Physics.onCollisionEnter(rustic.entity_id(), (other, normal) => {
print("Landed on", other, normal);
});
},
Update() {
const support = Physics.ground(rustic.entity_id(), 45);
if (support && support.walkable) {
// Surface projection: tangent = movement - normal * dot(movement, normal).
}
}
};
```

Press Play: one landing message appears, followed by stay events if subscribed.
Rotate the floor about Z to test ramps. Set friction to zero to observe sliding.
If no callback arrives, check attachment, Console errors, both flags on both
bodies, and that Update is running. If ground is null in Start, wait for a physics
tick. Merely defining `OnCollisionEnter` lifecycle functions does not subscribe.

## Language bindings

Lua 5.4 and Luau use `Physics.onCollisionEnter(id, function(other, normal) ... end)`
and `Physics.ground(id, 45)`; vectors are one-indexed arrays. JavaScript and HTML
inline scripts use the example above. Python uses the same method names and
`lambda other, normal: ...`. PHP uses `Physics::onCollisionEnter($id, $fn)` and
`Physics::ground($id, 45)`; a callback receives the two positional arguments.

C++ uses `Physics::onCollisionEnter(id, [](const RusticValue::Array& args){ ... })`
and `Physics::ground(id, 45)`, which returns a RusticValue object or null. C# uses
`Physics.onCollisionEnter(id, args => { ... })` and `Physics.ground(id, 45)`, which
returns a JsonElement (test ValueKind for Null). Java uses the same calls with a
Consumer<Object[]> callback and returns an Object (null or Map). Native callback
arrays contain the other ID at index 0 and the normal at index 1.

C uses `Physics.onCollisionEnter(id, fn)` where
`void fn(const RusticValue *args, size_t count)` receives those same two values.
`Physics.ground(id, 45)` returns `RusticGround` with `found`, `entity`, `normal`,
`angle` and `walkable`; check `found` before reading the other fields. C and Java
require an explicit maximum angle. The matching Stay/Exit methods use the same
callback signature in every language.

The shared query protocol is
`{"op":"physics_ground","entity":"<stable-id>","max_angle":45}`.
The event subscription protocol is the existing `connect` gameplay request with
signal `{name:"CollisionEnter",source:"<stable-id>"}` (or CollisionStay/Exit).
Friction accepts numeric values from 0 to 10 and rejects NaN/infinity. Old scenes
without a coefficient load with 0.5. These features use oriented box proxies;
curved primitives and meshes do not gain exact shape collision. Sheared transforms
fall back to enclosing AABBs. See [Basic physics](/docs/guides/physics).
2 changes: 1 addition & 1 deletion docs/Scripting/API/physics-actions/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ globalThis.behavior = {
## Behavior and limitations

- Attach to a Part above an anchored collidable floor. The query reports the nearest hit.
- Queries use primitive world AABBs including parent transforms. SphereCast uses rounded sphere/box contacts; model meshes do not automatically acquire primitive colliders.
- Raycasts and sphere casts use oriented primitive bounds including parent transforms; sphere casts retain rounded sphere/box contacts. Overlap and explosion candidate searches use conservative world AABBs. Sheared transforms use enclosing AABBs. Imported models require primitive collision proxies.

- See [Basic physics](/docs/guides/physics) for collider setup, gravity and collision limits.
- Check script attachment, enabled state and the Console when an operation fails.
4 changes: 1 addition & 3 deletions docs/Scripting/apiLanguages.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,4 @@ Find/List retain their existing snapshot semantics in external adapters. List re

Keyboard input remains limited to held WASD, arrows and Shift in embedded Play.
[Touchscreen input and object clicks](/docs/api/touch-input) provide multi-touch
input and CanTouch-gated click subscriptions in the embedded game view. Physics
collision contact callbacks are not dispatched. These are shared runtime limits
across languages.
input and CanTouch-gated click subscriptions in the embedded game view. Physics contact subscriptions and ground queries are available across languages; see [Contacts and slopes](/docs/api/physics-actions/contacts).
2 changes: 1 addition & 1 deletion docs/Scripting/gameplayActions/physics-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ end }
`sphereCast` adds a radius and uses rounded sphere/box contacts; `overlap(center,
radius, ignore?)` returns entity IDs. Hits contain entity, point, normal and distance.
Direction is normalized by the engine. Queries use the simulation's authored
primitive colliders represented by world AABBs, including parent transforms.
primitive colliders represented by oriented boxes, including parent transforms. Overlaps and explosion candidate searches use conservative world AABBs.
6 changes: 3 additions & 3 deletions docs/Scripting/sceneObjects.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ See [APIs in every language](/docs/guides/api-languages) for native signatures.
| `Position` | three finite numbers | Local translation relative to parent |
| `Size` | three finite numbers | Local transform scale |
| `Color` | three finite numbers | RGB; preserves existing alpha |
| `CanTouch` | boolean | Enables object clicks/taps; does not affect solid response |
| `CanTouch` | boolean | Enables clicks/taps and collision subscriptions when both bodies allow touch; does not affect solid response |
| `CanCollide` | boolean | Enables solid box collisions with other collidable primitives in Play |
| `Friction` | number, 0..10 | Contact friction coefficient; defaults to 0.5 |
| `Anchored` | boolean | Prevents physics movement; unchecked primitives fall under gravity |
| `Parent` | stable entity ID or null | Reparents the target; changes future path |

Expand All @@ -127,8 +128,7 @@ path string. Material and Enabled are not supported attributes; `set_enabled`
still affects the script owner.

Physics runs after fixed-update callbacks. Imported meshes do not yet simulate;
curved and rotated primitives use enclosing boxes. Physics does not dispatch
collision callbacks. See the **Basic physics** guide for setup and limitations.
curved primitives use oriented enclosing boxes. Rotated planes and boxes support slopes. Register collision callbacks through `Physics.onCollisionEnter/Stay/Exit`; see [Contacts and slopes](/docs/api/physics-actions/contacts). See the **Basic physics** guide for setup and limitations.

## Diagnose failures

Expand Down
Loading
Loading