Developped with Unity 2019.3.0f6
-
Require Odin Inspector (version used: 2.1.9): https://assetstore.unity.com/packages/tools/utilities/odin-inspector-and-serializer-89041
-
Require TextMesh Pro: Package Manager TextMesh Pro 2.0.0
Change the tag name
git subtree split --prefix=Assets/External/BulwarkStudios/GameSystems --branch upm
git tag 1.0.0 upm
git push origin upm --tags
- Install Odin Inspector
- Open the scene "Assets/Scenes/MainMenu.scene"
- Click on the play button
- Install Git for windows (https://git-scm.com/download/win)
- Close Unity if open
- Close Unity Hub if open
- Add in the Package Manager the git url: https://github.com/BulwarkStudios/Unity-GameSystems.git#upm
- To update the package re-enter the git url to proceed the update
Unity-GameSystems is a set of tools made after the release of Warhammer 40.000: Mechanicus (https://store.steampowered.com/app/673880/Warhammer_40000_Mechanicus/) to fix and clean design patterns used in the game.
In Mechanicus, when the game started to grow, it was hard to debug issues related to the state system. We have to log every things and we don't have a visual editor to see what's happening in real time. So I thought about a pattern with a more "data driven" approach to visualize the current state of the game. I choose to use what we can call a Singleton ScriptableObject pattern.
A game can be describe and split in many states like "Introduction", "MainMenu", "Settings", "Credits", "NewGame", "Overworld", "BuildingManagement", "UnitManagement", "Inventory", etc... In that pattern a game context is one of this state.
The GameContextSystem class will manage, update or change the current contexts. Using a ScriptableObject and the magic of Odin Inspector, you can view and debug contexts. The file is located in: Resources/GameSystems/Contexts/GameContextSystem.asset
A context has to extend GameContext, the system will find that class automatically and will create an associated ScriptableObject located in: Resources/GameSystems/Contexts/List/YourContextName.asset To have an access to a context just call: "YourContextName.Instance".
The system can have 4 contexts at the same time. However the system can manage "layers". When a layer is added, the old contexts are saved and only the contexts in the new layer are active. So when a layer is removed, the old contexts become active again.
Some API reference:
GameContextSystem.Load();
See the GameContextMainMenu class
GameContextSystem.SetContext(GameContextMainMenu.Instance, GameContextSystem.INDEX.MAIN);
GameContextSystem.SetContext(GameContextMainMenuCustom.Instance, GameContextSystem.INDEX.SUB1);
GameContextSystem.SetContext(GameContextNone.Instance, GameContextSystem.INDEX.MAIN);
GameContextSystem.AddLayer(GameContextSettings.Instance, GameContextSystem.INDEX.MAIN);
or
GameContextSystem.AddLayer();
GameContextSystem.SetContext(GameContextSettings.Instance, GameContextSystem.INDEX.MAIN);
GameContextSystem.RemoveLayer(GameContextSettings.Instance);
It began to be a nightmare to know which object listen to what. Events started to generate more and more spaghetti code despite a nice architecture. It was hard to locate issues. I use the same Singleton ScriptableObject pattern for the event to visualize their life in real time.
An event with no parameter has to extend GameEvent. The system can manage events up to 4 parameters. It has to extend GameEvent<T, P1, P2, P3, P4>. As the GameContext system, this system will find that class automatically and will create an associated ScriptableObject located in: Resources/GameSystems/Events/List/YourEventName.asset
An object can listen, unlisten or trigger an event using a static method directly. The ScriptableObject associated with the event will display all objects than are listenning the event.
Additionally the system can constrain the listening of an event with one or more GameContext or a specific method.
Some API reference:
GameEventSystem.Load();
GameEventNewGame.Listen(this, NewGame).AddGameContextConstraint(GameContextMainMenu.Instance);
// NewGame is executed when the event "GameEventNewGame" is triggered but only if the context "GameContextMainMenu" is active
GameEventNewGame.Unlisten(this, NewGame);
GameEventNewGame.Trigger();
In Mechanicus, we have a "Config" scene that contains a lot of data container objects. We can access to a data using the Service Locator pattern. The issue here was, if you or a member of team wanted to add a new data, the scene has to be open and saved. That may cause conflicts or merge issue with Git.
As the GameContext system et the GameEvent system, I use the same Singleton ScriptableObject pattern. A library has to extend GameLibrary, the system will find that class automatically and will create an associated ScriptableObject located in: Resources/GameSystems/Libraries/List/YourLibraryName.asset
The team members can now add data directly in the project window, without opening a scene and reducing the amount of conflicts in the source control system.
Some API reference:
GameLibrarySystem.Load();
GameLibraryUi.Instance.testButton
The Debug class in Unity is great, you can active or deactive all logs with a simple line of code:
Debug.unityLogger.logEnabled = false;
But there is no way (or I didn't find it) to prevent the compilation of Debug.Log calls. A simple Debug.Log("test") with a logEnabled = false will still allocate some memories.
So a solution to that is to wrap the Unity Debug log system with the [Conditional] attribute and compile the log calls only if the "Scripting Define Symbols" contains the "DEBUG_BUILD" constant.
Internally, we use Editor Console Pro (https://assetstore.unity.com/packages/tools/utilities/editor-console-pro-11889) and I wanted to take advantage of their custom filter feature. In this log system, you can tag (or not) every logs you want. This will add the tag in the console before the object you wanted to log.
Some API reference:
GameLogSystem.Initialize(new LogConfigTest());
Log.Info(GameLibraryUi.Instance.testButton, LogConfigTest.TAG.TASK);
Log.Warning(GameLibraryUi.Instance.testButton, LogConfigTest.TAG.TASK);
Log.Exception(GameLibraryUi.Instance.testButton, LogConfigTest.TAG.TASK);
Log.Error(GameLibraryUi.Instance.testButton, LogConfigTest.TAG.TASK);
Add tags to the enum "TAG" and active or deactive those tags in the "SetupTags" method.
We had a lot of trouble when designers or programmers wanted to setup the different states of a Button. And when we started to implement keyboard shortcut and gamepad control it was a nightmare.
This is not really a system on it's own, but more a redesign of the "Button" class from Unity. Add the "UiButton" script to any game object to start making a button. By clicking on the different state in the inspector, the scene will preview and update the button directly in the editor. Add any classes which extend "UiButtonEffect" to change the looks of the button for every possible states. Add any classes which extend "UiButtonEvent" to add behavior when specific event are triggered. Add any classes which extend "UiButtonConstraint" to control when the button is active or not.
See the MainMenu scene.
https://docs.google.com/document/d/1Vr-OTl4bF2BxqRWNAUzqmarMXYzwkCxpPjY25PfPKHo/edit?usp=sharing