6
\$\begingroup\$

I am using a client-server model to write a multiplayer-capable game (in Java). Currently the clients and the server are using the same code to run the game logic, in order to enable client-side prediction. This makes sense to me, since I don't want to have to duplicate my entire codebase to make this work.

However, there are certain things that should only be done by the clients (e.g. send user input to the server, update camera position, play sounds, etc.) and certain things that should only be done by the server (e.g. send messages to the clients, remove entities, etc.).

How can I share the same code between my client and server without peppering it with "if client", "if server", etc.?

EDIT: It has been suggested that I should extract all common code into a library and have 2 separate projects - the client and the server - each of which should extend the common code to include custom functionality. But won't this still result in a lot of duplication?

To take an example: let's say I have an Entity class. Whenever an Entity moves, it should play a sound if it's on the client, and send a TCP message if it's on the server. Therefore I create 2 other classes, ClientEntity and ServerEntity, both of which extend Entity. Now all of a sudden my Zombie class, which previously extended Entity, needs to be duplicated across the client and server projects so that the client version can extend ClientEntity, and the server version can extend ServerEntity.

If I have a lot of classes that all extend Entity, then this results in a lot of duplication. What am I missing here? :)

\$\endgroup\$
  • \$\begingroup\$ It would help what language you are using to write your game. If it is an object oriented you can override some functions and add the client/server specific code there - but without mor einfo its impossible to say. \$\endgroup\$ – Niko Nov 18 '14 at 11:30
  • \$\begingroup\$ @Dan Your ammendment ins't clear to me. Are you talking about duplicated entity classes or duplicated entity instances? In either case, those entities which only exist on one shouldn't also exist on the other. They shouldn't be communicated via network. \$\endgroup\$ – Philipp Nov 19 '14 at 15:36
11
\$\begingroup\$

As others have said, the first step is separating logic that's shared from logic that's not. While it's great to draw that line wherever it's clear, your addendum illustrates that sometimes you don't have a clean line to split the code down.

So, how do we solve cases where the client and server want to do semantically the same thing (play a sound), but take different code paths to do it (actually play it, vs instruct clients to play it)?

It sounds like you presently using a hardcoded play_sound() method, like so:

(my Java is rusty, please excuse that I'm writing these examples in Python)

class Entity():
    def move(self):
        # update entity's position
        game_engine.play_sound("entity_moved.wav")

entity = Entity()
entity.move()  # will use the game engine's sound engine

This locks you into always using the game engine's play_sound method. You could subclass entities, as you stated, but there's a better way!

Component Pattern (a.k.a. Dependency Injection)

The solution is to parameterize the sound component in your Entity class, a la the Component Pattern. When you initialize a new Entity, you pass into the constructor the sound engine you want to use.

class Entity():
    def __init__(self, sound_engine):
        self.sound_engine = sound_engine

    def move(self):
        # update entity's position
        self.sound_engine.play_sound("entity_moved.wav")

entity = Entity(game_engine.sound_engine)
entity.move()  # will still use the game engine's sound engine

The benefit of this approach is that the client and server code can initialize Entitys with different sound engines.

# On the server
class ServerSoundEngine():
    def play_sound(self, sound_file):
        notify_clients(sound_file)

entity = Entity(ServerSoundEngine())
entity.move()  # will notify clients to play the sound, instead of playing the sound itself

You can even store which sound engine to use with the rest of your game's in-memory runtime configuration, so you don't need a conditional step when initializing each Entity.

def initialize_game():
    if is_client:
        config["sound_engine"] = game_engine.sound_engine
    else:
        config["sound_engine"] = ServerSoundEngine()

# ...

entity = Entity(config["sound_engine"])

This also makes it much easier to unit test your code; you can use a mock sound engine in your unit tests that only keeps track of whether it was called, and make assertions about whether it was called correctly.

This idea can be taken to your other examples, too: instead of branching on is_client/is_server all over the place, you can initialize your game with a RealCamera or a FakeCamera. Initialize a real camera class on clients, or a dummy class with no-op methods when you're on the server, and voila! You've clearly separated what a client does from what a server does, attained high code reuse, and have no code branches once the game is initialized.

As the article suggests, you'll find the Component Pattern helpful in many other areas of your game.

To understand the Component Pattern better, here's Sandi Metz at RailsConf 2015 giving a super cool talk on dependency injection as an alternative to inheritance.

Observer pattern

You also have the option of taking this one step further and sidestepping the "configure the class" issue entirely. Your sound events may be a good candidate for the Observer Pattern.

With the Observer Pattern, your Entitys would broadcast an event (e.g, ZOMBIE_MOVED), which your game can detect and handle at a later point while updating the frame. This not only decouples your Entitys from any specific sound engine, but also from any particular sound-playing API. It actually decouples your Entitys from playing sound entirely - Entity movement logic no longer needs to know that playing a sound is a side effect of moving. This keeps your movement code simpler and delineates responsibilities in your code more strongly and clearly.

With this pattern, you would no longer need to configure entities with a sound_engine. Also, when writing unit tests, you wouldn't have to write a mock class class anymore - you could just assert that the event was emitted. This all comes at the expense of having one more moving part in your game, and one more layer of abstraction.

(Also see this article for more on the philosophy of decoupling side effects from logic).

\$\endgroup\$
  • \$\begingroup\$ Thank you for taking the time to write such a detailed and informative answer! This is EXACTLY the sort of thing I was looking for - you hit the nail on the head! \$\endgroup\$ – Dan Nov 20 '14 at 18:06
2
\$\begingroup\$

The cleanest way is to create a "core" project that includes shared game code.

You can use either interfaces or abstract classes to prepare your game code there and use inheritance [1] to extend/implement these classes in either the server or client side.

That way you do not need any if statements to distinguish between client and server code.

You should also make sure you do not rely on complex libraries in these core classes (i.e. there must not be any image reading -> thats for the client; or db stuff -> thats server only) in there. If you have game-data classes which both the client and the server can/should with a shared aspect, make it abstract oruse an interface.

[1] http://www.homeandlearn.co.uk/java/java_inheritance.html

\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer. Unfortunately I can't vote up but it was helpful. \$\endgroup\$ – Dan Nov 19 '14 at 8:59
2
\$\begingroup\$

Extract any shared code to a library which you maintain separately.

Then implement your client and server as a separate applications which implement anything specific themselves and reference the library for any shared functionality.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer - please see my edited question! \$\endgroup\$ – Dan Nov 19 '14 at 13:56
0
\$\begingroup\$

The solution I prefer is to put all game-specific logic in the client. The server only acts as an I/O reflector to make sure all clients are up to date. One benefit of this approach is that the same server can serve different games, since it knows nothing about the content of any specific game.

There are a few things that have to be done centrally, such as retain the current client list, and remember the canonical game state, but these are structured as game-neutral services that the clients use.

It works quite well for 60+ different games at Boardspace.net

\$\endgroup\$
  • 1
    \$\begingroup\$ The downside to this method, as I understand it, is that it makes it (relatively) easy for the client to cheat, since clients are authoritative about their own state. \$\endgroup\$ – Dan Nov 25 '14 at 8:49
  • \$\begingroup\$ Ultimately, you can't trust clients. Yet you must. This isn't qualitatively different regardless how you split the workload between the clients and server. All the game logic is duplicated among the clients, so if some client did something illegal, it could be noticed. Other mechanisms for cheating, such as stealing hidden information or providing computer assistance (ie; gun proxy or other ai) is not different no matter how the client works. \$\endgroup\$ – ddyer Nov 25 '14 at 17:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.