This week I released SoccerBox, which features body colliders and a soccer ball prefab that foregoes the
VRC_ObjectSync SDK script in favor of using manually synced position and velocity data to keep the motion of the ball synced for connected players.
Here is the U# code that runs on that ball:
To be honest, I didn’t spend long writing it and it might take improvements, but let’s discuss where it’s at.
First of all, networking an object this way is not a big-brain replacement for VRC Object Sync. The soccer ball only works because it’s specifically what I’m gonna call “a freefall object,” meaning its motion has three states:
- freefalling in a (mostly) deterministic path through the air
- experiencing an instantaneous change in velocity (i.e. during 1 frame)
- rolling along the ground
Notice, the above cannot be said of other moving synced objects, especially objects that can be picked up. Freefall objects are a bit of a special case.
With that said, the ball’s programming has five-ish layers of functionality. Layer 0 is just that all players are simulating the balls rigidbody physics in parallel thanks to Unity’s physics engine. The job of the next two layers is to send updates no more often than necessary to keep that simulation reasonably in sync.
In the first layer (see line 71), I account for most instantaneous velocity changes by always calling for data to be sent from
OnCollisionEnter. This accounts for players’ body colliders kicking and hitting the ball, and bounces on solid environment. (note: body colliders are only simulated locally, and what’s not shown in the script is some different code that changes ownership of the ball to the local player before updating and sending the new data).
If nothing but the physics engine and colliders existed, then
OnCollisionEnter would account for 100% of the ball’s motion. But within the SoccerBox prefab, there are at least three other motion-affecting scripts (respawn ball, chest trigger, and boundary net trigger; I’d also expect users of the prefab to have their own stuff that affects motion). So, layer #2 (line 51) is a repeating call to send movement data updates every 0.5 seconds. This “fallback layer” accounts for all other motion–it just does so in a kind of sloppy way. Good enough for MVP.
Layer #3 is smoothing (see line 111). I’m using a synced bool to flag “fallback” (or “polling”) updates differently from collision updates, with the idea being that polling updates should be presumed unnecessary as redundant with the local player’s physics simulation. The data is received, but applying the data is skipped if the synced position is close to the simulated position, resulting in a smoother sync.
Layer #4 is some low-hanging-fruit network optimization (line 55). If the owner’s ball is a sleeping rigidbody, then stop sending updates. This isn’t perfect because a non-owner might still manage to desync their ball, but it’s good enough for now.
Some other notes:
The actual data that gets send is read during
OnPreSerialization by the owner (see line 91). The big four pieces of data relevant to a physics simulation are position, velocity, rotation (a.k.a. angular position), and angular velocity. I just plain forgot to sync angular position, but I’m pretty sure that because the ball is a sphere collider, angular velocity is all that matters (for torque).
Thanks to everyone’s good pal Faxmashine for convincing me to do the networking this way!