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 Plugin base class and the on_before_physics / on_after_physics hooks.
  • Architecture & Main Loop — the physics pipeline and the per-tick invariants this relies on.