Appearance
Working with Game Config Data
Metaplay supports a data-driven way to specify the game's design, economy, and localization data using external tools like Google Sheets and Excel.
Appearance
Metaplay supports a data-driven way to specify the game's design, economy, and localization data using external tools like Google Sheets and Excel.
The game config system's main purpose is to extract the game's design, economy, and localization data so that it can be edited independently from the game's code. This data-driven approach allows faster iteration times as the client doesn't need to be rebuilt each time the design data is changed.
Another benefit of game configs is that less technical people can author the data without needing to touch the game code. It also enables using powerful tools like Google Sheets or Excel for specifying the contents. Other data sources can also be used, for example, plain CSV or JSON files or reading data from Unity Prefabs.
Game Config Libraries are collections of Game Config Items that usually correspond to a single .csv file or a sheet in Google Sheets. Each Library allows resolving references to the contained Items using their unique identifiers.
Metaplay provides a fully programmable build step for fetching, transforming, and parsing the sources to the Game Config Archives used by the runtime game. Configuring the build is described in Custom Game Config Build Pipelines.
Metaplay also supports uploading, reviewing, and publishing the built Archives to the server, where the new version is taken into use and distributed to all the clients via CDN.
In Metaplay’s game config system, the config structure is declared as C# classes in your shared game logic code. It’s also important to note that configs use the MetaSerialization system, and it’s, therefore, necessary to tag entries as MetaSerializable.
Game Config Libraries are introduced in either the SharedGameConfig class or the ServerGameConfig class based on whether the library should be accessible from client code or only from the server.
Each Item must have an unique identifier called ConfigKey. It's recommended to use a separate StringId for each Library type. In the case of complex identifiers or special use-cases, see Game Config Item Identifiers for further details. A special case of Game Config Libraries are Key-Value Game Config Data which are libraries with only a single Item and hence require no ConfigKey to be defined.
This example explains how to add data-driven configuration to a game with multiple kinds of troops (e.g., soldiers and ninjas) with various properties (damage, hit points, speed).
First, let's declare TroopKind as a StringId, which allows giving string-like identifiers for the different kinds of troops (e.g., Ninja or Soldier):
// Unique string-like identifier type for TroopInfos (see below)
[MetaSerializable]
public class TroopKind : StringId<TroopKind> { }Next, let's declare the TroopInfo class, which contains configuration data for each of the troop kinds:
// Configuration data for an in-game troop, identified by a TroopKind
[MetaSerializable]
public class TroopInfo : IGameConfigData<TroopKind>
{
[MetaMember(1)] public TroopKind Kind { get; private set; } // Unique kind of troop (used to identify this item)
[MetaMember(2)] public int Damage { get; private set; } // Damage dealt
[MetaMember(3)] public int HitPoints { get; private set; } // Hit points
[MetaMember(4)] public F32 Speed { get; private set; } // Movement speed (a fixed-point value)
// Extract the key that uniquely identifies the troop, `Kind` fully identifies this item
public TroopKind ConfigKey => Kind;
}The contents of the Troops.csv file (or the equivalent Google Sheet) would look like this:
| Kind #key | Damage | HitPoints | Speed |
|---|---|---|---|
| Soldier | 10 | 100 | 1.5 |
| Ninja | 50 | 150 | 5.25 |
Automatic Column Mapping
The column names are automatically mapped to the member names in the TroopInfo class when importing the sheet or file. No custom parsing code is needed when adding new Game Config types to a game.
The columns corresponding to the ConfigKey must be marked with a #key tag.
The TroopInfo data should be visible to the client, so we add an entry to the SharedGameConfig, which is the game-specific registry where all client-visible game config sheets/files are imported into. Here's a simplified example:
public class SharedGameConfig : SharedGameConfigBase
{
// Game-specific Libraries (one for each input file/sheet)
// The GameConfigEntry attribute is used to name this entry for purposes of
// serialization; for simplicity, we use the member name here.
[GameConfigEntry("Troops")]
public GameConfigLibrary<TroopKind, TroopInfo> Troops { get; private set; }
...
}Game Config Archives and the built-in spreadsheet Config Parsing system supports a wide range of types. This section describes the types and their syntax rules.
Built-in types are parsed as elemental types and each built-in type field maps to a single column in a spreadsheet. The following types are supported:
| Type | Example(s) | Notes |
|---|---|---|
int | 5, -123 | Other integer types can also be used |
string | Some String | Quoted strings are supported for escaping characters |
bool | true, True, FALSE | Case-insensitive |
Enum | SomeEnumValue | Must be valid enum values |
StringId | MyStr, com.example.iap_id | Maximum length is 1024 |
MetaRef<T> | MyStr, com.example.iap_id | Parsed as the key type of the config item type T |
F32 | -0.123, 32767.0 | Signed 16.16 fixed point, with deterministic parsing |
F64 | 1234567890.00000001 | Signed 32.32 fixed point, with deterministic parsing |
float | -0.111, 123456.123 | 32-bit floating point, WARNING: not deterministic! |
double | -123.321 | 64-bit floating point, WARNING: not deterministic! |
F32Vec2 and F64Vec2 | (1.23, 4.56) | |
F32Vec3 and F64Vec3 | (1.23, 4.56, 7.89) | |
IntVector2 | (123, 456) | |
MetaTime | 2022-12-25 12:34:56.789 | year-month-day hour:minute:second. Either integer or fractional seconds. |
MetaDuration | 1d 12h 30m 15s | List of number-unit pairs (d=days, h=hours, m=minutes, s=seconds) |
MetaCalendarDate | 2022-12-25 | Used for Schedule.Start.Date in activables |
MetaCalendarTime | 12:34:56 | Used for Schedule.Start.Time in activables |
MetaCalendarPeriod | 1d 12h 30m 15s | Used for Schedule.Duration and other periods in activables' schedules. Same syntax as MetaDuration. |
MetaActivableLifetimeSpec | Fixed 12h ScheduleBased Forever | Used for Lifetime in activables |
MetaActivableCooldownSpec | Fixed 12h ScheduleBased | Used for Cooldown in activables |
Nullable<T> | Either empty (for null) or same syntax as contained type |
Nested members can be specified with Field.Member syntax.
// Helper for specifying parameters outside the primary `TroopInfo` class.
// Can be reused in many GameConfigLibraries.
[MetaSerializable]
public class TroopParams
{
[MetaMember(1)] public int HitPoints { get; private set; }
[MetaMember(2)] public int Damage { get; private set; }
}
[MetaSerializable]
public class TroopInfo : IGameConfigData<TroopKind>
{
[MetaMember(1)] public TroopKind Kind { get; private set; } // Unique kind of troop
[MetaMember(2)] public TroopParams Params { get; private set; } // Example of nested members
// Extract the key that uniquely identifies the troop, `Kind` fully identifies this item
public TroopKind ConfigKey => Kind;
}The nested TroopInfo.Params members' values can be assigned into using the following spreadsheet syntax:
| Kind #key | Params.HitPoints | Params.Damage |
|---|---|---|
| Soldier | 100 | 10 |
| Ninja | 150 | 15 |
The game config pipeline supports parsing collections as well.
Prefer the read-only IReadOnlyList<T> over arrays or List<T> as this protects against accidentally modifying the data at runtime. For performance-critical code, you can use arrays. Just note that they don't protect against accidental modifications.
[MetaSerializable]
public class CollectionsExample : IGameConfigData<string>
{
[MetaMember(1)] public string Id { get; private set; }
// Simple list, backing storage is int[].
[MetaMember(2)] public IReadOnlyList<int> Ints { get; private set; }
// Collections of complex items can also be used.
[MetaMember(3)] public IReadOnlyList<TroopParams> Params { get; private set; }
// For performance-critical code, you can use arrays, though note that they can be accidentally modified!
[MetaMember(4)] public int[] Integers { get; private set; }
// Extract the key that uniquely identifies this item.
public string ConfigKey => Id;
}Collection data for arrays and lists can be specified with explicitly indexed horizontal syntax:
| Id #key | Ints[0] | Ints[1] | Params[0].HitPoints | Params[1].HitPoints | ... |
|---|---|---|---|---|---|
| Example | 10 | 20 | 100 | 150 |
Or with vertical collection syntax using empty brackets:
| Id #key | Ints[] | Params[].HitPoints | Params[].Damage | ... |
|---|---|---|---|---|
| Example | 10 | 100 | 15 | |
| 20 | 150 | 20 |
At most one instance of empty brackets can appear per column. Other than that, compound types can be nested. By combining vertical and horizontal collections, you can express two-dimensional collections:
| Id #key | Params[].Items[0].Type | Params[].Items[0].Cost | Params[].Items[1].Type | Params[].Items[1].Cost | ... |
|---|---|---|---|---|---|
| Example | BronzeSword | 100 | BronzeShield | 200 | |
| IronSword | 1000 | IronShield | 2000 |
Currently, collections cannot immediately contain other collections, but there must be a named member between them: MyList[][0] is not supported, but MyList[].OtherList[0] is.
A Game Config Item may also contain references to Game Config Items in the same or other Game Config Libraries. For more details, see Game Config Item References documentation.
In addition to the Game Config Data, the sheet data may also contain special syntax for various purposes. This section describes the supported special syntax rules and their effect.
Variant selector is /Variant column, and it is used annotate that the data definitions on that row only apply for the players in the certain variant of the certain experiment. The syntax rules for the are described in Create Your Experiment in the Game Config. See Introduction to Experiments for an introduction to experiments and A/B testing.
Variant columns are columns starting with /: followed by an Experiment Id and a Variant Id, for example /:EarlyGameFunner:Fast. Variant columns works similarly to variant Selector, except they mark the data definitions on that column only apply for the particular experiment variant. The syntax rules for the are described in Using Variant Columns.
Aliases column is a column with the name /Aliases and it is used to allow Items to have multiple ConfigKeys, also known as Game Config Item Identifiers. For more information, see Rename in the Spreadsheet how and when to use Aliases Column.
You can use constructors to set default values, generate data derived from your configs, and use non-public setters on your properties. By annotating a constructor with the MetaGameConfigBuildConstructor attribute, it will be used to create the type's instances instead of the parameterless constructor. The constructor's parameters determine the types and names of your source data. Optional parameters are allowed, and the default value is respected.
GameConfigKeyValues currently do not support constructor-based building.Troop stats can be represented in the following format, where speed is an optional field with a default value of 1.
| Kind #key | Damage | HitPoints | Speed |
|---|---|---|---|
| Squire | 5 | 100 | |
| Soldier | 10 | 100 | |
| Ninja | 50 | 150 | 5 |
To represent this as a C# class that is deserialized using a constructor, we can do the following:
[MetaSerializable]
public class TroopParams : IGameConfigData<TroopKind>
{
[MetaMember(1)] public TroopKind Kind { get; private set; }
[MetaMember(2)] public int HitPoints { get; private set; }
[MetaMember(3)] public int Damage { get; private set; }
[MetaMember(4)] public int Speed { get; private set; }
[MetaGameConfigBuildConstructor]
public TroopParams(TroopKind kind, int hitPoints, int damage, int speed = 1)
{
Kind = kind;
HitPoints = hitPoints;
Damage = damage;
Speed = speed;
}
public TroopParams() { }
}