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)
# update entity's position
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.
def __init__(self, sound_engine):
self.sound_engine = sound_engine
# update entity's position
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
def play_sound(self, 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
config["sound_engine"] = game_engine.sound_engine
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_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.
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).