Appearance
Appearance
Work in progress
The game config system is getting cool new features and this documentation has a hard time keeping up! As always, we are here to fill in the blanks for your use cases and workflows!
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.
Good to know
By default, the latest version is taken into use upon the client connecting, so players connecting at different times may have a different game config versions active.
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.
This section covers how to identify individual Game Config Items, how to reference the Items in a way that works with serialization, and how to handle renaming previously deployed Items.
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, hitpoints, speed).
First, let's declare TroopKind
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> { }
Pro tip
A string
could also be used instead of a StringId
, but using a StringId
gives more type safety when used in the game code: by default TroopKind
can only be assigned and compared to other TroopKinds
, but not other types of StringIds
.
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)] TroopKind Kind; // Unique kind of troop (used to identify this item)
[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, `Kind` fully identifies this item
public TroopKind ConfigKey => Kind;
}
Implementing the IGameConfigData<>
interface registers the type as a class containing a Game Config Item. When serializing a TroopInfo
, instead of writing out all the data within the Info class, only the ConfigKey
property is serialized instead. Similarly, when deserializing references to IGameConfigData<>
, only the ConfigKey
is deserialized and is then used to resolve the actual TroopInfo
from the game's SharedGameConfig
.
This serialization/deserialization of the ConfigKey
in place of the actual class members applies to both serializing for sending over the network or 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.
The contents of the TroopKinds.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 |
Note that all columns that make up the identity (i.e., what is returned by TroopInfo.ConfigKey
in this case) must have the #key
tag after the name of the member. Although only one identity column is used here, some other game config items could use multiple columns for their ConfigKey
, for example, a (Type, Level)
pair to specify behaviors for different levels of a specific game item or building.
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.
The TroopKinds
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("TroopKinds")]
public GameConfigLibrary<TroopKind, TroopInfo> TroopKinds { get; private set; }
...
}
Each Game Config Item must have a unique way to identify it, extracted from the Item with the IGameConfigData<>.ConfigKey
property. This identifier is also used to resolve references to any Items when deserializing data, e.g., when restoring player states from the database, or when sending messages or actions between the client and the server.
StringIds
The default recommended way to identify a Game Config Item is to use a StringId
as in the example above.
The StringId
has the following properties:
StringIds
of a given type can be compared to other StringIds
of the same type. E.g., two TroopKinds
can be compared against each other, but a TroopKind
cannot be compared with 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. In the case of renamed or removed identifiers, some form of migration will be required. See section Advanced: Handling Renamed Config Items for details.
Pro tip
While the values of StringIds
should mainly be imported data files, it is possible to create StringIds
directly in code with the StringId.FromString(string)
method, e.g. TroopKind kind = TroopKind.FromString("Ninja");
It is also possible to use int
s or string
s as identifiers for Game Config Items, but this is discouraged they lack the type safety that StringId
s provide.
Compound types (structs and classes with multiple members) can be used as ConfigKey
s. This is useful, for example, when specifying per-level data in the source files/sheets. Here's a simplified example:
// Declare a (Kind, Level) tuple to be used as a ConfigKey
[MetaSerializable]
public struct TroopKindLevel : IEquatable<TroopKindLevel>
{
[MetaMember(1)] public TroopKind Kind { get; private set; }
[MetaMember(2)] public int Level { get; private set; }
public TroopKindLevel(TroopKind kind, int level)
{
Kind = kind;
Level = level;
}
public bool Equals(TroopKindLevel other) =>
Kind == other.Kind && Level == other.Level;
public override string ToString() =>
$"{Kind}/{Level}";
public override bool Equals(object obj) =>
(obj is TroopKindLevel other) ? Equals(other) : false;
public override int GetHashCode() =>
Util.CombineHashCode(Kind?.GetHashCode() ?? 0, Level.GetHashCode());
}
The TroopKindLevel
can be stored as a single member, which is then returned by the ConfigKey
getter:
[MetaSerializable]
public class TroopLevelInfo : IGameConfigData<TroopKindLevel>
{
[MetaMember(1)] public TroopKindLevel KindAndLevel { get; private set; }
[MetaMember(2)] public int HitPoints { get; private set; }
public TroopKindLevel ConfigKey => KindAndLevel;
}
When using the single-member key, the full path to the KindAndLevel.Kind
and KindAndLevel.Level
members of the key must be used in the input spreadsheet data, including the #key
tag:
KindAndLevel.Kind #key | KindAndLevel.Level #key | HitPoints | ... |
---|---|---|---|
Soldier | 1 | 10 | |
Soldier | 2 | 15 |
Alternatively, the TroopKindLevel
can be stored as separate members and combined in the ConfigKey
getter:
[MetaSerializable]
public class TroopLevelInfo : IGameConfigData<TroopKindLevel>
{
[MetaMember(1)] public TroopKind Kind { get; private set; }
[MetaMember(2)] public int Level { get; private set; }
[MetaMember(3)] public int HitPoints { get; private set; }
public TroopKindLevel ConfigKey => new TroopKindLevel(Kind, Level);
}
When using separate members, both the Kind
and the Level
columns require the #key
tag in the spreadsheet data:
Kind #key | Level #key | HitPoints | ... |
---|---|---|---|
Soldier | 1 | 10 | |
Soldier | 2 | 15 |
As explained in Game Config References, if the key type of a config data type is non-nullable, then null references of that config data type cannot be serialized by default. However, you can explicitly define a sentinel key for null references, and then null references can be serialized. A sentinel key is a key use to replace a null value, but that doesn't mean anything in itself. You should pick a sentinel key that will never be used as the key of an actual config item. Augmenting the TroopLevelInfo
example from above, we get something like this:
[MetaSerializable]
public class TroopLevelInfo : IGameConfigData<TroopKindLevel>
{
...
// Null TroopLevelInfo references will be serialized with this key.
public static TroopKindLevel ConfigNullSentinelKey => new TroopKindLevel(null, 0);
}
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.
GameConfigKeyValue
s 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 TroopStats : 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() { }
}
The game config pipeline supports parsing complex data, like nested classes, value collections like arrays and lists, and basic combinations of the two.
// Sub-class for specifying parameters outside the primary `TroopInfo` class:
[MetaSerializable]
public class TroopParams
{
[MetaMember(1)] public int HitPoints;
[MetaMember(2)] public int Damage;
}
[MetaSerializable]
public class TroopInfo : IGameConfigData<TroopKind>
{
[MetaMember(1)] TroopKind Kind; // Unique kind of troop
[MetaMember(2)] TroopParams Params; // 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 |
[MetaSerializable]
public class CollectionsExample : IGameConfigData<string>
{
[MetaMember(1)] string Id; // Identity
[MetaMember(2)] List<int> Ints; // Example of a List<>
[MetaMember(4)] TroopParams[] Params; // Exapmle of a complex array
// Extract the key that uniquely identifies the troop, `Kind` fully identifies this item
public TroopKind ConfigKey => Kind;
}
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.
You can use key-value pairs to create individual tweakable parameters in your configs. For example, here's an instance 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;
}
To define them in your configs source, the first row should have the Member
and Value
cells indicating the columns containing the member name and value, respectively. The columns may be in any order.
Member | Value |
---|---|
InitialGold | 50 |
InitialGems | 10 |
The source may additionally have a /Variant
column to support overrides for experiments. Check out Working with Experiments for an introduction to experiments and A/B testing.
/Variant | Member | Value |
---|---|---|
InitialGold | 100 | |
InitialResourcesExperiment/A | InitialGold | 200 |
InitialGems | 10 | |
InitialResourcesExperiment/A | InitialGems | 20 |
The key-value configs also support nested members.
[MetaSerializable]
public class GlobalConfig
{
[MetaMember(1)] public IntVector2 StartingCoordinates { get; private set; }
}
Member | Value |
---|---|
StartingCoordinates.X | 12 |
StartingCoordinates.Y | 34 |
It also supports collections, including collections of non-scalars. Naturally, the collection can also be a nested member.
[MetaSerializable]
public class GlobalConfig
{
[MetaMember(1)] public List<int> MyIntList { get; private set; }
[MetaMember(2)] public List<IntVector2> MyCoordList { get; private set; }
}
Member | Value |
---|---|
MyIntList[0] | 10 |
MyIntList[1] | 20 |
MyIntList[2] | 30 |
MyCoordList[0].X | 1 |
MyCoordList[0].Y | 2 |
MyCoordList[1].X | 11 |
MyCoordList[1].Y | 22 |
Collections can also be written horizontally, without an explicit index inside the brackets. In this case, the values are spread out on multiple columns, starting from the column marked as Value
in the header, and extending to the right.
Member | Value | ||
---|---|---|---|
MyIntList[] | 10 | 20 | 30 |
MyCoordList[].X | 1 | 11 | |
MyCoordList[].Y | 2 | 22 |
It is also possible to user variants for nested members:
/Variant | Member | Value |
---|---|---|
StartingCoordinates.X | 12 | |
DifferentCoords/A | StartingCoordinates.X | 120 |
StartingCoordinates.Y | 34 | |
DifferentCoords/A | StartingCoordinates.Y | 340 |
However, the current implementation has a limitation when multiple different variants patch sub-members of the same top-level member. In this case, if a player belongs to both of those variants, only one of them will take effect for the whole top-level member. For example:
/Variant | Member | Value |
---|---|---|
StartingCoordinates.X | 12 | |
SomeExperiment/A | StartingCoordinates.X | 120 |
StartingCoordinates.Y | 34 | |
OtherExperiment/A | StartingCoordinates.Y | 340 |
Now, for a player belonging to both SomeExperiment/A
and OtherExperiment/B
, StartingCoordinates
will be either (120, 34)
or (12, 340)
, but not (120, 340)
like you'd expect.
Finally, comment rows and columns are supported:
Member | Value | // this column is ignored |
---|---|---|
InitialGold | 100 | Player's starting gold |
// this row is ignored | comment | comment |
InitialGems | 10 | Player's starting gems |
As described in Deep Dive: Data Serialization, when a reference to a IGameConfigData<TKey>
-implementing class is serialized, only the ConfigKey
property is serialized. When deserializing, the key is used to look up the item in the appropriate Game Config Library. Such Game Config Item references are supported in PlayerModel
(including its sub-models) as well as PlayerActions
. However, if an Item reference is contained within game config data itself, it must be wrapped in the MetaRef<>
type, instead of being a plain C# reference.
For example, building on the example in section Example: Troop Types, imagine that we further have a TroopGroupInfo
config, with each TroopGroupInfo
containing of a list of troop types. We would like 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;
}
Alternatively, in this case we can use 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;
}
Note
A null Config Item reference is represented by the MetaRef
variable itself being null. A MetaRef
object instance always represents a non-null reference.
Heads up
MetaRef
s are not resolved immediately upon deserialization, but only when all the Config Libraries have been loaded. An exception is thrown if the Ref
property is accessed before the MetaRef
has been resolved. If you need the ConfigKey
of the referred-to Config Item in a context where the MetaRef
might be non-resolved (such as in a ToString
method which may be used during the building of the Game Config), you should use MetaRef
's KeyObject
property instead, which is always available.
Usually, all GameConfigLibrary
s have distinct Item types. However, it is possible to have multiple GameConfigLibrary
s with either the same type or different types derived from the same base IGameConfigData<>
-implementing Item type. In this case, a config reference can refer to Items in any Libraries with a compatible type.
For example, imagine you have types WeaponInfo
and ShieldInfo
that derive from an EquipmentInfo
base class that implements IGameConfigData<EquipmentId>
. You can then have separate Libraries for both weapon and shield types:
public class SharedGameConfig : SharedGameConfigBase
{
[GameConfigEntry("Weapons")]
public GameConfigLibrary<EquipmentId, WeaponInfo> Weapons { get; private set; }
[GameConfigEntry("Shields")]
public GameConfigLibrary<EquipmentId, ShieldInfo> Shields { get; private set; }
}
In this situation, a MetaRef<EquipmentInfo>
could refer Items in either the Weapons
or Shields
Libraries. Note that a concretely typed MetaRef<WeaponInfo>
or MetaRef<ShieldInfo>
can still be used and would only refer to Weapons
or Shields
, respectively.
Removing or renaming config items will cause any persisted data (the PlayerModel
, for example) that refers to the item to no longer deserialize correctly.
For these situations, the GameConfigLibrary
API supports specifying ConfigKey
aliases. An alias is simply another ConfigKey
for an existing config item for purposes of deserialization. After deserializing via an alias mapping, no trace of the alias is stored in the result. This means that upon re-serialization, the current ConfigKey
will be written out rather than the old alias value.
ConfigKey
aliases should be registered upon loading a GameConfigLibrary
before any deserialization involving the game config takes place. A good place for this is SharedGameConfig.OnLoaded()
or ServerGameConfig.OnLoaded()
.
Let's consider an example of changing the Producer type id for the "Vue" Producer to "VueX" in the Idler reference project. The updated game config will contain an entry for "VueX" but no entry for "Vue". To have existing players that have unlocked the "Vue" producer migrate their progress to the "VueX" producer we'll introduce an alias to the Producers
config library:
protected override void OnLoaded()
{
base.OnLoaded();
ProducerTypeId oldKey = ProducerTypeId.FromString("Vue");
ProducerTypeId newKey = ProducerTypeId.FromString("VueX");
Producers.RegisterAlias(id: newKey, alias: oldKey);
}
The alias resolution mechanism is used only for deserializing references to game config items. Any direct use of renamed ConfigKey
values must be handled by custom logic. In our example case, the Idler PlayerModel
in fact stores a Dictionary of ProducerModel
objects keyed by ProducerTypeId
values. To carry over a "Vue" ProducerModel
to the renamed Producer type id we'll add a MetaOnDeserialized
function to PlayerModel
:
[MetaOnDeserialized]
public void OnDeserialized()
{
Dictionary<ProducerTypeId, ProducerTypeId> renames = new Dictionary<ProducerTypeId, ProducerTypeId>();
foreach (ProducerModel producer in Producers)
{
// The ProducerInfo reference to an old config item will be updated
// to a renamed config item by alias resolution. We can use this to detect
// the case where our separately stored key is no longer the current key for
// the item.
if (producer.Key != producer.Value.Info.ConfigKey)
renames[producer.Key] = producer.Value.Info.ConfigKey;
}
foreach (KeyValuePair<ProducerTypeId, ProducerTypeId> rename in renames)
{
Producers[rename.Value] = Producers[rename.Key];
Producers.Remove(rename.Key);
}
}
The renaming mechanism can also be used for gracefully deleting config items. This is done by introducing a "sentinel" config item that represents obsoleted config entries and registering aliases for the sentinel item for deleted config keys. In the MetaOnDeserialized
handler, the game code can then handle any occurrences of the sentinel item as appropriate.
The SpreadSheetParser
implementation in the Metaplay SDK supports declaring ConfigKey
aliases conveniently from Google Sheets. This is done by adding a special column by name /Aliases
that contains one or more aliases (a.k.a. old names) for the config element. As part of the game config build, the alias information will be parsed and serialized into the resulting Game Config Archive and then automatically registered on the Game Config Library when the Archive is imported.
Game Config Archives are used as the atomic unit of transfer from the server to the clients via CDN, meaning that all the included game config data changes at the same time. The Metaplay SDK builds separate Archives for client-visible data (containing only the SharedGameConfig
class) and for data consumed by the server (additionally containing the ServerGameConfig
class). Most games will package the localization data in a separate Game Config Archive.
This section describes how the build is triggered. For a description of how the build steps are done and configured, see Custom Game Config Build Pipelines.
By default, the Unity SDK comes with a game config build Window, you can find the window under Metaplay/Game Config Build Window
.
To build the game configs from Unity, open the game config build window and press the Build
button.
When the StaticGameConfig
Archive is generated, it is timestamped with the time of its creation, a unique version ID is generated by hashing all its contents, and the Archive is written to the disk.
When using a local server, the latest version of each Archive is loaded at the server start and distributed to the clients on connecting. When running in Offline Mode, the latest Archives are loaded on application start.
Customizing
The UnityGameConfigBuilder
of Idler is intended to be a reference for implementing the build flow, and the flows can be customized to fit each game's or team's needs. For example, many developers prefer triggering the game config builds from Unity menus, but it's also possible to invoke the builds on the server.
Metaplay supports uploading and publishing the built Game Config Archives to the server via its HTTP API. The uploads can be triggered in the Unity editor via the GameConfig Build window by clicking "Publish".
All uploaded game configs are persisted into the database along with metadata. The full history of game configs uploaded to the server can be viewed in the game's LiveOps Dashboard on the Game Configs page:
Pro tip
In addition to game configs uploaded via the HTTP API, the server will persist and publish game configs from disk on startup if it determines that the config found on disk is newer than the currently active config. This is to allow updating the active game configs on server (re-)deployment.
The details view of a game config in the LiveOps Dashboard renders the contents of the contained Game Config Items.
To easily identify game configs later, the LiveOps Dashboard allows adding a human-readable name and description to the uploaded configs. Game configs that are no longer needed can also be archived to reduce clutter in the list view.
Game config content can be conveniently compared to the currently active one in the LiveOps Dashboard either by clicking on the "binoculars" icon in the list view or the View Diffs button in the details view.
The default operation is to upload and publish the current Game Config Archive when the Publish button is clicked, but it is also possible to only upload a new Archive to the server without publishing it. With this workflow, it is possible to review your changes in the dashboard before publishing them.
By default, each client will negotiate the version of the Game Config Archives to use upon connecting to the server. This means that not all the players are necessarily using the same version of the game configs. This needs to be taken into account when players are interacting in a multiplayer game.
Good to know
Whenever a user uploads, publishes, or edits an Archive through the LiveOps Dashboard, they leave a trail in the audit log. This helps you to track and understand changes to your game configs. For more information see LiveOps Dashboard Audit Logs
When running the game in offline mode in the Unity editor, it is possible to build the game configs while the game is running. The new version is then automatically hot-loaded, which allows rapid iteration on the data, as the game does not need to be restarted.
Metaplay 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.
Localization Configs mostly behave the same as design game configs, but have a few key differences:
For a guide on implementing localization with Metaplay, please see Localization.