Appearance
Appearance
The primary purpose of the game configs system is to extract the game's design, economy, and localization data so that it can be edited separately from the game's code, making it faster to work with and be more accessible to developers with less technical backgrounds. This article explains everything you need to know to get started using it in your game.
You can iterate on the game design data in a tool of your choosing (e.g., Google Sheets) and publish new configs to the players over-the-air. You can also view your game config data using the LiveOps Dashboard.
🚀 Pro Tip
Game configs are a deep topic, and there are at least as many workflows as there are game studios. However, Metaplay SDK's game configs pipeline is highly customizable and extendable beyond the basics. Look at the Further Reading section at the end of this page for proper deep dives!
The smallest unit of game config data is called a Game Config Item. Depending on how you like to model your game data, you could consider Items as individual objects in code or rows in a spreadsheet. Items can have as many properties as you like.
Kind #key | Damage | HitPoints | Speed | ... |
---|---|---|---|---|
Soldier | 10 | 100 | 1.5 |
Note that the columns that specify the item's identity must have the #key
tag, e.g., Kind #key
in this example. Items can also have multiple columns that make up the identity, e.g., a (Id, Level) tuple, in which case, both the colunms would have the #key
tag.
You typically have collections of Items that make logical sense to group together, such as game items, heroes, cards, etc. We call them Game Config Libraries. Designers often prefer to make one spreadsheet tab per Library to keep the various Libraries and their Items neatly organized.
Kind #key | Damage | HitPoints | Speed |
---|---|---|---|
Soldier | 10 | 100 | 1.5 |
Ninja | 50 | 150 | 5.25 |
... |
Finally, the SDK will package all your Libraries into binary files called Game Config Archives. They can be uploaded to a running server (either local or in the cloud) and published to game clients over-the-air without a separate client update or the need to reboot the game server.
Let's say you have a game with idle combat in which you improve your army by buying some troops and other combat units. Here's an overview of the initial set-up steps to make a game feature data-driven via game configs.
⚠️ Immutable game configs
Game Configs should not be mutated in game logic! Mutating the configs in the game logic can create non-deterministic behavior and unpredictable bugs, and even affect other players unintentionally. After the parsing stage, the configs should be read only!
The first thing you have to do is figure out how to represent your troops in your configs. By default, we use Google Sheets, but you have other options like using other spreadsheet applications, reading from JSON or CSV, or even using Unity prefabs. For most cases, it's as simple as adding a column for each property you need and adding rows for every Item. In this case, we have a column for each attribute we want our troops to have, and we have Soldier
and Ninja
as our troop types.
Kind #key | Damage | HitPoints | Speed |
---|---|---|---|
Soldier | 10 | 100 | 1.5 |
Ninja | 50 | 150 | 5.25 |
After figuring out how the data will look on the spreadsheets, you should add the equivalent class to your C# code. You might have already noticed, but this class will be the template for an entire Library. This way, each Library will have distinct columns for each class member, and you’ll have different Libraries for each new C# IGameConfigData<>
class you add.
// Unique string-like identifier type for TroopInfos (see below)
[MetaSerializable]
public class TroopKind : StringId<TroopKind> { }
// Configuration data for an in-game troop, identified by a TroopKind
[MetaSerializable]
public class TroopInfo : IGameConfigData<TroopKind>
{
[MetaMember(1)] TroopKind Kind; // Unique kind (id) of troop
[MetaMember(2)] int Damage; // Damage dealt
[MetaMember(3)] int HitPoints; // Hit points
[MetaMember(4)] F32 Speed; // Movement speed (a fixed-point value)
// Extract the key that uniquely identifies the troop
public TroopKind ConfigKey => Kind;
}
Implementing the IGameConfigData<>
interface registers the type as a class containing a Game Config Item.
Metaplay ships with a utility to build game configs quickly and easily.
You can open the window using Metaplay/Game Config Build Window
.
Building is quite simple, fill in a name and the url to the spreadsheet, and press build. We'll take care of converting the data and nicely packaging it into a compressed file.
This will automatically load the new configs into the Unity editor. If you happen to be running the game client in offline mode, it will also hot-load the changes into the running game. This is great for rapid iteration.
Metaplay SDK automatically updates the contents of SharedGameConfig
and recursively updates any references in the active PlayerModel
(or other models) to the updated Game Config Item classes.
Publishing is as simple as selecting the right environment, and pressing publish. This will automatically upload the latest built game config to the environment of your choice.
All uploaded game configs persist in the database along with their metadata. You can also view the complete history of game configs uploaded to the server in the game's LiveOps Dashboard on the Game Configs page:
There, you can also edit the name and description of the available Configs:
The default operation is to upload and publish the current Game Config Archive when calling PublishGameConfigArchiveToServerAsync
. Still, it is also possible to only upload a new Archive to the server without publishing it. With this workflow, you can review your changes in the dashboard before setting them as active.
By default, each client will negotiate the version of the Game Config Archives to use upon connecting to the server. Incidentally, not all players will necessarily be using the same version of the configs, and you need to take that into account when programming interactions in a multiplayer game.
Making Config Changes Before Updating the Server
It's not possible to preemptively push Config changes to an environment before updating the server. However, you can build the Configs locally and push the generated mpa file to the source control in the branch where you're planning on deploying the update from. Then, when you deploy an update in that environment, the server will automatically pick up the new Config from the branch you are deploying.
After you set a Game Config Library, designers can iterate on it by editing the config data, adding Items in their preferred tool, and triggering the build step. You can also test it locally before publishing it to the server and setting it as active.
Each Game Config Item must have a unique identifier, extracted in the Item from the IGameConfigData<>.ConfigKey
property. The SDK also uses this identifier to resolve references to any Items when deserializing data, e.g., when restoring player states from the database or sending messages or actions between the client and the server.
The default recommended way to identify a Game Config Item is to use a StringId
:
[MetaSerializable]
public class TroopKind : StringId<TroopKind> { }
[MetaSerializable]
public class TroopInfo : IGameConfigData<TroopKind>
{
...
public TroopKind ConfigKey => Kind;
}
The StringId
has some very convenient properties:
StringIds
to other StringIds
of the same type. So, for example, you can only compare two TroopKinds
against each other, but you cannot compare a TroopKind
to an InAppProductId
.StringId
reference itself being null. Empty strings are not allowed as values, nor can the contained value be null.The StringId
values should remain stable over the lifetime of a game. However, you will need to employ some form of migration in the case of renamed or removed identifiers. See section Handling Renamed Config Items in Working with Game Config Data for details.
🔑 Pro tip
It is also possible to use int
s or string
s as identifiers for Game Config Items, but this is discouraged since they lack the type safety that StringId
s provide
You can also use compound types (structs and classes with multiple members) as ConfigKey
s. Using them can be helpful, for example, when specifying per-level data in the source files/sheets. Check the Compound Identifier Types section in Working with Game Config Data.
As described in Deep Dive: Data Serialization, when a reference to an IGameConfigData<TKey>
-implementing class is serialized, only the ConfigKey
property is serialized. When deserializing, the SDK uses the ConfigKey
key to look up the Item in the appropriate Game Config Library. The PlayerModel
(including its sub-models) and PlayerActions
support such Game Config Item references. However, if game config data contains an Item reference within itself, it must be wrapped in the MetaRef<>
type instead of a plain C# reference.
For instance, building on the example in the Example: Troop Types section, imagine that we further have a TroopGroupInfo
config, with each TroopGroupInfo
containing a list of troop types. Again, we want to express the list of troop types using references to TroopInfo
Game Config Items instead of mere TroopKind
values so that their existence gets validated (to guard against typos and such) when the game config is loaded.
TroopGroupInfo
could be defined as follows:
[MetaSerializable]
public class TroopGroupId : StringId<TroopGroupId> { }
[MetaSerializable]
public class TroopGroupInfo : IGameConfigData<TroopGroupId>
{
[MetaMember(1)] public TroopGroupId GroupId; // Unique id of group
// Note: This has to use MetaRef<TroopInfo> instead of plain TroopInfo.
[MetaMember(2)] public List<MetaRef<TroopInfo>> Troops; // Troops in this group
public TroopGroupId ConfigKey => GroupId;
}
The concrete reference within the MetaRef
can be accessed via the Ref
property:
public int TroopGroupTotalDamage(TroopGroupInfo groupInfo)
{
int total = 0;
foreach (MetaRef<TroopInfo> troopInfo in groupInfo.Troops)
total += troopInfo.Ref.Damage; // Note: not "troopInfo.Damage"
return total;
}
Or alternatively, using a helper extension from the SDK:
public int TroopGroupTotalDamage(TroopGroupInfo groupInfo)
{
int total = 0;
// MetaRefUnwrap gets the Ref for each element of the list,
// decreasing verbosity in the rest of the loop.
foreach (TroopInfo troopInfo in groupInfo.Troops.MetaRefUnwrap())
total += troopInfo.Damage;
return total;
}
It's important to note that a MetaRef
is not resolved immediately upon deserialization but only when the client loads all of the Game Config Libraries. For this reason, you should only use the Ref
property in contexts where it's certain the MetaRef
has been resolved, or it'll throw an exception.
So, generally, when you implement methods or properties which can be accessed in any context (as opposed to ones you know you'll only access during normal game operation), you should not assume that the MetaRef
s are resolved. However, if you only need access to a Config Item's key, you can use the MetaRef
's KeyObject
property, which does not need to wait for the Libraries to finish loading:
// Suppose you were debugging which Config Items are built during the Configs build process
public string DebugDescription => $"Adding {(TroopKind)Kind?.KeyObject} troop to archive.";
Adding Soldier troop to archive.
Adding Ninja troop to archive.
...
🔍 Note
A null Game Config Item reference is represented by the MetaRef
variable itself being null. A MetaRef
object instance always represents a non-null reference.
When the serializer parses a Game Config Item class like TroopInfo
(see Basic Use Case and Workflow section), instead of writing out all the data within the Info class, only the ConfigKey
property is serialized.
// Extract the key that uniquely identifies the troop
public TroopKind ConfigKey => Kind;
Similarly, when deserializing references to IGameConfigData<>
, only the ConfigKey
is deserialized and used to resolve the actual TroopInfo
from the game's SharedGameConfig
. This serialization/deserialization of the ConfigKey
in place of the existing class members applies to serializing for sending over the network and persisting into the database. This keeps the serialized payloads smaller and, more importantly, allows changing the game config data without needing to update all the players in the database.
📦 Good to know
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.
You can create individual tweakable parameters in your configs using simple key-value pairs. Here's an example of a GlobalConfig
that defines the players' starting gold and gems:
[MetaSerializable]
public class GlobalConfig : GameConfigKeyValue<GlobalConfig>
{
[MetaMember(8)] public int InitialGold { get; private set; } = 50;
[MetaMember(9)] public int InitialGems { get; private set; } = 10;
}
You can define them in your configs source like so:
Member | Value |
---|---|
InitialGold | 50 |
InitialGems | 10 |
Later, declare your new key-value config somewhere in your SharedGameConfig
.
public class SharedGameConfig : SharedGameConfigBase
{
[GameConfigEntry("Global")]
public GlobalConfig Global { get; private set; }
}
You can find some more in-depth information about using key-value pairs in the configs in the Key-Value Pairs section.
After getting the basics of game configs down, here are some places you can go next: