People like to argue about the respective merits of client/server versus peer-to-peer network topologies, but personally I think this is the wrong debate. Who says you have to pick just one? I’m a big fan of hybrids that combine the best of both worlds.
Network programming is all about compromise. It is impossible to have everything be both 100% accurate and 100% lag free, so we have to make tradeoffs. Sometimes we can put up with a bit more lag in order to increase accuracy, while other times it makes more sense to sacrifice accuracy in the interest of a snappy response.
The million dollar question: which machines control what data?
For every piece of data in your game, you can do a cost/benefit analysis by considering these questions:
- Is this data self contained, or is there other data that must be kept consistent with it?
- Are all players equally sensitive to lag in this data, or does one care about it more than others?
- How much does it matter if this data is lagged? Can I tweak the game design to make lag matter less?
- If we try to predict how this data will change, but guess wrong, is that a problem, or can we smooth over the error?
The answers will suggest how the data should be controlled:
- If there are many pieces of data that must be kept consistent, they should all be controlled by a single authority machine.
- If one player cares more than others, this data should be controlled by their local machine.
- If lag matters a lot, you should apply prediction algorithms to the data.
- If prediction errors are not acceptable, you should not use prediction.
Note how the choice of what machine controls the data (questions 1 and 2) is independent of whether to apply prediction algorithms (questions 3 and 4).
Moving spaceships around the screen
- Low latency is important, especially for the player steering the ship.
- It is ok if the ship is in slightly different places on each machine, as long as they are roughly similar. The worst that could happen is two ships might collide and bounce in a slightly wrong way, but the game can survive this.
- Slightly wrong prediction guesses are not fatal. We can smooth the ship from the wrong position toward the correct one, and nobody will notice.
Conclusion: each spaceship should be controlled by their local player machine. Prediction algorithms should be applied.
Winning a race
- Latency is not particularly important. It is ok if it takes half a second after you cross the finish line before the results come up (the game could hide this delay by displaying a cool swooshy unfurling animation).
- It is not ok if each machine gets a different result. A race must never have two winners!
- Wrong prediction guesses are not acceptable. It would be terrible if the game said “Well done: you finished 1st” only to change its mind a moment later.
Conclusion: a single authority machine should decide the race results. Prediction should not be used.
- Latency is important for the guy who died, but not particularly for anyone else.
- It is not ok if each machine gets a different result. If I’m dead on my machine, I must be dead on yours, too.
- Wrong prediction guesses are not acceptable. It is no good if we show a flamboyant animation of our dude spinning around, blood spraying everywhere, screaming, then he crumples to the ground with one final death-rattle, only to change our minds and bring him back to life again!
Conclusion: each machine should be responsible for deciding when their local player is dead. Other machines should not attempt to predict this.
Enhancement: you could get clever and apply some visual-only prediction here. For instance if my machine thinks I got a headshot, but isn’t 100% sure yet because it doesn’t know exactly where the target player was, it could start playing an “I’m wounded” animation. This gives some immediate visual feedback, while still keeping its options open. If a later network packet confirms the kill, it can transition from the wounded animation into the final death crumple, while if the kill prediction turns out to have been a mistake, it can just cancel the wounded animation and go back to the normal state with no harm done.
Getting into a vehicle
I once worked on a prototype for a game where you could run around on foot and get in or out of many different vehicles. While you were in a vehicle, this was exactly the same scenario I previously described as “moving spaceships around the screen”, but getting into vehicles was different. You can’t have more than one player driving a vehicle at the same time!
To handle this, we implemented dynamic object ownership. This gave us a new piece of metadata: in addition to deciding which machine controlled each vehicle, we now also had to decide which machine controlled which machines controlled each vehicle.
The vehicle state (steering, physics, collision, etc) was controlled by whatever machine was currently driving it, but we put a single authority machine in charge of deciding who that driver should be. When vehicles were empty, the coordinator allocated them to whatever machine was currently controlling the fewest.
When you ran up next to a vehicle and pressed the “get in” button, here is what happened:
- Client machine sends a “please can I get in” packet to the vehicle coordinator.
- Client machine starts playing an animation of the character climbing into the vehicle, but cannot actually drive it yet since they do not control it.
- Vehicle coordinator assigns them to control the vehicle.
- By the time the “ok, you control it now” packet gets back to the client, the “get into driver seat” animation has finished playing, so they are ready to drive off. The animation hid the lag, so players never realized it took a moment for them to gain control.
- But if two people try to get in at once:
- Vehicle coordinator says “sorry, you can’t have this vehicle” to one of them.
- Client machine cancels the “get into driver seat” animation, and gets back out again.
- The other player (who got in first) can now drive off.
When you were in a vehicle, this was a peer-to-peer architecture, but the more important decisions of getting in and out were handled in a client/server fashion.
The idea of giving some important decisions to a single authority machine does not require a single machine to be the authority for all data. You could have machine A be the authority for getting in and out of vehicles, while B is in charge of coordinating powerups, and C takes care of deciding when to end the session and tracking who has won.
And that is all I have to say about networking.