Actuators
Package @ferret/physics-actuators, capability name actuators, phase field.
What it is for#
"This force, at this point, on this schedule", as data: a push of a hand, the stroke of an oar, a gust on a flag, a motor that holds a speed. The forces enter the same integration as gravity and drag (the field phase), so a free body obeys momentum and angular momentum exactly: one off-centre force turns the body.
An actuator#
"physics": {
"capabilities": ["actuators"],
"actuators": [
{ "name": "oar-left", "node": "Boat", "kind": "force", "at": [-0.9, 0, 0.2],
"dir": { "frame": "body", "v": [0, 0, 1] }, "magnitude": 600,
"profile": { "periodic": { "period": 2, "duty": 0.4, "shape": "half-sine" } }, "command": 0 }
]
}
| Field | Meaning | ||
|---|---|---|---|
node | the entity of the body (a RigidBody), or a target added with addTarget (a Vessel through vesselTarget) | ||
kind | force (N), impulse (N·s per firing), torque (N·m), motor (holds speed, "30rpm" or rad/s, within maxTorque, gain N·m per rad/s), spin (a shaft that turns: below) | ||
at / point | where it acts: a point in the body's frame, or the name of a child node whose origin is the point (default: the centre of mass) | ||
dir / axis | a vector in the body frame (a plain [x, y, z]), or `{ "frame": "body" \ | "world" \ | "<other node>", "v": [...] }` |
profile | "const", { "for": { duration } }, { "delay": { after, inner } }, { "periodic": { period, duty, phase, shape } } (square, sine, half-sine, triangle, saw), { "pulse": { period, count } }, { "curve": { keys, interp, loop } }, { "ramp": { time } } | ||
over | what the profile runs over instead of time: { "angle": "<node>", "turns": 1 } (one cycle per turn of that body about axis), { "phase": "<actuator>" }, { "sensor": "<name>" } (added with addSensor); then the profile's variable is in cycles and its period is 1 | ||
instances, offset | copies of one definition shifted along what the profile runs over ("180deg" for an angle): a firing order, two oars | ||
command | the run-time multiplier (a throttle); on: false starts it off |
An impulse fires on each firing of a pulse profile, or on the rising edge of any other profile.
Readouts and events#
actuator.state gives per actuator: force, torque, power (F·v at the point, τ·ω), work since start, the profile value, the phase of the cycle, the number of firings, and an error when its body is missing. Events: actuator:start, actuator:stop, actuator:done (a for or finite pulse ended), actuator:stroke (an impulse fired).
Commands#
actuator.command { name, command }, actuator.on, actuator.off, actuator.fire { name } (from the start), actuator.state { name? }; editor: edit.physics.actuator.set { name, node, kind, ... } (checked against the schema), .remove, .list.
Spin: a shaft that turns#
{ "name": "mill", "node": "MillShaft", "kind": "spin", "axis": [1,0,0], "speed": "12rpm", "mode": "unstoppable" }. mode is one or a list:
unstoppable: a kinematic body whose angle is written asinitialAngle + ω·ton the shared clock (not integrated), so it is exact after hours, needs no replication on the net, never slows, carries and crushes what touches it.jammable: a motor withmaxTorque; held under 10 % of its speed forstallTime(0.5 s) it raisesactuator:stall { stalled: true }, andstalled: falseonce it is above half speed again.switchable:actuator.command0 or 1 turns it on and off alongrampUp/rampDownseconds; the angle is the exact integral of the ramp; switch instants are the eventactuator:switch. Alone it is an unstoppable shaft with a switch; it combines with either of the others.
Inside a machine: addSpinSink(node, drivetrain.speedSource(...)) makes the spin the speed source of a drivetrain shaft (the shaft is eternal, the gears behind it feel real torques). actuator.state adds speed, angle and stalled.
Bindings: who drives the command#
Bindings (stage A3) connect an actuator's command to the world without code. They live in the level's physics.bindings list (dropped with the level), or edit.physics.binding.set / .remove / .list, or cap.bind(def).
"physics": {
"capabilities": ["actuators"],
"actuators": [ { "name": "oar-left", "node": "Boat", "kind": "force", "point": "Oarlock_L", "dir": [0, 0, 1], "magnitude": 400, "command": 0 } ],
"bindings": [
{ "actuator": "oar-left", "action": "row-left", "as": "press" },
{ "actuator": "oar-left", "event": "anim:notify", "match": { "entity": "Rower", "name": "blade-in" }, "as": "set", "value": 1 },
{ "actuator": "oar-left", "event": "anim:notify", "match": { "entity": "Rower", "name": "blade-out" }, "as": "set", "value": 0 },
{ "actuator": "oar-left", "animate": { "entity": "Rower", "trigger": "row", "on": "start" } },
{ "actuator": "winch", "interactable": "WinchLever" },
{ "actuator": "gust", "zone": "Hill", "actor": "tag:player" }
]
}
One source per binding; as says what the source does (the default is in bold):
| Source | Modes |
|---|---|
action (an input action) | hold (command while down), press (one impulse or one cycle: actuator.fire), toggle, axis (the stick or trigger value times value; component x or y) |
interactable (interact:use of an entity) | toggle, press, on, off |
zone (zone:enter / zone:exit, actor = an entity name or tag:player) | inside (command while anyone counts as inside), toggle, on, off, press |
event (any world event; match compares payload fields, an entity matches by name) | on, off, toggle, press, set (the command becomes value) |
value is the command while active (default 1), rest the command otherwise (default 0). Physics events switch actuators too: { "actuator": "belt", "event": "machine:stall", "match": { "element": "winch" }, "as": "off" }.
What comes back. Events: actuator:start, stop, done, stroke, stall, switch, and actuator:overload { overloaded } when a motor sits at its torque limit; state for a UI from actuator.state. Animation sync, both ways: an animate part writes a number of the actuator (from: phase, value, power, speed, command, work; times scale) to a parameter of the animation graph of the named entity every step, or fires a trigger of the graph when the actuator starts, stops, is done, strokes or stalls. The other way round, the animation drives the profile through its notifies: an anim:notify event binding (as above) turns the force on while the blade is in the water in the animation, so picture and push never disagree. The entity needs an Animator (or any component with a graph).
AI. setCommand(world, actuator, value), fireActuator and switchActuator in @ferret/ai are behaviour-tree leaves: a rower on a galley or a horse at a capstan sets the same command as the player does.
Bindings read the input service (down, pressed, axis) once per fixed step; a binding with a missing service or actuator reports its error through cap.bindings() and leaves the rest running. Which button does which action is the game's call.
Not yet#
Bindings run on every peer; owner-only execution and command forwarding come with A8, the motion language with A4 and A5.
The stroke of an oar#
A blade actuator with oar: { pivot, reach, axis, arc: [catch, finish], period, drive: 0.4, lift: 0.25, feather: true } carries a plate along an oar's path (body frame, metres and radians): the blade is at the end of reach from the oarlock pivot, turned about axis from arc[0] to arc[1] in the drive and back in the recovery. In the recovery it is lifted out of the water along the axis (within a quarter of the recovery) and turned edge-on. The force is plateForce of the flow against the blade's own velocity, in the medium at the blade's place, so it pulls in the water, slips in the air, and pulls less against a current. command 0 stops the stroke. strokePose(oar, time) is the pure path, for tools and tests.
Over the net (A8)#
netActuators(cap, net, { now, delay, owns }) makes actuators safe on several machines. Forces, torques, motors and jammable spins act only where owns(def) is true (the owner of the body; the others receive the body by replication). unstoppable and switchable spins are functions of the shared clock and run on every machine. command, on, off and fire from the returned object go to everyone with an effect time now() + delay; each machine applies them when its clock reaches that time, so a switch happens at one moment everywhere. Call step() every fixed step. Messages that are not well formed are ignored.
Inspector and editor (A9)#
actuator.arrows gives one entry per running actuator: its force arrow at the point where it acts (length by magnitude, colour by kind), torque, phase, power, work, and for a spin the speed, angle and stall. actuator.profile {profile, from, to, samples} samples a profile for the editor's curve preview. motion.eval is the REPL.
From Unreal (A9)#
tools/ue-export/physics-dynamics.mjs: PhysicsThrusterComponent becomes a force actuator (kg cm/s2 / 100 = N), RotatingMovementComponent a spin unstoppable (axis from pitch/yaw/roll, rate in rad/s), WindDirectionalSource a uniform flow source (speed x strength). A radial force, a point wind source and a pivot translation have no equivalent and are listed in report.unsupportedDynamics.