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 Entity
s 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 Entity
s 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 Entity
s from any specific sound engine, but also from any particular sound-playing API. It actually decouples your Entity
s 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).