3.9 KiB
Physics + graphics modding layer (API 2)
Low-level Rapier + BoardWorld primitives. No vehicle type — assemble cars, carts,
and props from bodies, joints, meshes, and sdk.net rules in Lua.
Primitives
| Area | Surface |
|---|---|
| Physics | spawn/remove, add_collider, forces/impulses/torques, pose/velocity, read() |
| Queries | raycast with filter="all" or "ground", velocity_at, effective_inv_mass, spring_ray, local_ang_accel_impulse |
| Contacts | touching() (steady pairs incl. ground), contacts() (begin/end edges) |
| Joints | revolute, prismatic, joint_motor, joint_spring, remove_joint |
| Graphics | sdk.graphics.mesh, visibility, remove; sdk.ui.text |
| Player | read, attach, detach — attach pauses native skate for that player |
| Network | sdk.net.publish/read — JSON rules (driver seat, scores); not physics replication |
Manifest "api": 2.
Fixed tick (owner client)
- Map ground injected into
DynamicsWorld(ensure_ground) - Per mod: physics snapshot from last Rapier step →
on_fixed_update - Lua queues commands (forces, spawns, motors, …)
begin_force_frame→ apply commands (removes before spawns) → skater proxies →step- Contacts drained; meshes/attach synced
read(), touching(), and contacts() during on_fixed_update reflect the previous
step. Commands you issue this tick take effect on the next step.
Spawn height
Use the same raycast API with filter="ground" for terrain height. The default
filter="all" still hits dynamic bodies, mod bodies, and skater proxies.
local hit = sdk.physics.raycast(
{x, 500, z},
{0, -1, 0},
{max_distance=1000, filter="ground"}
)
local ground_y = hit and hit.point[2] or 0
Exclude one or more owned bodies without changing the filter:
local hit = sdk.physics.raycast(origin, direction, {
max_distance=100,
filter="all",
exclude={"chassis", "wheel_fl"},
})
Respawn pattern (multiplayer-safe): queue remove on frame N, spawn on frame N+1
so teardown is applied before placement queries.
Contacts
for _, t in ipairs(sdk.physics.touching()) do
if t.a == "wheel_rl" and t.b == "ground" then ... end
end
contacts() fires started=true/false for pair changes only; do not use it as a ground
counter.
Drive assembly (reference pattern)
Built from primitives on the owner sim; remotes follow dyn:{mod}:{body} poses.
- Sensor chassis —
sensor=true, no terrain contact; carries mass/inertia - Wheel bodies — separate dynamic bodies with mesh/sphere hulls,
friction≈1.3 - Prismatic suspension — chassis↔wheel, axis down,
joint_springtarget = rest length along axis - Revolute axle — same anchors; rear
joint_motorfor drive - No scripted tire impulses on wheel contact points
Validate with crates/skate-mods/tests/physics_api.rs (wheel_motor_on_ground_moves_chassis).
Example mod
sdk/examples/physics-sandbox/ — crate, WASD forces, ground contact HUD, E attach, R reset.
cargo run --locked -p skate-mods --example check_mod -- sdk/examples/physics-sandbox
cargo test --locked -p skate-mods physics_api
Copy the folder into mods/ (or set SKATE3_MODS).
Multiplayer
| Replicates | Does not replicate |
|---|---|
Body poses (dyn:… APPLICATION) |
Joints, motors, forces, spawns |
| Matching mod fingerprint | Per-tick command stream |
Use sdk.net.publish for shared rules (who drives, scores). Owner simulates physics;
late joiners see poses, not a full rigid-body replay.
Dual-world collision
Skater BoardWorld volumes ↔ Rapier kinematic proxies each fixed tick. Dynamics shapes
export into network_proxies for native skate collision. Player hit / wipeout bridging
from Rapier contacts is still planned.
Deferred
sdk.player.hit, play-state masks, portal render targets- Shapecast / multi-ray helpers
- Packaged ZIP helper (use loose folders +
check_mod)