Spawning and Moving a Crate over TCP (Protocol)
Example 1 made a scene object move from inside the server (a plugin, server-side Python). This one drives an entity through its whole life from outside, over the wire — no plugin, no server code. We spawn a crate, launch it, re-steer it in flight, freeze it, then delete it, using the five live-mutation commands: entity_spawn, entity_add_component, entity_set_data, entity_remove_component, entity_destroy. Every step is one payload piped into robosim-ncat.
Assumes a server is running on port 42069 (python cli/robosim.py).
1. Spawn a crate
entity_spawn takes the same (components, data) shape an entity has on disk — spawning is literally "load this one entity". Here: a crate model, a pose, and an AABB collider, parked at world position (0, 2, 2).
echo -e 'entity_spawn {"components": ["HasTag", "HasModel", "HasPose", "HasCollision"], "data": {"tag": "spawned", "model_path": "resources/models/cube/cube.obj", "texture_path": "resources/textures/crate.png", "scale": [1.0], "pose": [[1,0,0,0],[0,1,0,2],[0,0,1,2],[0,0,0,1]], "collider_kind": [2]}}' \
| ./cli/robosim-ncat.py localhost -p 42069 --json
The response hands back the new entity's id — you need it for every step below:
{"status": "Added a new entity to the world", "entity_id": 16, "timestamp": "..."}
The id is assigned by the server (next free slot), so don't hard-code it. Capture it with jq:
EID=$(echo -e 'entity_spawn {"components": ["HasTag", "HasModel", "HasPose", "HasCollision"], "data": {"tag": "spawned", "model_path": "resources/models/cube/cube.obj", "texture_path": "resources/textures/crate.png", "scale": [1.0], "pose": [[1,0,0,0],[0,1,0,2],[0,0,1,2],[0,0,0,1]], "collider_kind": [2]}}' \
| ./cli/robosim-ncat.py localhost -p 42069 --json | jq -r .entity_id)
echo "spawned crate is entity $EID"
2. Make it move
The crate is static — it has HasPose but no HasVelocity. Add that component, seeding its candidate_velocity (a 6-vector twist [vx, vy, vz, wx, wy, wz]). Here [1, 1, 0, …] drifts it along +x and +y. (Write every element as a float — 1.0, not 1; the field is float32 and an all-integer array is rejected.)
echo -e 'entity_add_component {"entity_id": '"$EID"', "component": "HasVelocity", "data": {"candidate_velocity": [1.0, 1.0, 0.0, 0.0, 0.0, 0.0]}}' \
| ./cli/robosim-ncat.py localhost -p 42069 --json
{"status": "Added component HasVelocity to entity 16", "timestamp": "..."}
Watch it drift — poll the pose a few times and its translation advances (z holds at 2):
for i in 1 2 3; do sleep 0.6; echo -e 'sim_get_state' \
| ./cli/robosim-ncat.py localhost -p 42069 --json \
| jq -c --argjson e "$EID" '.world.entities[]|select(.entity_id==$e)|{x:.data.pose[0][3], y:.data.pose[1][3], z:.data.pose[2][3]}'; done
# {"x":0.93,"y":2.93,"z":2} -> {"x":1.9,"y":3.9,"z":2} -> {"x":2.83,"y":4.83,"z":2}
3. Re-steer it in flight — entity_set_data
entity_add_component only adds — call it again on HasVelocity and it errors (the entity already has it). To change a field on a live entity you overwrite it with entity_set_data. Here we kill the +x/+y drift and make the crate climb straight up (candidate_velocity = [0, 0, 1, …]):
echo -e 'entity_set_data {"entity_id": '"$EID"', "component": "HasVelocity", "data": {"candidate_velocity": [0.0, 0.0, 1.0, 0.0, 0.0, 0.0]}}' \
| ./cli/robosim-ncat.py localhost -p 42069 --json
{"status": "Entity id: 16. Updated data of following fields: ['candidate_velocity']", "timestamp": "..."}
Poll again: x and y freeze where they were, z now climbs. The crate turned in mid-air the instant we wrote the field:
for i in 1 2 3; do sleep 0.6; echo -e 'sim_get_state' \
| ./cli/robosim-ncat.py localhost -p 42069 --json \
| jq -c --argjson e "$EID" '.world.entities[]|select(.entity_id==$e)|{x:.data.pose[0][3], y:.data.pose[1][3], z:.data.pose[2][3]}'; done
# {"x":3.17,"y":5.17,"z":2.93} -> {"x":3.17,"y":5.17,"z":3.87} -> {"x":3.17,"y":5.17,"z":4.78}
entity_set_data writes any field of a component the entity already has — a new candidate_velocity to re-steer, a new pose to teleport.
4. Freeze it — entity_remove_component
Drop HasVelocity — the crate's entire motion input — and it stops. The integrate step (candidate_pose = pose @ trexp(candidate_velocity · dt)) runs only for entities matching query(HasPose, HasVelocity) (src/robolib/systems.py); with the velocity component gone, the crate falls out of that query and nothing advances its pose. Its HasCollision stays, so it's now a solid, static crate again:
echo -e 'entity_remove_component {"entity_id": '"$EID"', "component": "HasVelocity"}' \
| ./cli/robosim-ncat.py localhost -p 42069 --json
{"status": "Component HasVelocity removed from entity 16", "timestamp": "..."}
Its component set is now motion-free, and the pose stops changing no matter how often you poll:
echo -e 'sim_get_state' \
| ./cli/robosim-ncat.py localhost -p 42069 --json \
| jq -c --argjson e "$EID" '.world.entities[]|select(.entity_id==$e)|guide.components'
# ["HasModel","HasPose","HasTag","HasCollision"] (no HasVelocity)
5. Delete it — entity_destroy
Finally, remove the entity from the world entirely:
echo -e 'entity_destroy {"entity_id": '"$EID"'}' \
| ./cli/robosim-ncat.py localhost -p 42069 --json
{"status": "Entity 16 destroyed", "timestamp": "..."}
It's gone — a lookup by its id in sim_get_state now returns nothing:
echo -e 'sim_get_state' \
| ./cli/robosim-ncat.py localhost -p 42069 --json \
| jq --argjson e "$EID" '[.world.entities[]|select(.entity_id==$e)]|length'
# 0
Destroying an id that isn't in the world is an error, not a silent no-op — same for removing a component the entity doesn't have. Bad mutations are rejected cleanly; they never crash the sim.
Why it works
candidate_velocity, not velocity. The physics tick is control → integrate → detect → resolve → commit, and the universal integrate step runs for any entity with HasPose + HasVelocity. It reads candidate_velocity — so seeding that field feeds motion directly. (See Example 1 for the full pipeline.)
One write = perpetual motion. A robot's candidate_velocity is recomputed each tick from its control law (HasMotionInput + a physics level). Our crate has neither, so nothing overwrites candidate_velocity — the value you set (in step 2, then step 3) persists, and the crate moves at that constant velocity until you change it again.
Add vs. set vs. remove. entity_add_component grants a component the entity lacks (and rejects a duplicate); entity_set_data overwrites fields on one it already has; entity_remove_component takes it away. Because physics is query-driven, motion is just a component — so removing HasVelocity (step 4) is how you make a mover static.
Spawn ≡ disk-load. The (components, data) you sent in step 1 is byte-for-byte what that entity looks like in a saved map (sim_get_state / sim_save_state). There is one entity-construction path; the wire and the disk feed the same one. So anything you can put in a map, you can spawn live.
See also
- Protocol Endpoints — every protocol command, including all the
entity_*mutators. - Protocol & Wire Format — length-prefixed msgpack framing and the
(components, data)schema. - Example 1: Adding Velocity to a Scene Object — the same idea from a server-side plugin, with the physics details.