Adding Velocity to a Scene Object
Scene objects — cubes, the house — are static: they carry HasPose but no HasVelocity6DoF. Motion is not a "robot-only" thing. The physics system integrates velocity into pose for any entity that has both HasPose and HasVelocity6DoF; robots are just entities that also have a control law (HasMotionInput + a physics level).
So to make the house move, you don't need a robot, a physics level, or a motion input. You need two things: give it a velocity component, and write to it. A plugin can do both at runtime.
The plugin hook
This lives in a Plugin subclass. on_before_physics runs once per tick, just before the physics system:
import numpy as np
import raylib as rl
from robosim.components import HasVelocity6DoF, HasCollision, ColliderKinds
class ManualMovePlugin(Plugin):
def on_before_physics(self, world):
house_eid = 13
ent = world.get_entity(house_eid)
if rl.IsKeyPressed(rl.KEY_B): # toggle the "mover" capability on/off
if not ent.has_component(HasVelocity6DoF):
ent.add_component(HasVelocity6DoF)
else:
ent.remove_component(HasVelocity6DoF)
if not ent.has_component(HasCollision):
ent.add_component(HasCollision, collider_kind=np.array([ColliderKinds.AABB], "int32"))
ent.candidate_pose[:] = ent.pose # seed collider at the real pose, not the origin
world.update() # commit the structural change before this tick reads it
if ent.has_component(HasVelocity6DoF):
ent.candidate_velocity[:] = [0.5, 0, 0, 0, 0, 0] # drift +x; the integrate step turns this into motion
Press B: the house gains a velocity (and a collider) and drifts along +x, bouncing off things it hits. Press B again: the velocity component is removed and it goes static.
Why it works — and the five things that matter
1. candidate_velocity, not velocity. The physics tick is control → integrate → detect → resolve → commit. The universal integrate step is the one that moves things:
candidate_pose = pose @ trexp(skew(candidate_velocity · dt))
It reads candidate_velocity. on_before_physics runs immediately before it, so writing candidate_velocity feeds integrate directly. Writing the persistent velocity would do nothing here — for a control-less mover, nothing copies velocity into the candidate.
2. Capabilities are additive. has_component / add_component / remove_component let a plugin grant or revoke a capability live. Adding HasVelocity6DoF turns a static object into a mover; removing it makes it static again. There is no Frozen flag to clear — "static" simply is "lacks HasVelocity6DoF". This is the whole point of decoupling velocity from collision.
3. Seed candidate_pose when you add a collider. candidate_pose defaults to the identity matrix (the world origin). A static object's candidate was never integrated, so a collider built from it would land at the origin — the object would appear to collide with empty space at (0,0,0). ent.candidate_pose[:] = ent.pose builds the collider at the house's real position.
4. Structural changes are lazy. add_component / remove_component are buffered. Call world.update() before code in the same tick reads the new component (here: before _detect_collisions and the candidate_velocity write need them committed).
5. Re-assert each tick. on_before_physics fires every tick, so setting candidate_velocity there keeps the house moving and lets you change its velocity live (steer it, ramp it, stop it).
See also
- Plugins — the
Pluginbase class and theon_before_physics/on_after_physicshooks. - Architecture & Main Loop — the physics pipeline and the per-tick invariants this relies on.