NEP Dev Update #13: Networked physics

by Cuchaz

Wow. It's been a long time since I've written one of these. A lot has happened since then.

I took another break from working on my indie game for a while to make some money. I still have my part-time job doing research at the university, but I also filled the rest of my work time doing freelance work for other games. Sadly, I'm not allowed to say anything about that yet, but the projects were fun. I'm going to save all the money I made from the freelance work to cover future business expenses and maybe art too! Someday, Non Essential Personnel will have nice art. But for now, I'll focus on making the engine and game mechanics as solid as they can be.

So, about that. This post is about networked physics and how tricky it can be. Lots of indie games just skip networking and multiplayer entirely. Networking is hard. But multiplayer is a big part of what I want my game to be, so the networking isn't optional, and it has to be good.

Last time, I blogged about improving the physics simulations in the engine to be precise enough to model ballistics correctly. This time, I'll say a little about what it takes to get two different computers to do the exact same physics simulation when they can only communicate across a laggy and unreliable network.

Let's say we want to synchronize two simulations between a client and a server. Let's also say the server simulation is the "correct" simulation, and the client simulation will attempt to match it as closely as possible.

The simple approach

The most obvious and simple way to synchronize two physics simulations like this is to broadcast the state of the server simulation at the client, and the client will adopt that state as soon as its received. To examine how effective this is, let's pick one single property of the simulation, like the position of a physical object, and plot its values over time on both the client and server. If there's 100ms of latency on the connection, that looks something like this:

Position timeseries plots on client and server for two different trajectories Time-series data for two trajectories: On the left, the object starts at rest, moves a bit, and then stops. This trajectory is like a player moving in one direction and then stopping. On the right, the object starts at rest, jumps straight up, and then falls back down. This trajectory is like a player jumping on the ground.

Aside from a little network jitter, the client does a pretty good job of matching the server. It's just about 100 ms behind. We assumed the network connection had about 100 ms of latency, so there's nothing we can do to cut that down. But we can try to hide that latency so at least it doesn't feel like we're lagging.

A slightly better approach for latency hiding

If want want to hide this latency from the player, we're going to have to do a little time travel. We don't even have to break any laws of physics here, since the physical simulation is completely under our control. Time travel is within our power. =)

Before, we were just broadcasting object positions from the server to the client. This time, let's broadcast the velocities and a timestamp too. Now, when the client simulation updates its physics each frame, it has more information to work with:

  • current position pc on the client at time tc
  • position ps on the server at some time ts
  • velocity v on the server at some time ts

Since we know there's latency on the network connection, that time ts must be behind the current time tc. So the snapshot the client sees is of a past state of the simulation. To sync it up with the client simulation, we're going to travel back in time to ts, and then predict where the object should be now. If we assume that the object is traveling at a constant velocity and doesn't bump into anything the client doesn't know about, then that prediction is pretty simple:

pc = ps + v*(tc - ts)

If we want to get fancy, we can even blend between the original position and the corrected position on the client so corrections are smoothed. If we run our two test cases again with this update step, that looks like this:

Position timeseries plots on client and server for two different trajectories, showing a little more agreement than last time

As you can see, this update step does a much better job at hiding the latency of the straight motion on the left. For most of the motion, the large 100 ms gap is completely gone. It does tend to overshoot though which causes a kind of "rubber-banding" in networked games. Notice the right side of the line where the client position goes past the server position and then corrects back.

And the parabolic trajectory on the right side is just all wrong. If this was a trajctory for a jumping player, then the latency hiding would make the client's version of the player appear to jump much higher than it really could. That's pretty much a disaster for any kind of side-scrolling game with platformer movement mechanics. Apparently a linear model doesn't describe quadratic motion very well at all. Who knew.

Let's use a quadratic update step instead

We did pretty well with broadcasing position and velocity, so why not try broadcasting position, velocity, and acceleration?

Object movement in horde is specified by forces acting on masses, so we are tracking forces and accelerations in the engine as well as velocities. So what happens when we upgrade to a quadratic update step? We're no longer assuming that the velocity remains constant, so we can match more complicated kinds of motion. But we still have to assume that the acceleration remains constant. Here's the math:

pc = ps + v*(tc - ts) + 1/2*a*(tc - ts)2

And here's the result:

Position timeseries plots on client and server for two different trajectories, showing even more agreement than last time

The straight motion is about the same, although we've cut down on our rubber-banding a tiny bit. There's a much bigger improvement in the parabolic motion though. And crucially, we've gotten the height of the jump exactly right!

And that's a wrap

There are some other tricks we can do to hide latency too. Like intentionally skewing the clocks so the client runs a bit ahead of the server. This can completely eliminate latency on one side of the network connection (client -> server) and make sure the player motion matches between the client and server very precisely. This is pretty important for making sure the player motion "feels good". The downside is that it increases the apparent latency in the other direction (server -> client). Depending on the kind of game, this might be an acceptable tradeoff. I'm trying it out for my game, but I'm not committed to it just yet. We'll see how it feels.

Hopefully now I can leave networking and physics alone for a while and work on more content updates. I'm currently on a bug crusade at the moment, but when that's done, I want to try to polish the current game and try to get some feedback from my testers. The very minimial-est version of the game game loop is working now and I'm pretty excited about it.

You can arive at a world, gather some items, and send them through the Remote Insterstellar Gateway to make the moneys. You can even build a very simple machine. That's basically the simplest version of the core mechanics of Non Essential Personnel. After this, it's all about making this game loop more exciting and interesting by adding content. But it took a long time just to get to here.