Code Architecture
Walkthrough of the Metadata Sample project code architecture
Last updated
Walkthrough of the Metadata Sample project code architecture
Last updated
The folder structure that I use in every Unity project is fairly consistent.
Assets
_Game
Everything game specific lives here. The underscore (_) makes Unity sort this folder to the top of the Project window.
Code
All of the custom code lives here
Content
All content (raw assets) lives here
GameData
Game prefabs / config files / scriptable objects
Scenes
Game Scenes
Inside the Code folder there is also a fairly consistent set of sub folders that are used in my projects:
As you can tell I use a hybrid MVC structure for most of my code. It's not very intuitive in Unity to work like this, but separating the Logic (controllers) from the visuals (Views) helps keep the code clean and well separated. Views can include UI screens, but also Weapon visuals, Character Animations and FX etc as well.
Let's look at how we can incorporate Metadata Driven Development into our workflows in Unity:
Each Sheet in Google Sheets represents a unique Data type (class / struct).
This document will walk through the setup of a data type for a unique Google Sheet that we want to parse into Unity.
The image above provides a high level overview of the code architecture. Note that the 'service' layer is persistent across the entire application (and happens in the Startup.unity scene). The top layer happens in the Gameplay.unity scene, once the Initialization of all of the metadata has happened.
Let's walk through the code.
In the Sample metadata Google Sheet we have a table for ‘Weapons’ that we want to configure.
Each column in the spreadsheet represents game tuning values that were provided by the Design team.
Follow the ‘Configuring a Google Sheets Table’ instructions to configure BG Database to import the metadata into Unity.
The first thing we need to do is create a ‘Model’ to hold the metadata once we load it.
This script should be created in _Game/Code/Runtime/Models
You can also see that there is an ‘WeaponConfig’ class that is referenced in the Model above. This is a DTO transformation class that converts the metadata into any Unity specific representations. This is the ONLY piece of the entire architecture that references any Unity assets.
This class represents any Unity specific objects that we might need to reference. For example a prefab, materials, or other Unity specific assets that might need to be referenced.
In the case of the Weapon, we currently only need to reference the Prefab that represents the enemy, like so. If we require any additional 'Unity' references here (for example a list of Materials for customization, or a sprite for an inventory or HUD thumbnail), we can expand the list here.
This class should be created in _Game/Code/Runtime/DTO
Each table is parsed and managed as a separate Service. Services inherit from the IMetadataService<T> interface, where T is the ‘Model’ that you created in step 1.
Services are simply MonoBehaviours that are added dynamically to the ServiceManager at runtime. Check out any of the ‘Services’ under _Game/Code/Runtime/Service for example implementations.
Services are (almost always) Persistent (exist across the life cycle of the application), where Controllers only exist in a single scene.
Services are managed via the ServiceLocator pattern, and are defined like so:
ServiceLocator is another package that I have published on Github
In the ServiceManager, they are initialized like this (in ServiceManager.cs). If you create your own custom metadata service, register and initialize the Service like shown below:
Once added to the ServiceLocator as shown above, any service can be referenced via their ‘Instance’ property, like so:
In your Service’s Init() method, create a Dictionary to store the Metadata that you are loading, for example:
To access the BG Database table, you reference them by the name of the table, and then can iterate through the rows like so:
Once you have the reference to the Row (attrRow
above), you can Get any field by their specified data type like so:
Each of the standard data types can be referenced in a similar manner (int, float, bool, string etc). There is additional documentation available in the BG Database documentation.
You 'Can' parse Enums directly with BG Database, but I don't typically do this myself.
What I do is setup the Enums as a Dropdown in Google Sheets and then parse them as a String from BG Database and Parse them into their corresponding enum type like so:
The only catch with parsing the Enums directly like I am doing is that you either need to be very careful with Case SenSitiVitY, OR do the TryParse ignoring case, like I am doing above.
Implementations of a Google Sheets parser are available in the project under _Game/Code/Runtime/Service
For example: WeaponMetadataService.cs
Each Entry DTO that you create (in Step 2) is referenced in the GameData Scriptable Object. This is the central ‘Object Registry’ that contains references to all of the objects referenced in the Metadata. The Services (from #4 above) reference the corresponding DTO entry while parsing the metadata.
See more about the Scriptable Object here
As you can see, all of the Weapons are referenced in the Scriptable Object. This allows us to iterate over the items in the metadata and in turn spawn / reference the items at runtime. The ‘Name’ field for all of the Entry DTO’s reference the Metadata ID (in the Spreadsheet).
Metadata Service is a wrapper for the GameData scriptable object (and referenced in ServiceManager.cs as well. You can access any of the DTO entries via their Name ID. If you create your own entry, add a Getter to the Metadata Service, like so:
Now that we have all of the metadata configured and read, accessing the metadata at runtime is very straightforward. There are a number of example Components in the sample project that load metadata of their corresponding data types. You can implement your own wrapper like this OR directly access the service from your own code in a similar manner.
These classes are the layer that touches the gameplay. For example when a scene loads, the enemies in the map can read their config data from the Metadata system, weapons can get configured, and in general any element in the game that might need to be balanced, tuned or otherwise updated by a game designer can be cleanly defined into Metadata tables.
Example Controllers:
GameController.cs - References the Weapon metadata at runtime, gets a list of all of the weapons and spawns them into the scene
WeaponWeaponController.cs - once spawned, reads in their metadata and updates a View (UI) panel to show their metadata
There is also an example 'View' in the project that demonstrates how I typically communicate between the Controller layer and the View layer. Controllers setup and initialize the View, and any further updating between the layers happens via events.