2

Intro

I'm writing an FPS game in c++. There is a timed game mode, players run around a map shoot from a variety of weapons which are either hitscan or projectile based, when a shot connects, based on information about the game state players are assigned points, when the time is up the player with the most points wins.

I am currently working on the client side, where we use opengl for graphics, glfw for input and window, and other libraries for physics and sound. In each of the designs I've thought about there are always systems: Graphics, Input, Physics, Sound, Gameplay which store their own state and can be interacted with through methods they expose.

Design 1

In this approach in int main() { ... } we initialize the systems and store them in variables which reference the respective systems, whenever a function is called which needs a certain system we pass it in as an argument.

For example, we have character.update_velocity(delta_time, Input, Sound) and then inside we have the input object is needed to be passed into this call because we check Input.forward_pressed etc... in order to change their velocity depending on what keys are pressed. if (Input.jump_pressed) { Sound.play_sound("jump") ... }.

Apparent Benefit: You're focusing on telling the computer on what to do to make the game happen rather than other things.

Apparent Downside: Your function signatures get cluttered with systems. You need to work around callbacks with fixed signatures because you can't pass systems directly as parameters into them.

Design 2

We use the same approach, but instead of passing the systems that are needed to specific function calls, we instead make each of the systems global which means that the functions don't need to take them in as arguments, but the code inside these functions stays the same as design 1.

Apparent Benefit: The signature of functions is not cluttered by a bunch of systems. Fixes the callback thing mentioned in Design 1.

Apparent Downside: It's not immediately obvious what systems a function call will use by looking at its signature

Design 3

In this design we use an event system to separate concerns. Now instead of passing in the systems into the respective function calls that need them, the function calls emit signals which then call the code. There is a class called SignalMediator which contains all of the systems and then sends signals to respective systems whenever certain events get triggered.

For example, we have character.update_velocity(delta_time, Input) when a jump occurs we call SignalMediator.emit_signal("jump_occurred") now the Sound object subscribes to the SignalMediator's jump_occurred signal and defines what should occur when this happens.

Apparent Benefits: Our code becomes easier to read because separate functions only interact with a single system at once.

Apparent Downside: The code that makes the game happen is now spread out over more files so it's harder to see all of the functionality at once

Question

I've started writing the game following design 1, but I'm heavily considering design 3 and I've frozen myself up because I'm not sure how design 1 will scale over time as opposed to design 3. I'm also curious if any of the designs I've listed have performance overheads that I'm not considering.

If anyone has insight into these designs or other designs that keep performance, code complexity and maintainability in mind could you share your experiences?

5
  • 2
    This will undoubtly be downvoted and close voted by the dogmatic hardliners on this site which downvote every question which looks a little bit broad, but I like the question and think it should be possible to give you an equally broad answer (so +1 from me).
    – Doc Brown
    Commented Apr 10 at 5:52
  • 2
    I went to look at how the various open source Quake implementations do this, to see if they would be a good read, and Quake 1-3 use globals everywhere and are written in C. So design #2 would definitely work, even if it's not pretty.
    – pjc50
    Commented Apr 10 at 8:56
  • 1
    Design 4: KISS. If you didn't have systems that drive arbitrary wedges inbetween very related things, you'd have never made this question. I personally don't see a reason for abstracting Sound for example. Usually it's a separate worker thread whose job is to dequeue representation of what sound/music it should start sending to the audio device. So the only abstraction you need is play_sound("jump") that can be called from anywhere because that actually makes it trivial to read your code unlike anything else. Or you know, sound_system.enqueue("jump"); If that makes you feel better.
    – shinyoi
    Commented Apr 11 at 5:20
  • @shinyoi Thanks for the feedback. Imagine there's a bug reported that the player jumps different heights for some reason, in the middle of update_velocity(...) we make a call play_sound(...) so we can't run this function without the sound system up and running. So we comment that line out and make some tests for it which we can run to verify the height of the jump, but now the production version is missing the sound of the jump, how would you have testable and production ready code at the same time with your proposed approach? Commented Apr 12 at 1:54
  • @cuppajoeman That's an entirely orthogonal problem, however this is as easy as defining a macro that turns play_sound into a no-op while testing without commenting out things arbitrarily, e.g. #define play_sound(x) do {} while(0) or even a different function call so you can test your game as it is including fake audio thread with simulated lag for example. Testing environment will always be hacky and buggy mess and I think it's not beneficial to worry about it that much. Ease of writing code that works, and final program doing its job is the primary goal.
    – shinyoi
    Commented Apr 16 at 10:00

2 Answers 2

4

What strikes me first when reading your question is that a character object seems to require a direct dependency to the Input system. This applies to all three of your design alternatives and looks wrong to me (ok, "wrong" is surely opinionated, lets say it would not be my first design choice).

I think it is unlikely a character object in a game needs to know the player uses a keyboard to control it, and which keys will make it "move forward" - that is not it's concern, that's too much knowledge about the technical environment. This kind of dependency also makes it hard to unit test the methods of the character, since you cannot test it without an Input system (or at least a more-or-less complex mock for it).

Note this is not just an issue of "function signatures", it is an issue of dependencies. The issue stays the same in design #1 or #2. The function signatures are just a symptom of the desease. Worse, design #2 will hide these symptoms, but not eliminate the cause - the dependency stays the same, but is actually harder to spot.

So design #3 - using events - is clearly a step into the right direction. It already eliminates the dependency from the Sound system, which is almost the same like the issue with the Input system.

I would, however, recommend to go a step further:

  • the Input system should emit signals whenever something like a "key pressed" event occurs

  • there could be a middle man object like a "KeyProcessor" (or maybe a "GameMoveController") which subscribes to these events and translates technical key-press events to domain actions. This could include picking the right character object from whereever the character objects are stored in your program, and then calling methods which tell the character how to change their state.

    Of course, your character objects may provide high-level methods for moving forward or jumping, which may cause a complex internal state update. Still I would avoid those methods to depend directly on some Input object, which may contain 150 other methods or properties from which the character will only need 3 or 4.

This is just a start (and not the only possibility to design such a system), but I hope you get the general idea. Try to narrow down the dependencies, which are caused not just by the parameter lists of functions, but also by global dependencies. Your design #3 is a first step into the right direction, but don't stop with that.

(Regarding performance: as we have written this in more than hundred other questions: don't think too much of it as long as you don't observe any real, measurable performance problems. It is unlikely any of the suggested alternatives will have a noteable impact on this).

14
  • Hello Doc, I appreciate your feedback on this. I'm going to go ahead with design 3 and see if I can improve it over time. I am curious though, how do you recommend I actually implement this design? I'm guessing we would be using inheritance making the character implement some KeyListener interface where we would define on_jump_occurred and the KeyProcessor would then emit events to KeyListeners using polymorphism, is that the idea? Commented Apr 10 at 18:25
  • @cuppajoeman: maybe. However, without knowing more about the actual requirements and overall organization of your program, I hesitate to give you more specific advice, it could be easily over- or underengineered. When you google for "publisher subscriber C++", you will certainly find some examples. But using modern C++, listeners might be implemented with less code based on functors/lambda expressions/closures, and no inheritance.
    – Doc Brown
    Commented Apr 10 at 21:13
  • Regarding performance is completely untrue, here's a harsh fact noone likes to talk about: overdesigning always leads to worse performance than dumb plain code that even my demented grandmother would understand, and even worse maintainability in the long run. First of all, do you really need an entire "System" for 1 Player? Okay maybe you're making couch multiplayer, is Keybinds players[8]; really that scary? I think the fact that they're asking this question in the first place is already showing that a simple problem is being dealt with complex solutions that actively waste their time.
    – shinyoi
    Commented Apr 11 at 4:56
  • @shinyoi: well, we don't know the size of the code base really is, and how it really looks like, but my answer takes the requirement as a fact this program has to deal with the mentioned systems (if the design of these systems is already overdesign is something only the OP could tell us). The number of players is definitely not an indicator how simple or complex the code base is, one player games can be as simple as Tetris or as complex as Tomb Raider. C++ allows a lot of zero-cost abstractions (that does not mean one should use the without need, of course).
    – Doc Brown
    Commented Apr 11 at 5:55
  • ...@shinyoi: but if you think you can write a better answer, what stops you from doing so?
    – Doc Brown
    Commented Apr 11 at 5:58
2

Design 3 is very popular, however there is an obvious missing design in your list. OOP.

With OOP you could inject the sound system into your Character object on construction and keep your method parameters clean.

Design 3, the mediator pattern is popular because most games are written for an engine framework. The framework provides a generic way of scripting objects in the game, but this also limits how the objects can interact. Using a mediator gets around that limitation. So you can keep your scripting simple.

A mediator doesn't give you the same compile time help with your method signatures as a direct function call would. Say you need to pass the position of the sound, and maybe some other parameters when the sound is played. this means you need to pass some event object to the mediator with that data in and you won't get compile time checks that you have sent a everything you need.

I think you need a mix of both. Events can help with all sorts of simple problems, "give a bonus when you have shot ten people!", "stop the game after three flag captures", more complex stuff that everything uses, like sounds, can benefit from an injected dependency with a compile time checked interface

2
  • OOP is not mutual exclusive to an event based design- quite the opposite, OOP provides means to implement an event system (+1 either for everything else in this post).
    – Doc Brown
    Commented Apr 11 at 19:09
  • in the context of the question I mean OOP (with DI) as a way of avoiding function parameters a "Design 4" if you will
    – Ewan
    Commented Apr 11 at 19:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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