Integrated/listen servers for singleplayer

Started by
13 comments, last by hplus0603 4 years, 8 months ago

Whenever you launch singleplayer, most games will run an integrated server (Minecraft, Quake, CS:GO, etc.). How should I go about this? Should I just launch a new server process and connect my client to that once the server's launched? Should I run a server in a separate thread? My only problem with the latter is that my (Java) code uses methods such as 


if (Engine.isClientSide())
    ...

Should I use the Thread.currentThread() or new Exception.getStackTrace()[1].getClassName() to work out what part of code is running my method?

Furthermore, should I share entities and assets between the client and server? I do a physics simulation on the client as well - should that be disabled?

How much logic should a client do when connected to a dedicated server? For example, pressing E to open a door. Should the client play the animation as well or just tell the server that a user interacted with a door? Also, what about shooting an arrow - at the moment the code only runs on the server because there would be two arrows; one spawned on the server and one on the client.

Sorry for all the questions and a thanks for reading,
William

Advertisement

If your code snippet is a problem because it's a singleton, this is exactly why you shouldn't use singletons. :) Each 'half', the client and the server, should have its own top-level object which you can query that can say "I am a server" or "I am a client". There isn't any need for guesswork or inspecting system-level objects.

Should you 'share' entities - ideally not. You want this code to be as unchanged as possible relative to the normal standalone versions. That may not be quite as efficient but it will be much more maintaintainable.

Should you share assets - definitely. They're read-only data, so it doesn't matter that they're shared. You might need to add some simple wrappers to ensure that assets unused by the client don't get released if the server is using them, but it depends entirely on how you manage them.

Physics simulation - ideally, your client is totally separate from the server, and therefore you would have 2 simulations, and you resolve the differences between them the same way that you do for a remote client.

How much logic should a client do when connected to a dedicated server?  - exactly the same logic as when it's connected to a remote server.

 

Disclaimer: This is just what has worked for me, I'm nowhere near to being an expert.

From my point of view, the chance of non-deterministic stuff happening if you run your server and client in separate threads "feels" wrong for a singleplayer game. I've approached this problem by splitting each client and server frame in two. The sequence is the same regardless of whether you're in singleplayer, running a dedicated server, connected to a remote server or whatever, just with some steps removed. A simplified overview:

 

First, the client processes all its input and sends it to the server. Local movement prediction is performed.

Second, the server reads the input it has gathered for the upcoming frame and prepares to step the simulation.

Third, the physics simulation is stepped. This is not controlled by either client or server module, and so always happens exactly once regardless of which modules are running.

Forth, The server integrates the results of the physics step and updates the game state. It then sends the game state to the clients.

Fifth, the client integrates the most-recent state is has received from the server.

 

I use component-based entities, where some component structures are shared between the client and the server. Most notably, physics and movement state components are shared. This means that if you are running both the client and the server, you don't need to "send" that part of the game state, the client has direct access to it in the fifth step. You also don't need to do local movement prediction since the available state is perfectly up to date anyway. The one complication in this is determining whether the client or the server is responsible for allocating and releasing the shared components. For now, I'm using a very simple approach of setting a value when the game starts and referring to that before trying to execute any "both module" code. There's probably a more elegant solution, but this is sufficient for my needs.

As for prediction, as a general rule you should only be predicting your own motion. In your example of opening a door, if there's a lead-in animation for your character before the door opens, go ahead and start that, but don't actually open the door until the server says so. You're aiming to give immediate feedback to your player inputs, but not make significant changes to the world.

And now you see why globals and singletons are a bad idea! ?

In general, I vastly prefer systems where the configuration is explicitly passed along, so anything that needs to know whether it's client or server, would have a "NetworkEnvironment" struct that tells it about that. That also makes unit testing the code without side effects much easier.

If you're too far down this hole to un-dig your way out of it, then spawning a new process seems reasonable, as long as you don't need to run on mobile targets or consoles, where doing so may not be well supported.

Another option is to treat a "local game" as "a server with a renderer," btw. That's worked for some games.

enum Bool { True, False, FileNotFound };
10 hours ago, Kylotan said:

If your code snippet is a problem because it's a singleton, this is exactly why you shouldn't use singletons. :) Each 'half', the client and the server, should have its own top-level object which you can query that can say "I am a server" or "I am a client". There isn't any need for guesswork or inspecting system-level objects.

That is probably the best. My only problem with that is then I'll have to pass engine objects around and having a dependency like that can be a pain. I could have an instance of the Engine per thread/group of threads? One group for the client; one group for the server.

10 hours ago, Kylotan said:

How much logic should a client do when connected to a dedicated server?  - exactly the same logic as when it's connected to a remote server.

By dedicated, I mean remote server (i.e a server that is its own process and always running). It was a question about networking in general (not just listen servers).

7 hours ago, OandO said:

Disclaimer: This is just what has worked for me, I'm nowhere near to being an expert.

From my point of view, the chance of non-deterministic stuff happening if you run your server and client in separate threads "feels" wrong for a singleplayer game. I've approached this problem by splitting each client and server frame in two. The sequence is the same regardless of whether you're in singleplayer, running a dedicated server, connected to a remote server or whatever, just with some steps removed. A simplified overview:

 

First, the client processes all its input and sends it to the server. Local movement prediction is performed.

Second, the server reads the input it has gathered for the upcoming frame and prepares to step the simulation.

Third, the physics simulation is stepped. This is not controlled by either client or server module, and so always happens exactly once regardless of which modules are running.

Forth, The server integrates the results of the physics step and updates the game state. It then sends the game state to the clients.

Fifth, the client integrates the most-recent state is has received from the server.

 

I use component-based entities, where some component structures are shared between the client and the server. Most notably, physics and movement state components are shared. This means that if you are running both the client and the server, you don't need to "send" that part of the game state, the client has direct access to it in the fifth step. You also don't need to do local movement prediction since the available state is perfectly up to date anyway. The one complication in this is determining whether the client or the server is responsible for allocating and releasing the shared components. For now, I'm using a very simple approach of setting a value when the game starts and referring to that before trying to execute any "both module" code. There's probably a more elegant solution, but this is sufficient for my needs.

As for prediction, as a general rule you should only be predicting your own motion. In your example of opening a door, if there's a lead-in animation for your character before the door opens, go ahead and start that, but don't actually open the door until the server says so. You're aiming to give immediate feedback to your player inputs, but not make significant changes to the world.

Thank you so much for your advice. I'll try to implement it once/if I can separate my singletons. I'm also using component-based entities and each frame I track certain variables and if they've changed, I send an update to the client. In step 3, do you just check which modules are enabled and then return based on that?

9 minutes ago, hplus0603 said:

In general, I vastly prefer systems where the configuration is explicitly passed along, so anything that needs to know whether it's client or server, would have a "NetworkEnvironment" struct that tells it about that. That also makes unit testing the code without side effects much easier.

i.e passing an Engine object to methods when running them?

10 minutes ago, hplus0603 said:

Another option is to treat a "local game" as "a server with a renderer," btw. That's worked for some games.

I'll see if that is an option.

2 hours ago, willepj said:

 I'm also using component-based entities and each frame I track certain variables and if they've changed, I send an update to the client. In step 3, do you just check which modules are enabled and then return based on that?

Step 3 doesn't care which modules are enabled, it does exactly the same thing regardless. It steps the physics simulation, and stores the new transformations and derivatives in the relevant physics component. Both the client and server know the layout of the physics component structure, and know how to get a pointer to a component they are interested in reading from or writing to.

In my specific case, the movement state component is also shared between modules in the same way. There's some code duplicated between client and server along the lines of:

Function_A: Turn "This player wants to run forward" into "Apply an impulse to this physics body in a specific direction." and...

Function_B: Turn "Physics body has this transformation" into "This player has this new movement state."

If you are running the server, Function_A is triggered by the server during Step 2, and Function_B during Step 4. If you're not running the server, the client is aware of this, and runs Function_A during Step 1 and Function_B during Step 5, but only for the local player.

Voila: values calculated by the server if you're running it, by the client if you're not, and available to the client either way.

12 hours ago, willepj said:

That is probably the best. My only problem with that is then I'll have to pass engine objects around and having a dependency like that can be a pain. I could have an instance of the Engine per thread/group of threads? One group for the client; one group for the server.

Passing objects around is what programming is about. :) Usually it's best to find ways to live with that rather than putting things into the global namespace.

One approach that I've used and which you see in engines like Unreal is for your main game entities and systems to all get a reference to the engine and/or the world when they are created. Then it's just a member variable they can query at any time.

You might also consider whether you even need all these objects to have access to the Engine object at all. Usually the top level objects 'orchestrate' the other objects, which don't typically need to have backwards references to everything else. And if you find that everything is referring to this one other object, it's very likely that object has too many responsibilities concentrated within it.

 

12 hours ago, willepj said:

By dedicated, I mean remote server (i.e a server that is its own process and always running). It was a question about networking in general (not just listen servers).

That's a question far too broad for a single forum post unfortunately. It depends entirely on your game. Some games can have absolutely no client-side logic and are little more than just a renderer. Most games have the majority of logic on the server with some client-side logic for added responsiveness. And peer-to-peer games have no server at all. Usually the ideal for a client-server game is for the server to handle all the essential logic, but you then have to consider how to handle the time delay between a server deciding something has happened and a client needing to act on it, especially in situations where the client has a slightly different view of the game due to this delay.

 

12 hours ago, willepj said:

In step 3, do you just check which modules are enabled and then return based on that?

Just be careful if you're trying to do a single physics step on a process that is both a server and a client. Usually what we call physics includes a ton of gameplay stuff, and a lot of that may need to act differently depending on whether it's in the context of a server or a client. If it's in the context of both... that can be tricky.

 

1 hour ago, Kylotan said:

One approach that I've used and which you see in engines like Unreal is for your main game entities and systems to all get a reference to the engine and/or the world when they are created. Then it's just a member variable they can query at any time.

I did something not too dissimilar in an old engine of mine. Although, the only places where I call Engine.isClientSide() and Engine.isServerSide() is when setting game variables (like sv_cheats 1), running a command (like noclip) and shooting a weapon. These variables and commands should be accessible from both the server and the client (if running on a listen server), and so it wouldn't be possible to instantiate them with an Engine instance.

1 hour ago, Kylotan said:

You might also consider whether you even need all these objects to have access to the Engine object at all. Usually the top level objects 'orchestrate' the other objects, which don't typically need to have backwards references to everything else. And if you find that everything is referring to this one other object, it's very likely that object has too many responsibilities concentrated within it.

i.e engine.setVariable(var, value) rather than var.setValue(value). The whole point of the Engine class (which can be found here) is to allow common code (code in the Engine project - like weapons, variables, commands) to access client or server side code. When the game is launched, the client sets Engine.instance to new ClientEngine(), which allows GameVariable#setValue(...) to send a packet telling all clients connected or the server its connected to that the variable has changed (via Engine#broadcast). Other things it does is events - onEntitySpawn, onComponentAdd, onMonitorValueChange, etc. and ServerEngine can override these and send a packet whenever a component is added or changed or an entity is spawned. Furthermore, it also acts as a Scene object, allowing common code to add an entity to the scene, or find all entities with a specific component.

Thanks for your quote. It got me thinking. I'll see if I can use an event bus instead of relying on polymorphism. Do you think I should create AddEntityToSceneEvents and GameVariableUpdateEvents? The former would do the same as Engine.getEntities().add(...) and the latter would replace Engine.getInstance().broadcast(...). Whilst writing this reply, I also noticed that I use the Engine class in the Client, Editor and Server projects a lot. The code in these projects have access to GameClient, GameEditor and GameServer respectively, which are singletons all contain a Scene instance and BaseNetworkable as well.

Before I start implementing this, I'd just like to know your opinion on adding an event system (of course with the ability to add multiple listeners to an event), and therefore (hopefully) reduce complexity and allow for the Engine class to be a singleton.

I think that your Engine class should probably be a bunch of other, smaller classes, rather than a 'convenient' place for everything.

I also think that a lot of your other objects don't really need to access the engine or send messages directly or anything like that. For example, if I move an object on the server, it shouldn't need to broadcast a message itself. There should instead be a system that looks for objects that have moved and does the broadcasts on their behalf. The system can use events and react instantly to the movement if you like - but the main thing is that the movement code should not care about the networking.

I don't have a strong opinion on whether you should use events or polymorphism. It's all just a fancy way of getting one function to call another with reduced coupling. The main thing is that you don't want one monolithic class with a bunch of responsibilities which you then need to subclass with another, bigger class that handles all the same responsibilities. Split it into smaller interfaces, create objects with (ideally) just 1 responsibility, and you'll quickly see that most of your game doesn't need to 'reach out' into the Engine anyway.

1 hour ago, Kylotan said:

For example, if I move an object on the server, it shouldn't need to broadcast a message itself. There should instead be a system that looks for objects that have moved and does the broadcasts on their behalf.

Sorry. I should have made myself more clear Engine#broadcast will send a message to all clients on the server, and on the client, it will send a message to the server. I do have a system which tracks variable changes and when it detects a change, it calls Engine#onMonitorValueChange.

1 hour ago, Kylotan said:

Split it into smaller interfaces, create objects with (ideally) just 1 responsibility, and you'll quickly see that most of your game doesn't need to 'reach out' into the Engine anyway.

Let's use the example of commands and game variables. Let's create an IGameVariablePermissionManager or something like that. When we call GameVariable#setValue(..) if GameVariableManager.permissionManager.canSet(this) returns false, then return and don't set the variable. However, should I have an IGameVariablePermissionManager for the server and client or should it look at some variables (like sv_running or sv_dedicated) in order to work out if it a client, integrated server or dedicated server?

I will be splitting up my Engine class soon though.

This topic is closed to new replies.

Advertisement