So Vehicles Happened…
Vehicles are something I’ve been itching to do since day one. I put them off thinking it would be too much work and we would just patch them in later.
This week I got sick of waiting and I’m pretty glad I did.
The first problem we had to figure out is how to network them.
This post assumes you already understand basic networking concepts. If you are new to multiplayer programming Glenn Fielder has an amazing series of in depth articles on everything you need here.
Most moveable objects in the HurtWorld universe are synchronized at a variable update rate with a combination of interpolation and extrapolation. They often run a decent amount of time behind the server because the player doesn’t ever drive them directly. The only thing up until now that the player can control is the player, which runs client side prediction and a few other latency hiding tricks to ensure that input has zero latency.
Vehicles get a little trickier… In Unity we use a character controller combined with a series of raycasts to move the player around. This allows us to simulate multiple movements between frames without having to wait for the physics solver. This is crucial for being able to rewind and replay for client side prediction.
A vehicle will need to rotate (character controllers can’t), simulate wheels and different physics constraints that would be a pain-in-the-ass to re-implement manually. So for now we are constrained to using the PhysX solver.
Here we run into a limitation of the Unity implementation of PhysX. The solver goes in one direction, at a fixed speed in a single universe. We can’t rewind, we can’t create an isolated island of physical objects and simulate it at will, there is one physx simulation, and it progresses with the entire Unity scene.
Fingers crossed in the future unity change this so we have access to the PhysX API directly or at least some unity specific rewind capability, not holding my breath.
Running only server side
This leaves us with one option, run the PhysX sim on the server, and tell the client about it the best we can.
First thing we need to do is get our input to the server. The client runs no simulation, just sends an RPC fairly often with the input controls we need.
Then we simply setup a rigid body on the server with four wheel colliders attached, modify the slip, torque and suspension settings as if it was a single player game.
Once we are happy with the simulation, we need to figure out what the client needs to know to simulate the car visually with minimal data.
Lets start with the body of the car. This can be synchronized like any other moving object in the world. In our case, it uses the exact same code as the rigidbody crates that spawn when you drop an item.
There are many ways to do this, ours is a simple interpolation of known states with an update frequency of 20 times per second of position and rotation. If we do nothing else, the car still simulates like a car because the server is doing all the work for us. The client knows nothing about wheels at this point, but it still looks like a car.
So we are pretty close with only a Vector3 and Quaternion per update, and these can be compressed later.
One thing to note is we have a round trip delay on any input now, as the server simulates our vehicle. Because of this we need to make sure the handling isn’t too sluggish otherwise the delay will make it feel unbearable. To compensate we made the our vehicles handle almost too well, so with an added delay they feel right.
Auxiliary data we need to simulate the car on the client
We could ask the server what the state of the suspension is per tick, but this would get expensive quickly and is mostly a waste.
Its pretty easy to assume where the wheel will be based on the chassis position. To do this we can just do a raycast at each wheel on the client up to some maximum value, then move the wheel to either touch the ground, or sit at max extension of the suspension without any data.
First thing we want to do is steer the wheels, how accurately you do this is up to you. I only needed three states (left middle and right). Then, I just smooth them out on the other end. This only uses 2 bits of a byte which we can use later on.
Secondly we need to rolling rotation, this one is a bit trickier to assume, even if we tracked relative ground movement and just rotated them, we would miss out on things like burn outs or handbrake turns because the wheels would always track with the ground. We don’t need a massive amount of precision on these values as mostly the wheels will be a blur. You can compress the RPM value into a single byte using something like the following:
private byte QuantizeRPM(float rpm, float maxRpm = 200)
return (byte)(((Mathf.Clamp(rpm, -maxRpm, maxRpm) + maxRpm) / (maxRpm * 2f)) * 254f);
private float UnQuantizeRPM(byte rpm, float max = 200f)
return ((float)rpm / 254f) * (max * 2f) – max;
We also don’t need separate speeds for left and right, with the exception of if you have a differential simulation and what to see it, we just send one RPM for the rear and one for the front.
This part isn’t 100% necessary unless you have gears and you want to hear the revs increase and drop. This can also be packed into a single byte using the method above, make sure you get the max range correct though, you won’t notice wheels hitting the RPM ceiling, but you will with engine RPM sounds.
If you’ve ever tried to implement audio for a car engine, you’ll know just pitch bending a single rev loop sound terrible, even with fading of volume. There is a great example in the Unity 5 standard assets for a 4 channel car audio system that blends between the following:
- Low revs accelerating
- Low revs decelerating
- High revs accelerating
- High revs decelerating
The idea is that when the throttle is down, the engine sounds vastly different from when your foot isn’t on the accelerator. To do this we need to know when we are throttling.
I tried just checking if the engine RPMs were rising, this sorta worked, but caused it to sound like you were running out of petrol while cornering or going up hill so we added one more bit for throttle state.
Remember above we had some spare room inside the steering byte, we can pack it into there for free
Lastly we want to show smoke and play screech sounds when the wheels are losing traction, again we could try to assume whats going on based on movement of the point on the rigidbody vs. wheel RPM, that would work fine as long as our RPM values are accurate enough. As we have a few free bits leftover inside the steering byte we can throw this one in for simplicity.
Currently I send a bool for front and back axles and treat them equally, if you are making a racing game you would probably want more precision.
For a fairly quick implementation without too much optimizing we have a decent looking car for around 640 bytes per second. I’m sure better can be done, but this will do us for now.
Here is the final result in our 4WD test terrain