Advertisement

Tidy Multiplayer Design

Started by July 18, 2024 12:03 PM
1 comment, last by hplus0603 1 month, 1 week ago

Hello everyone,

I've been working on a game project: multiplayer fighter, which has client-server (authoritative) networking.
It works, but was written in a bit of salad code (worsen by the fact that was migrated from Unity).

I'm trying to tidy things up a bit, and given the client-server nature, the main flow I have goes like this (I'm not writing classes here, but operations to perform regarding players, in order of execution):

  • Players Local
    • read input
    • perform action
    • send to server
  • Players Remote
    • execute what the server says
  • Players Local
    • check what the sever said and rollback if different

Similar stuff happens in the server, but all players are treated as remote (so no input controllers read).

This means that I have similar blocks of code for Players that are local, and Remote ones, but slightly different or with some extras. Also, similar code for the immediate local action, and the deferred ones (but slightly different), after server messages. To summarize: before server, after server, local player, remote player. All the same but somehow different.

Do you know any standard or recommended way of organizing this? I've separated the network code as much as I could: it's in a separated external project. Middle classes in my Game project interact with both the Network and the Game. But still, given the multiplayer nature, there are these controversial elements, as shown in the bulletpoints.

I've thought about a “ControllerManager” class that would handle reading the controllers inputs and dealing with the local stuff. But then I thought “it'll probably go out of the ‘controller’ realm. Maybe better 'PlayerManager'. Hm, but I'll need a 'PlayerManagerLocal' and 'PlayerManagerRemote'…"

At the moment I have these responsibilities in some sort of “LogicClient” and “LogicServer” classes, but it's actually not the same if it's a menu screen or gameplay screen… And this is when a simple “button pressed” is multiplied x 4 different blocks of code…

My goal is to have a standardized structure where I don't have to worry about networking (just simple “sendMessageToServer()”), I don't have to worry about Input reading (should always be the same) and the player moves the same way (regardless if it's local or remote). If for example, if I need to have both a “createPlayerLocal()” and a “createPlayerRemote()” methods, it's acceptable. The question here is where would they belong to.

I don't know if I'm explaining myself properly; I hope you can get the idea 😅
Thanks in advance!

There are three common ways to organize this:

  1. Write different functions. Keeping them in sync when changing is annoying.
  2. Use if() statements on the inside of the functions, if the differences aren't particularly big. (Unreal does a lot of this.)
  3. Use implementation inheritance or delegation, and take a virtual function call hit each time something needs to be done differently. Essentially, replace each if() statement with a virtual function call, either to yourself (inheritance), or to a delegee.

Btw, the “read, process, send” linear sequence may make sense if your architecture is simple and your simulation step rate is equal to display frame rate.

However, it's frequently very common to hand off between the stages using some kind of queue:
* Each time you get an input, post an input event to the player handling queue.
* Each time you need to simulate a step (which may be more than once during a frame, if you use fixed frame rate!) you will dequeue input events if they exist, and handle whatever the state of the player is, and emit whatever needs to go to the server, time-stamping with whatever simulation tick it happened on.
* Each time a network tick happens, collect all the (timestamped) events you've seen, and forward in a single packet to the server.
* Each time you receive events from the server, put them in an appropriate queue to be decoded and handled at the appropriate timestep.

Whether “the appropriate timestep” is “as soon as possible” or “at some future simulation step” depends on which particular approach you take to replay and remove player interpolation/extrapolation.

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement