I’m currently finding myself in a problem in terms of designing proper code architecture. I have a main GameEngine class which contains the list of players, GameMode enum, GameState enum, functions that handle the logic for each game state and it also subscribes to events related to new packets arriving over the network. The problem is that the class becomes too big.
I thought about splitting the logic of different game states of each game mode to different classes, but then I arrive to another problem. What if I’ll work on a game mode that is going to be very similar to this basic one I’m currently working on (say a game of Deathmatch vs Capture-the-flag…both the modes will use mechanics such as killing enemies, respawning, picking up items, what have you). So I was thinking that I could put back those common mechanics to GameEngine. But then I’m returning to the same initial problem - GameEngine being too big. What would you propose?
Personally, and bear in mind that you can adopt any number of styles according to personal taste, I’d recommend splitting off functionality for your game modes and logic handling into even smaller chunks and classes. Rather than having a class that handles all of the asset management, networking, initialization, etc. try making a class for each one. And instead of a class for each game mode break down what happens in each mode into smaller classes so they can be re-used across any number of modes.
The real problem then becomes what made you gravitate towards a GameEngine class in the first place: inter-class communication. One of the best ways I’ve found of doing this is through an event manager. Either a custom implementation or by hooking into Unity’s event system with your own custom events. The advantages of such a system are that you can have classes who subscribe to a particular signal but don’t need to know anything about how it goes about getting sent. Likewise, the class sending the signal doesn’t need to be tightly bound to those who are receiving it, it just sends out the signal “blindly” and everything that’s subscribed then acts on it. This de-coupling of classes lets you separate your logic into smaller chunks because you don’t need to keep track of tightly bound connections in a monster class, you can edit and change your classes without affecting the logic in others, and it clearly labels and forces you to recognize what parts of your code are significant and… eventful.
This is one way of doing it and I would also recommend using inheritance and other methods in combination as well. But one thing I would strongly recommend is this: scrap the GameEngine class altogether, it will challenge you to refactor in a way that will help you in the long run.
Have a look at the Game Programming Patterns site. The whole lot is a definite read, entertaining and accessible. It doesn’t just preach, it comes from the perspective of real-world problems and takes first-draft code and refines in ways that are simple & make sense, not just academic for the sake of it.
I’ve tried several times to get into design patterns, and that site helped more than anything, and got me starting to use patterns more and knowing where they might be appropriate. It’s not a set of instructions, but a set of techniques to try out and ease into when you’re comfortable.
If you didn’t understand that, just look into Component, Update & Game Loop sections specifically at first. Those techniques hugely help organising sprawling code.
You are mentioning GameMode enum and GameState enum. So I am assuming that your game controller is build upon a Finitite State Machine (FSM). The source of your problems is likely to be intrinsic to the usage of FSM.
FSM is appropriate when your problem involves few states and few transitions. But it becomes a hell to manage when your problem grows in complexity. Since any modification requires to modify the transitions. Furthermore, FSM is hardly resuable: it’s hard to isolate an interesting part of an FSM and reuse it for building another FSM (again because of the high inter-dependency of the states due to the explicit transitions).
An alternative to FSM, that does not have these disadvantages, is Behaviour Tree (BT). BT stays manageable as your logic grows in complexity (because of its hierarchical nature) and you can easily reuse whole or part of the tree to define other BTs. This is possible because the transitions from one node to another is not explicitly defined, rather they depends of the position of the nodes in the tree. That’s make it easy to insert/move/delete a node, therefore it makes modifications extremely easy.
If you are looking for a Behaviour Tree framework, I suggest Panda BT (www.pandabehaviour.com). It is a script-based BT framework that I am maintaining. If you have questions about using this tool, your are welcome on this thread.