This article is the first in a series of coding tutorials for Unreal Engine 4. The reader of this article is assumed to know basic C++ and the UE4 API.
In the past year of UE4 development, I had problems figuring out how to structure my game code: where to put game logic code to keep it readable/maintainable, testable and reusable. This blog post summarizes what I and my fellow developers have learnt as I finally feel confident with my understanding.
GameMode
The documentation describes three main tasks of GameMode: player management, level management and game rules like winning conditions. GameModes come with two restrictions: it is not possible to swap out the GameMode out at runtime without opening a different level and the GameMode only exists on the server (only really relevant for online games). Importantly, the latter implies that there should be no data in the GameMode that needs to be transferred to the client. Those should instead be stored in the GameState.
Server? Client? I’m making a single player game. - Our games are all, too! For local games, client and server will be one and you will therefore have a GameMode.
In our games, we have actually never made use of the separation into GameState and GameMode up till now. Instead, everything went directly into the GameMode, as we never needed to run server and client separately. I see this as a mistake which originated from my misunderstanding of GameMode and GameState. Instead, I advise you to write your code “as if” you had to split them one day.
“But, YAGNI!”, you may cry. Though, as the title may suggest, my main motivation for this is not being able to split the code into client and server.
No GameMode
One of the most common code patterns you would find in my code, was the following:
// in SomeActor.cpp auto gm = Cast<AMyGameMode>(GetWorld()->GetAuthGameMode()); check(gm); gm->NotifyOfSomeEvent();
The check is guaranteed to succeed, as we never run the game with separated client and server. Instead I believe this code is bad because of the `gm->NotifyOfSomeEvent()`, which makes the SomeActor class depend on the GameMode class. We will need to `#include “MyGameMode.h”` just because of this function call. Whenever MyGameMode.h changes, the SomeActor.cpp file will be recompiled.
Now suppose you do this in every actor class. In Wastepaperbin VR for instance: Paper, Printer, Wastebin, Lever, the PlayerPawn and more. Every time you change MyGameMode.h, all these classes are recompiled and I found that that happens rather frequently. Especially when you do not split off your GameState properly as you may add new properties there often.
While one solution is to just split off everything into the GameState to avoid this, there are further reasons to focus on following the “as if server and client are split” rule.
Game Code is Throwaway-Code
Apart from when you litterally write “throw-away-code” for a game called “Wastepaperbin”, yes, you can probably throw it away if you cannot reuse it. Jokes aside, I recommend writing your code in a reusable way, as this can save you time in the future! You can write your own “technical capital”.
For our VR games, we write user interaction components, a Lever actor for example. This is code that might as well be reused by another game. Following the pattern above, there was code like this:
// in ALever.cpp (not actual code, condensed to "the gist") void ALever::leverFlipped(bool up) { auto gm = Cast<AMyGameMode>(GetWorld()->GetAuthGameMode()); check(gm); if(up) { gm->SetChallengeMode(); } else { gm->SetLeaderboardMode(); } }
This code again, would not work with client-server separation, of course. The real culprit here is, though, that the code is so tightly “coupled” to the GameMode, that it is not possible to use this code in another game without changing it. Optimally, we would like to copy it into another game or even use it in a plugin shared by both games.
This can be fixed by adding a delegate, which is bound from the outside (e.g. in the GameMode if need be or, better, in a LevelScriptActor). That may then look like this:
/** * @brief Delegate executed when a lever flips into a new state. * @param IsUp `true` if the lever has been flipped to up state, `false` otherwise. */ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLeverStateChanged, bool, IsUp);
The above code snippet declares your new `FLeverStateChanged` delegate, which can then be added as a property in the Lever actor:
UPROPERTY(BlueprintAssignable) FLeverStateChanged OnLeverStateChanged;
Other actors can then bind to the delegate like this:
ALever* lever = /* ... */; lever->OnLeverStateChanged.AddDynamic(this, &ASomeActor::onLeverStateChanged);
Where `onLeverStateChanged` will look something like this:
/* Header: */ UFUNCTION() void onLeverStateChanged(bool IsUp); /* Source: */ void ASomeActor::onLeverStateChanged(bool IsUp) { /* Do something according to IsUp */ }
To call the delegate in the lever and consequently executing all bound functions, you would simply call broadcast:
void ALever::SomethingHappenedThatFlipsUpTheLever() { OnLeverStateChanged.Broadcast(true); }
Instead, yes, you could check `if (gm)` and then only do this on the server. Using delegate will additionally make the code reusable and allow you to remove that MyGameMode.h header, which can avoid recompilations of the source.
Hardened Mode
Using the aforementioned delegates to decouple your code has one huge advantage: it makes automated testing easier.
I feel like unit testing is one of the most overlooked tasks during game development. With Cook for the Giant (VR), not writing unit tests from the start ended up being my biggest regret:
For every change I made on the GameMode, I would need to start the game, load a level, play the level to the end (which is time-constrained, as the time to read in a given level is fixed) and then replay the level to check what happens when you fail it, replay the level to ace it, deleting the save file beforehand to make sure the gold medal spawn animation is run correctly and so many more combinations of interacting events. Fully testing the game with every edge case that could potentially cause the game to crash would easily take half an hour or more.
Go figure, I didn’t. As a result, you can check older versions of the game to find game-crashing bugs, which have been added compared to the version before.
The game logic is complex enough that you can never be sure you did not break anything while fixing something else. This is a big reason for why I love unit testing, you save time ensuring parts of your game still work as they did before.
Back to delegates and decoupling: assume you want to test the above
lever. To test whether something caused the lever to flip in the
original code, you need a valid GameMode. And how would you assure the
lever has been flipped? In this case by checking which mode is set in
the GameMode. Setting the mode will cause the GameMode to hide some
actors, unhiding others… oh, you need all of those, too! Just to
test a lever.
The test may fail, because your test level is missing an Actor that
the GameMode needs when changing the GameMode, which is not at all
what you wanted to test in the first place!
After refactoring the code to use delegates, you merely need an empty level with your lever actor in it, bind the delegate from the unit test and then execute the code to cause the lever to flip. Then check if the delegate has been called. You are testing exactly what you need, not more, not less, allowing you to focus on one thing only.
More Modes
In Wastepaperbin VR we came across the problem of wanting to support different sets of game rules, an arcade-like mode and a campaign-like mode. If you put the game logic of both into one GameMode, you will end up with code clutter like this in many functions:
if(Mode == KingOfTheHill) { // do one thing } else { // do another thing }
If instead you split into two GameModes, you will end up having to `OpenLevel`, which results in annoying loading times if you are reusing the same levels. We currently solved this by introducing always loaded streaming levels whose ALevelScriptActor subclasses contain the game logic. This again goes against the “as if client and server are separated” rule, which makes me believe there is a better way. Potentially moving the game rules into an independent invisible actor which is spawned from the GameMode is a better solution.
Conclusion
This article is meant to make you think: Whenever you need to use the GameMode class from within another actor, maybe you are doing something the wrong way around. Instead, think of the GameMode as being a chess player, that moves the pieces around and reacts to other pieces being moved rather than the pieces themselves knowing the game and acting accordingly. It would be a really stressful game if you had all the pieces screaming at you all the time.
Takeaways
- GameMode contains your games rules
- Decoupling means making code independent of other code
- Decoupling your code can help with compile times
- Decoupled code can be tested more easily
- Decoupled code can be reused more easily
- Avoid `GetWorld()->GetAuthGameMode()` to force yourself to decouple your Actors from GameMode
- Delegates can be used to make your code more reusable
- Write Tests to save time ensuring your code keeps doing what it is meant to do after changes
Discuss
Join the discussion over on