Appearance
Game Config Item Identifiers
Game Config Items must define an unique identifier so can be referred to. This article describes how to define such identifier.
Appearance
Game Config Items must define an unique identifier so can be referred to. This article describes how to define such identifier.
Each Game Config Item must have a unique identifier, defined by its IGameConfigData<>.ConfigKey property. This identifier is key used to retrieve the config data from the Game Config. For more information how to access the retrieved config data, see Game Config Item References.
When serializing a reference to a Game Config Item, instead of writing out all the data within the type, only the ConfigKey property is serialized instead. Similarly, when deserializing references to IGameConfigData<>, only the ConfigKey is deserialized and is then used to look up the actual Game Config Item from the game's Game Config. For more information on the serialization, see Deep Dive: Data Serialization.
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 identifiers 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 Renaming Config Items for details.
Uniqueness Domain
The identifier must be unique within the type of an Item in a single Game Config. For example, two TroopInfos cannot have the same ConfigKey, but hypothetical TroopInfo and TroopVisualInfo could use the same keys provided that TroopInfo and TroopVisualInfo do not share a base class.
The 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>
{
// Unique kind of troop
[MetaMember(1)] public TroopKind Kind { get; private set; }
...
public TroopKind ConfigKey => Kind;
}The StringId is defined as a string in the spreadsheet:
| Kind #key | Damage | HitPoints | Speed |
|---|---|---|---|
| Soldier | . | . | . |
| Ninja | . | . | . |
Key Columns
The columns corresponding to the ConfigKey must be marked with a #key tag.
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.Creating StringIds in Code
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");
In addition to StringId, primitive strings can also be used as identifiers for Game Config Items:
[MetaSerializable]
public class TroopInfo : IGameConfigData<TroopKind>
{
[MetaMember(1)] public string Kind { get; private set; }
...
public string ConfigKey => Kind;
}
[MetaSerializable]
public class WeaponInfo : IGameConfigData<WeaponInfo>
{
[MetaMember(1)] public string Kind { get; private set; }
...
public string ConfigKey => Kind;
}| Kind #key | Damage | HitPoints | Speed |
|---|---|---|---|
| Soldier | 10 | 100 | 1.5 |
| Ninja | 50 | 150 | 5.25 |
Using raw strings is discouraged since they lack the type safety that StringIds provide:
// Example of an error from type mismatch
// Should be Player.CurrentTroop.Weapon.Kind
string kind = Player.CurrentTroop.Kind;
// Oops, KeyNotFound exception or wrong data
var currentWeapon = Player.GameConfig.Weapons[kind];Other primitive types are supported too. For example, items can be identified with int, but this too is dicouraged due to lack of type safety.
Compound types (structs and classes with multiple members) can be used as ConfigKeys. This is useful, for example, when specifying per-level data in the source files/sheets.
For example, we can define an identifier type TroopKindLevel as follows and parse its members directly:
// 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 |
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 used 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);
}