Megacrush Unity Assets
API DocumentationOther Links
Metadata Driven Development
Metadata Driven Development
  • Overview
  • Sample Spreadsheet
  • BG Database Configuration
    • First Steps
      • Setting up BG Database Locally
      • Creating an Import Job for Google Sheets
    • Import & View Metadata
    • Configuring a new Table
  • Code Architecture
  • Video Walkthrough
  • GameData Scriptable Object
    • The New Item Wizard (Editor tool)
  • Get the Project
  • BG Database Documentation
Powered by GitBook
On this page
  • Folder Structure
  • Code Structure
  • Code Architecture
  • Step 1: The Data Model (C#)
  • Step 2: The Data Transformation Object (C#)
  • Step 3: The Service (C#)
  • Step 4: Parsing the BG Database Metadata (In the Service)
  • Step 5: GameData Scriptable Object
  • Step 6: The Metadata Service
  • Step 7 - The Metadata (Gameplay) Controller(s)
  • Step 8 - Views

Code Architecture

Walkthrough of the Metadata Sample project code architecture

PreviousConfiguring a new TableNextVideo Walkthrough

Last updated 6 months ago

Folder Structure

The folder structure that I use in every Unity project is fairly consistent.

Folder
Purpose

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

Code Structure

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:

Code Architecture

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.

Each column in the spreadsheet represents game tuning values that were provided by the Design team.

Step 1: The Data Model (C#)

The first thing we need to do is create a ‘Model’ to hold the metadata once we load it.

[System.Serializable]
public class WeaponModel
{
    /// <summary>
    /// these are all parsed from GoogleSheets
    /// </summary>
    public string name;
    public string displayName;
    public int maxAmmo;
    public int magSize;

    /// <summary>
    /// we parse these from the string on google sheets into the enum in the code
    /// </summary>
    public WeaponType type;
    public AmmoType ammoType;
    
    /// <summary>
    /// this is loaded from game data scriptable object after we load the metadata
    /// </summary>
    public WeaponConfig config;
} 

This script should be created in _Game/Code/Runtime/Models

Step 2: The Data Transformation Object (C#)

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.

using UnityEngine;

// DTO == Data Transformation Object ie convert Unity references into metadata
namespace PixelWizards.DTO
{
    [System.Serializable]
    public class WeaponConfig
    {
        public string name;         // the ID for this item
        public GameObject prefab;   // the game object that is associated with it
    }
}

This class should be created in _Game/Code/Runtime/DTO

Step 3: The Service (C#)

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.

using System.Collections.Generic;

namespace PixelWizards.Interfaces.Interfaces
{
    /// <summary>
    /// Generic Interface for Services that parse / load metadata from BG Database
    /// Ironically the MetadataService doesn't inherit from this
    /// </summary>
    /// <typeparam name="T">The Model class that this metadata service references</typeparam>
    public interface IMetadataService<T>
    {
        /// <summary>
        /// Init the service, parse the metadata
        /// </summary>
        public void Init();
        
        /// <summary>
        /// Get a single instance of this metadata type
        /// </summary>
        /// <param name="id">the 'name' / id of the item we want to retrieve</param>
        /// <returns>a single instance of 'T' (the metadata Model)</returns>
        public T Get(string id);
        
        /// <summary>
        /// Retrieve all of the metadata items for this type.
        /// </summary>
        /// <returns>a List of type T, where T is the metadata model.</returns>
        public List<T> GetAll();
    }
}

Services are (almost always) Persistent (exist across the life cycle of the application), where Controllers only exist in a single scene.

namespace PixelWizards.Services
{
    public class WeaponMetadataService : MonoBehaviour, IMetadataService<WeaponModel>
    {
        public static WeaponMetadataService Instance => ServiceLocator.Get<WeaponMetadataService>();
    }
}
private void RegisterServices()
{
    // our primary scriptable object service. 
    var metadataService = gameObject.GetOrAddComponent<MetadataService>();
    ServiceLocator.Add(metadataService);
    
    // each google sheet has their own service to parse the appropriate data
    var weaponService = gameObject.GetOrAddComponent<WeaponMetadataService>();
    ServiceLocator.Add(weaponService);
}

Once added to the ServiceLocator as shown above, any service can be referenced via their ‘Instance’ property, like so:

private void InitServices()
{
    // init the services
    MetadataService.Instance.Init();
    WeaponMetadataService.Instance.Init();
}

Step 4: Parsing the BG Database Metadata (In the Service)

In your Service’s Init() method, create a Dictionary to store the Metadata that you are loading, for example:

private Dictionary<string, WeaponModel> weapons = new();

To access the BG Database table, you reference them by the name of the table, and then can iterate through the rows like so:

// get a reference to the table
var meta = BGRepo.I[weaponTable];

// how many rows do we have in this table?
var count = meta.CountEntities;

for (var i = 0; i < count; i++)
{
    var attrRow = meta.GetEntity(i);
}

Once you have the reference to the Row (attrRow above), you can Get any field by their specified data type like so:

// read in the data from google sheets
var entry = new WeaponModel
{
    name = attrRow.Get<string>("name"),
    displayName = attrRow.Get<string>("displayName"),
    maxAmmo = attrRow.Get<int>("maxAmmo"),
    magSize = attrRow.Get<int>("magSize"),
};

Handling Enums

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:

// parse the Weapon Type from GoogleSheets
var type = attrRow.Get<string>("type");
if (Enum.TryParse(typeof(WeaponType), type, false, out var output))
{
    entry.type = (WeaponType)output;    
}
else
{
    Debug.Log("Invalid Weapon type? " + type);
}

Example BG Database Services

Implementations of a Google Sheets parser are available in the project under _Game/Code/Runtime/Service

Step 5: GameData Scriptable Object

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.

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).

Step 6: The Metadata Service

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:

/// <summary>
/// Get the weapon config for the given name
/// </summary>
/// <param name="weaponName">name of the weapon, should match weapon metadata table</param>
/// <returns>Weapon config for the given item, if not found, returns null</returns>
public WeaponConfig GetWeaponConfig(string weaponName)
{
    var weaponConfig = data.itemEntries.First(t => t.name == weaponName);
    if (weaponConfig == null)
    {
        Debug.Log("No Item: " + weaponName + " in Data Container?");
    }

    return weaponConfig;
}

Step 7 - The Metadata (Gameplay) Controller(s)

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:

Step 8 - Views

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.

In the we have a table for ‘Weapons’ that we want to configure.

Follow the ’ instructions to configure BG Database to import the metadata into Unity.

You can also see that there is an ‘WeaponConfig’ class that is referenced in the Model above. This is a 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.

Services are simply MonoBehaviours that are added dynamically to the at runtime. Check out any of the ‘Services’ under _Game/Code/Runtime/Service for example implementations.

Services are managed via the pattern, and are defined like so:

is another package that I have published on Github

In the ServiceManager, they are initialized like this (in ). If you create your own custom metadata service, register and initialize the Service like shown below:

Each of thecan be referenced in a similar manner (int, float, bool, string etc). There is additional documentation available in the .

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 ignoring case, like I am doing above.

For example:

See more about the

- References the Weapon metadata at runtime, gets a list of all of the weapons and spawns them into the scene

- once spawned, reads in their metadata and updates a View (UI) panel to show their metadata

Sample metadata Google Sheet
‘Configuring a Google Sheets Table
WeaponModel.cs
DTO
WeaponConfig.cs
IMetadataService.cs
ServiceManager
ServiceLocator
ServiceManager.cs
standard data types
BG Database documentation
TryParse
WeaponMetadataService.cs
Scriptable Object here
GameController.cs
WeaponWeaponController.cs
ServiceLocator
Code sub folder structure
The high level architecture of the Metadata Sample project.
Data structure for the Weapons table