3
\$\begingroup\$

We currently have the problem that our main class GameController is being pulled into every backend file in our game. We're wondering what are common solutions to this problem.

Here's a bit more about the game architecture. The game is a board game, so since ~90-95% of the time there isn't anything happening, the game is set up more like a rest API. It waits for a user prompt, when received, the msg is distributed to the respective components of the game and the proper logic is executed. No large update loops, just executes logic when prompted.

The problem is that as this msg cascades through the system, GameController acts more like a relay point between the systems. It's how the nodes communicate to each other so that all game components are updated properly. The problem is that it's created this system where all new/old classes contain a pointer to parentGame, so GameController is everywhere.

Are there any simple architectural solutions to avoid having every class contain a pointer to parentGame? Is this necessarily a bad thing?

Some example code:

class GameController {
    bank: Bank
    action: Action
    ...
}
class Bank {
    parentGame: GameController

    constructor(game: GameController) {
        this.parentGame = game
    }
}
class Action {
    parentGame: GameController

    constructor(game: GameController) {
        this.parentGame = game
    }
}
```
\$\endgroup\$
7
  • \$\begingroup\$ You mention twice "The problem is that". You simply stated facts of what happens, but not in what those facts are actually issues. Why is this pattern not working for you? \$\endgroup\$
    – Vaillancourt
    Oct 14 '20 at 1:16
  • 2
    \$\begingroup\$ Board games are a bit tricky, honestly you will likely always need access to some kind of shared statemachine for each game object so you can communicate changes to score, board position, draw cards, etc. Given how commonly you use this object (and I'm guessing by the name of it there is only one instance per game) you may want to modify the GameController to use a Singleton pattern so you don't have to keep passing it to game objects. If you really want to get rid of it or use it less you probably need to examine your game logic and break it into smaller parts. \$\endgroup\$ Oct 14 '20 at 1:43
  • \$\begingroup\$ @BenjaminDangerJohnson This comment looks like a good candidate for an answer :) \$\endgroup\$
    – Vaillancourt
    Oct 14 '20 at 1:46
  • 1
    \$\begingroup\$ @Vaillancourt Maybe, but I'm hoping more info on "Why is this pattern not working for you?" is provided before making any solid recommendations. There honestly could be much better solutions to either sticking with the current architecture or switching to Singletons. \$\endgroup\$ Oct 14 '20 at 1:52
  • \$\begingroup\$ @BenjaminDangerJohnson I'll admit that it is also why I have not posted an answer either :) \$\endgroup\$
    – Vaillancourt
    Oct 14 '20 at 2:05
1
\$\begingroup\$

I've encountered systems like this; hell, I've even written some when I was in a hurry during prototyping. It is not specifically wrong, it is just one way in which systems can evolve. Reasons systems evolve this way include:

  • The root controller class / instance needs to be privy / proxy to much or all of what other actors in the system, are doing (perhaps even before it is done); maybe it needs to take some secondary action.
  • The root controller is the only one who is authorised to use this message to issue imperatives to other sub-controllers, i.e. the usual Hierarchy of Control we see in most programs. So by this manner of thinking, messaging in this system has to go via the root controller.
  • The lack of time, willingness, or knowledge as to how to improve the system.

...the reality is the first two are just ways of thinking about the system. This kind of "deep propagation" up and down through a control tree gets increasingly tedious to manage explicitly, as the tree deepens... and deepens... as it tends to do in an app as complex as the typical game.

Is this necessarily a bad thing?

As someone else said, Yes, usually. Under most conditions, tight coupling is to be avoided. The reason is that the cost of modifying the code becomes large, quickly.

Are there any simple architectural solutions to avoid having every class contain a pointer to parentGame?

One way is to pass methods from super-controllers downward, let sub-controllers call them. This breaks encapsulation; but it is a relatively quick solution. Ultimately though, if these methods are modifying state on the parent, it still acts as a sort of messaging hub, just as you describe.

I would not recommend this except as an interrim measure on the road to a better architecture.

Another way is the observer pattern, A.K.A. pub(lish)-sub(scribe). This prevents your having a direct link to an instance of a specific class; rather, you have indirect, generic links to instances of a specific interface from which your classes inherit. As subscriber, you can now listen to the messages which the publishers broadcast, or broadcast your own message to those subscribed to you. A given class can even be both a publisher and a subscriber, provided it inherits or implements both sets of functionality!

Anything that needs to know when something else changes, but doesn't want to create a specific, typed link to the something else in question, should use this pattern.

One of the reasons it works so well is the observer has a list of these abstract / interface-derived objects. If there are no objects, the iterator does nothing, and there is no null reference error (which you will get plenty of, in your current project). If there is one object, or many objects which the observer is listening to, it treats them all similarly. Lists / Arrays are powerful this way, and it is one reason language design is headed in this direction; avoiding dreaded NREs / NPEs completely.

The essence is that you can have multiple observer arrays and multiple response styles (callbacks) for those different sorts of objects observed by each array. Different objects may subscribe, for example, to messages from both children and global services, with each type of child, and each type of service, being intercepted and handled in a different manner.

\$\endgroup\$
3
1
\$\begingroup\$

I think the most important question is: why are all your modules/systems accessing GameController?

Are they interacting with GameController so that they can access data or functionality provided by this class? If that's the case, your GameController is probably accumulating too much responsibility and you might benefit from applying the single responsibility principle (https://en.wikipedia.org/wiki/Single-responsibility_principle). You might want to refactor GameController into multiple classes so that concentrated functionality/data is naturally diluted away and your systems start depending less on GameController and more on other classes. However, you mentioned GameController works as a relay to your systems, so I believe that you already have an architecture that is not so monolithic.

Are your systems interacting with the GameController so that they can access other systems? If that's the case, your GameController is probably behaving like a module/system container, exposing all modules - high or low level - to any module, and so you might benefit from applying the dependency inversion principle (https://en.wikipedia.org/wiki/Dependency_inversion_principle). You might want to inject the system's dependencies (via ctor or setters) from GameController instead of acquiring them from each system via GameController's getters. That way they won't have to have any knowledge about the GameController and the other modules/systems that they don't need to know about. You could even use a DI container implementation, but I fear that might be too much for your current needs.

In the end, it can also be a bit of both. You might have to break down your solution into smaller modules/systems and you might want to decouple them from GameController.

Is [accessing the GameController from all other classes] necessarily a bad thing?

I'd say yes. That couples most of your classes to the GameController class, meaning that if you change GameController's public interface, you'd probably need to recompile those classes. And if GameController is the corner-stone of your solution, how frequently would that happen?

How you're going to tackle that, however, depends on how you got to this situation which is what I tried to get to with the questions above.

\$\endgroup\$

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.