Adding Velocity to a Scene Object (Plugin)

First, the big picture: the simulator is one loop

Before the example, hold one thing in your head. The whole simulator is a single loop, and every plugin lives inside it. Each iteration is one render tick (~60 Hz), and on every iteration the loop calls the same handful of callbacks on every plugin you loaded, in order. Your plugin's on_before_physics is just one of those calls — it fires once per tick, right before the physics step it feeds:

plugins = PluginsManager([ManualMovePlugin(), ..., CustomPlugin()])   # your --plugins selection

while not window_should_close():          # one iteration == one render tick (~60 Hz)
    world.update()                        # flush last tick's add/remove_component

    plugins.io_handler(world)             # inbound plugin TCP messages -> events written to the ECS

    plugins.on_before_physics(world)      # calls on_before_physics on EACH plugin, so CustomPlugin's
    for _ in clock.subticks():            #   runs HERE, this tick: read key B, (re)write candidate_velocity
        physics(world, dt)                #   integrate -> detect -> resolve -> commit  (per subtick)
    plugins.on_after_physics(world)

    render()
    plugins.responses_handler(world)      # exactly one reply per message that arrived this tick

The consequence is the whole trick. Your hook runs again next tick, and the tick after that. So anything you write inside it, you re-assert every tick. Write candidate_velocity once and it isn't a command that sticks — it gets overwritten and it's gone. Write it every tick and the thing keeps moving. Write a different value next tick and you've steered it — ramp it, turn it, stop it. That's not a limitation to work around; it is the handle for live control. Every hook is per-tick like this (on_before_physics, on_after_physics, io_handler, responses_handler) — the full loop is on the Architecture & Main Loop page.

That's the mental model. Now the example.

Making the house move

Scene objects — cubes, the house — are static: they carry HasPose but no HasVelocity. But motion is not a robot-only thing. The physics system integrates velocity into pose for any entity that has both HasPose and HasVelocity; a robot is just an entity that also has 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 every tick (that's the loop above). A plugin does both at runtime:

import numpy as np
import raylib as rl
from robolib.plugin import Plugin
from robolib.components import HasVelocity6DoF, HasCollision, ColliderKinds

class CustomPlugin(Plugin):

    def on_before_physics(self, world):
        house_eid = 13
        ent = world.get_entity(house_eid)

        if rl.IsKeyPressed(rl.KEY_B):                       # press "B": toggle the "mover" capability
            if not ent.has_component(HasVelocity6DoF):
                ent.add_component(HasVelocity6DoF)
            else:
                ent.remove_component(HasVelocity6DoF)

            if not ent.has_component(HasCollision):         # make it collidable too
                ent.add_component(HasCollision,             # AABB: axis aligned bounding box collision method
                                  collider_kind=np.array([ColliderKinds.AABB], "int32"))
                ent.set_component_data(component=HasPose, data={"candidate_pose": ent.pos}) # needed for collision (TO BE EXPLAINED BETTER)

        if ent.has_component(HasVelocity6DoF):
            ent.set_component_data(HasVelocity, {"candidate_velocity": [0.5, 0, 0, 0, 0, 0]})

        # Updates (add_component, set_component_data, etc.) are buffered and not commited immediately.
        # The main loop also calls world.update() on each tick, but if you need instant change, do it here as well.
        world.update()

Press B: the house gains a velocity (and a collider) and drifts along +x, bouncing off what it hits. Press B again: the velocity component comes off and it goes static.

The details that make it work

1. candidate_velocity, not velocity. The physics tick is control → integrate → detect → resolve → commit. The universal integrate step is the one that moves things — think of it as plain integration:

pose[t+1] = pose[t] + candidate_velocity * dt

(In reality pose is an SE(3) matrix, so this is a screw-motion exponential rather than a +, but the idea is identical — see btrexp in src/robolib/utils/mathutils.py, applied in src/robolib/physics/motion.py.)

The point: integrate 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 HasVelocity turns a static object into a mover; removing it makes it static again. There is no Frozen flag to clear — "static" simply means "lacks HasVelocity". 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).

Registering it

A plugin runs only if it is registered (mapped to a name) and selected (named at startup):

  1. Register — drop your module in src/plugins/ (say custom_plugin.py) and add the class to REGISTERED_PLUGINS in src/plugins/__init__.py. That package is the only place you touch — the server (robosim.py) never changes:

    # src/plugins/__init__.py
    from .custom_plugin import CustomPlugin       # your module under src/plugins/
    
    REGISTERED_PLUGINS = {
        "manual_move": ManualMovePlugin,   # the built-ins
        "custom": CustomPlugin,            # your plugin, under a short name
    }
    
  2. Select — start the server with that name: python cli/robosim.py --plugins custom. The flag takes several (--plugins manual_move custom); the server hands REGISTERED_PLUGINS to PluginsManager, which instantiates REGISTERED_PLUGINS[name]() for each, and every plugin's endpoints join the protocol. With no flag it defaults to manual_move.

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.