# Asking the physics without waiting (orders and tickets) How a kit written in Rust talks to the physics of a game: reads that never wait, orders that are fire and forget, and questions that hand out a ticket you poll. The same contract holds wherever the physics runs. Readable offline: `gripforge docs physics-async`, or the MCP tool `gripforge_docs` (topic `physics-async`). ## What it is for On the web the physics can run on a Worker of its own, with its own loop at its own fixed step, writing its results in memory shared with the game. The game thread then never waits for it and never wakes it: it reads what is published, sends orders, and asks questions whose answer arrives a tick or so later. Natively, the engine's own physics (or a thread of the native player) sits behind the same calls. A kit written once against this contract works in every place, so a ray cast or a character move is **never** a call that returns the answer in the same line. If you only configure games, you do not need this guide: the kits of the catalogue already use it. It is for those who write a kit (see `gripforge docs kit-third-party`) or a test that talks to the physics. ## The three kinds of call `kx.physics()` in a Rust kit gives you: | Kind | Calls | Behaviour | | --- | --- | --- | | **Reads** | `body`, `has_body`, `has_character`, `mass`, `grounded`, `character_position`, `character_grounded`, `contact_events`, `contacts`, `poses` | never wait; they read the state the physics last published (a body added and not yet taken reads the pose its add asked; a removed body is gone at once) | | **Orders** | `add_body`, `add_character`, `add_static_box`, `add_dynamic_box`, `remove`, `set_body`, `set_position`, `apply_impulse`, `apply_force`, `apply_torque_impulse`, `move_kinematic`, `set_character_position`, `set_gravity`, `set_manual_stepping`, `step` | fire and forget; bodies are slots chosen by the caller, names stay on the caller's side; if the ring of orders is full the order waits in the writer's backlog, in order, and the writer never blocks | | **Questions** | `ask_move_character`, `ask_cast_character`, `ask_raycast`, `ask_overlap_sphere`, `ask_overlap_box` | return a **ticket**; `poll(ticket)` answers `Pending` until the physics took the order, then the answer once | The questions without `ask_` (`move_character`, `cast_character`, `raycast`, `overlap_sphere`, `overlap_box`) no longer exist on a kit's physics: a question is asked, and its answer polled. ## Asking and polling ```rust // Ask: an order with a ticket. Keep what the answer will need. let ticket = kx.physics().ask_raycast(origin, dir, range, Some(&shooter)); state(kx).shots.push(AskedShot { ticket, shooter }); // Take every answer that has come, oldest first. while let Some(asked) = state(kx).shots.first().copied() { let std::task::Poll::Ready(hit) = kx.physics().poll_hit(asked.ticket) else { break }; state(kx).shots.remove(0); apply_hit(state(kx), &asked, hit); } ``` | Question | Typed poll | Answer | | --- | --- | --- | | `ask_move_character(entity, delta)` | `poll_moved` | `Option<(V3, bool)>`: the position after the move and whether it is grounded | | `ask_cast_character(entity, origin, delta)` | `poll_hit` | `Option`: the first hit, if any | | `ask_raycast(origin, dir, max_toi, exclude)` | `poll_hit` | `Option`: the first hit, if any | | `ask_overlap_sphere(center, radius, exclude)` | `poll_found` | `Vec`: the entities found | | `ask_overlap_box(center, size, rotation, exclude)` | `poll_found` | `Vec`: the entities found | `poll(ticket)` is the untyped form. All polls return `std::task::Poll`. Rules to follow: - **Do not wait on a ticket.** Keep it (with what the answer needs) and poll again on the next tick. Where the physics runs on the game's thread the answer is there at once; on a Worker it is there a tick or so later. Write the same code for both. - **Use the state the last answer left**: for a character, `grounded` and the last moved position, not a value you computed before asking. - **`forget(ticket)`** gives a ticket back unread. An answer nobody reads goes back by itself after 600 ticks. - **Poll oldest first** and stop at the first `Pending`, as in the example: the answers then apply in the order you asked. The reference kit for the pattern is `character.third_person` (the move of the character). ## From JavaScript and tests The JSON names of `physics.port` (`raycast`, `overlapSphere`, `overlapBox`) ask a ticket and read it in the same call where the physics answers at once; where it cannot, they say "answers later" instead of waiting. To handle the later case, ask with `askRaycast` / `askMoveCharacter` / `askOverlapSphere` and poll: the op `physics` with `poll` answers `{ pending }` or the answer. In the headless test host, `physics.ask.raycast(...)` returns a promise that is polled after every frame: ```js const hit = await physics.ask.raycast([0, 2, 0], [0, -1, 0], 10); ``` ## Where the physics runs The contract is the same in all three places; the only difference is how soon an answer arrives. | Place | When | Answers | | --- | --- | --- | | The game's thread | a page whose memory is not shared between threads; the tests | when the ticket is handed out | | A Worker of its own | the web, where the memory is shared between threads | a tick or so later; the game thread never waits | | The engine's physics | Godot, Unity, Unreal through the callbacks registered with `gfk_set_physics_callbacks` (the `physics: "engine"` default of those hosts) | when the ticket is handed out; the engine's own callbacks answer | The loader decides the first two; a game does not ask for them. On the web the physics module is placed on a Worker when the shared memory is available, and `calls.physics.why` says why it stays on the page's thread otherwise. ### Counters `host.physicsCounters()` answers `{ thread, steps, loopMs, loops, eventsDropped, asked, answered, inFlight, backlog }`: the thread it runs on, the steps made, the time of its loop, the questions asked and answered, those still in flight and the orders waiting in the backlog. `asked - answered` should stay small; a growing `backlog` means the physics is behind the game. ### Native engines An engine host that registers `gfk_set_physics_callbacks` (see `gripforge docs c-api`) gives its own physics: the callbacks **are** the port. The poses, forces and step copy nothing between the library and the engine; the names of a ray hit or an overlap and the contact events are copied out of the engine, and `gfk_physics_copied` reports how many bytes that was. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | A character is one tick behind its move on the web | expected on a Worker; read the state after the poll, never assume the move is done | | `Pending` for a long time | the ticket was forgotten or already read (an answer is given once), or the physics is paused because the game is not running | | An old call named `raycast` is refused by a kit | removed: use `ask_raycast` and `poll_hit` (the codec's error names `ask_*` and `poll`) | | The loop is on the page's thread | no shared memory in this page; `calls.physics.why` gives the reason |