Parenting Entities
Summary
This tutorial shows how to make one entity ride another: a character standing on a moving platform, a turret riding a vehicle hull, a crate carried on a lift.
Read this part before anything else, because it is the opposite of what most engines lead you to expect. Reactor has no synced parent hierarchy, on the server or the client. Nothing propagates a parent's movement to its children. Entity.Parent moves nothing, does not reparent Unity transforms, and is not what holds a rider on a platform. It is a client-side label recording a relationship you maintain yourself.
Parenting is therefore something you build rather than something you enable, out of three cooperating pieces:
The server applies the parent's per-frame motion to the child by hand, every frame.
The link is published as an ordinary entity property holding the parent's entity id.
The client reads that property and sets
Entity.Parent, so client code can walk the relationship.
Only the first of those moves anything. Skip it and your rider stands still while the platform slides out from under it, no matter what the other two are doing.
It builds on Networked Properties and RPCs and the physics queries from Detecting Collisions.
Requirements
Completion of Networked Properties and RPCs
There Is No Hierarchy to Sync
On the server, ksIServerEntity has a transform but not a parent transform. Setting one entity's position does not move another. This is deliberate: entity transforms are synced independently, and a server-side hierarchy would mean resolving that hierarchy before every sync.
On the client there is no transform hierarchy either. Every entity game object sits directly under the room object regardless of what its Entity.Parent says, and you can confirm that in the Unity hierarchy at runtime: a turret parented to a hull is still a sibling of it, not a child.
So there is nothing anywhere that moves a child when its parent moves. That job is yours.
Each frame you measure how far the parent moved and rotated since the last frame, and apply the same motion to the child. The three pieces fit together like this:
The server half is what actually keeps the child in the right place. The client half records the relationship on the client, so client code can walk from a child to its parent and back through Entity.Parent, Entity.Children and Entity.ForEachDescendant instead of re-reading the property and resolving the id every time it needs to know what something is riding.
Entity.Parentis a client-side relationship and does not reparent the Unity transform. Entity game objects stay where the entity factory put them, andtransform.parentdoes not change. If you need the child's game object to be a Unity child of the parent's, reparent it yourself.
Applying the Parent's Motion
Both kinds of parenting below use the same helper. Put it in a common script so the client can use it too.
Motion.cs
The last term is the one worth understanding. newPosition - lastPosition alone is enough for a parent that only slides. As soon as the parent rotates, a child standing away from the parent's center must also swing around it, and rDelta * localOffset - localOffset is that arc. Leave it out and a rider on a turntable will stay put while the platform spins underneath them.
Note that it is a function of localOffset, so it contributes nothing for a child sitting exactly at the parent's center. A turret mounted on the middle of a hull will look correct with or without it. Test with an off-center child, or the bug hides.
A Fixed Parent
The simplest case is a child attached once and never reassigned, such as a turret on a hull. Store the parent, publish its id, and apply its motion every frame.
seTurret.cs
A Dynamic Parent
A character should ride whatever it happens to be standing on, and that changes as it walks from the ground onto a platform and off again. Raycast down each frame and use whatever you hit.
seRider.cs
The destroyed check is not optional, and it is easy to leave out because nothing fails visibly without it. A dynamic parent writes its parent property every frame, including the frame on which its own entity is destroyed. Removing the handler in
Detachedis not enough on its own, because the entity can be destroyed partway through a frame while a handler registered by its scripts still has a turn to run. The result is:Reactor refuses the write and carries on, so nothing breaks. It is a warning rather than an error, and it appears when a player disconnects or leaves, which is exactly when nobody is watching the server log.
Contact records what the parent's transform was at the moment of the hit, so the next frame has something to subtract from:
Contact.cs
Receiving the Link on the Client
Clients learn about parenting from the property. Add a client entity script to anything that can be parented.
ceParent.cs
Setting the initial value in
Initializematters as much as handling the change. Properties are synced to a client when it first sees the entity, so a client that joins after a turret is already mounted gets no change event and would otherwise never parent it.
Parenting needs no predictor configuration. The parent link is an ordinary synced property holding an entity id, not a predicted one, and parented entities use whatever predictor they would have used anyway. Nothing in this tutorial requires a predictor override or a PredictedProperties entry.
Update Order
A child reads its parent's transform, so the parent must have moved first. Reactor runs update handlers in order of their integer group, which is what you use to enforce that. A chain of movers, hulls, turrets and riders is ordered like this:
0
platform mover
platforms and lifts
1
hull
vehicles, which may be standing on a platform
2
turret
turrets, which ride a hull
3
rider
characters, which may stand on any of the above
Get this wrong and the child reads the parent's position from before it moved this frame, so it trails by exactly one frame of the parent's motion. It does not accumulate, and it disappears the moment the parent stops, which is what makes it easy to miss.
The size of the error is the parent's speed divided by the server frame rate. Measured on a platform travelling 35.36 units in 4 seconds (8.84 units/second) at a 60 Hz server frame rate, moving a rider from group 3 to a group ahead of the platform produced a lag of 0.147 units, directed opposite the platform's travel, against a predicted 8.84 / 60 = 0.147. At walking speeds the same mistake is a couple of millimeters and invisible until something moves quickly.
Update groups run in numeric order, and the physics step falls between group -1 and group 0. Handlers sharing a group run in registration order, which is not something to rely on. Give parents and children distinct groups.
Spawning a Parent and Child Together
A prefab may contain more than one ksEntityComponent, for example a hull with a turret as a child game object. Spawning it produces one entity per component, and Room.SpawnCollection returns them so you can link them up.
Because both entities share a type string, distinguish them by the scripts they carry rather than by
Entity.Type.entity.Scripts.Get<seTurret>() != nullidentifies the turret;Entity.Typecannot.
Checking Your Work
Parenting bugs are easy to miss by eye, because a rider that is slightly wrong still looks attached. Measure instead. Take the child's offset from the parent, expressed in the parent's local space:
While the child is parented and not moving under its own power, that value must stay constant:
If it stays constant while the parent slides, translation is correct.
If it stays constant while the parent rotates, and the child is off-center, the arc term is correct. This is the check that catches a missing
rDelta * localOffset - localOffset.If it shifts when the parent starts moving and returns when the parent stops, your update order is wrong.
For reference, a correct implementation holds that offset to within a few millimeters through twenty degrees of parent rotation, on a child sitting 4.6 units out from the parent's center.
Common Problems
Child ignores the parent entirely
Nothing is publishing Prop.PARENT, or the client has no script setting Entity.Parent from it.
Child follows a sliding parent but not a rotating one
The arc term is missing from the motion helper, or the child is being tested at the parent's exact center where that term is zero.
Child trails the parent while moving and catches up when it stops
Update order. The child runs before the parent has moved.
Child is flung away when stepping between platforms
The motion is being applied across a parent change. Only apply when this frame's parent is the same as last frame's.
A client that joins late never sees the parenting
Initialize reads the change event but not the current property value.
"Cannot set property N on destroyed entity" warnings when a player leaves
An update handler wrote the parent property on the frame its entity was destroyed. Return early on Entity.IsDestroyed.
Parenting works in the editor but not for a second client
The parent's entity id is being sent as something other than the entity id, or the property id collides with another user of the same id.
Where to Go Next
Shared Entities and Ownership Transfer: let players take turns driving the vehicle the turret is mounted on.
The Server Update Loop: update groups and the time API.
Automatic Client Data Syncing: how properties reach clients, and how prediction uses the parent relationship.
Last updated

