Appearance
Game Config Item References
How to refer to Game Config Items in Models and in other Game Config Items.
Appearance
How to refer to Game Config Items in Models and in other Game Config Items.
IGameConfigData<>.ConfigKey property and used by the SDK to retrieve the config data from the Game Config.To access a specific Game Config Item, instead of keeping a Game Config Item Identifier as a key and then accessing the Game Config library using the key at runtime, you can use a Reference. References provide superior usability, improve safety and support for migration tooling, all with no runtime performance penalty.
References have two major lifecycle phases. First a reference is created, parsed or deserialized, like any other data type. This results in the reference knowing the Item's Identifier, but not yet the Item itself. This is called an unresolved state. Then the reference's value is loaded by looking up for the Item in the Game Config using the Identifier as the key. This phase converts the Identifier into the actual targeted Item and the reference becomes resolved. The specifics and the timing of the resolution depend on the type of the reference.
Compatible Data Format
All reference types have the same serialization format as their Identifiers, and switching between them does not cause compatibility problems with existing serialized data. For more information, see Deep Dive: Data Serialization.
The same rules apply for references and Game Config Item Identifiers, and they 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.
Metaplay supports three different ways to represent a reference to an Item in a Game Config: MetaRefs and MetaConfigIds and plain references.
MetaRef is a reference to a specific item, where the resolution happens after all Game Config Libraries are loaded. This makes it suitable for cross-references and recursive references in Game Config Data.
Null References
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.
For instance, imagine that we 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>
{
// Unique identity of group.
[MetaMember(1)] public TroopGroupId GroupId { get; private set; }
// Troops in this group.
// Note: Must use MetaRef<TroopInfo> when referring to other game config libraries.
// Using IReadOnlyList<T> prevents accidental modification of the config data.
[MetaMember(2)] public IReadOnlyList<MetaRef<TroopInfo>> Troops { get; private set; }
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;
}MetaRefs may also refer to the their own type of an Item. For example, if we have a config item class RoomInfo which describes a room on the game map. The rooms have dependencies such that completing a room unlocks the next room. You could choose to represent this dependency with a NextRoom reference within the RoomInfo class itself.
[MetaSerializable]
public class RoomId : StringId<RoomId> { }
[MetaSerializable]
public class RoomInfo : IGameConfigData<RoomId>
{
[MetaMember(1)] public RoomId Id { get; private set; }
[MetaMember(2)] public string Name { get; private set; }
[MetaMember(3)] public MetaRef<RoomInfo> NextRoom { get; private set; }
public RoomId ConfigKey => Id;
}When you need to access the item referenced by a MetaRef, we again access its Ref property. For example, to retrieve the next room from our RoomInfo:
RoomInfo GetNextRoom(RoomInfo currentRoom)
{
// Get the referenced item using the current game config
RoomInfo nextRoom = currentRoom.NextRoom.Ref;
return nextRoom;
}MetaRef is 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, you should use MetaRef's KeyObject property instead, which is always available.
Accessing a MetaRef before the whole library has been loaded is mostly possible via low-level hooks and during customized game config building. For example, if you access a MetaRef field in deserialization callbacks such as [MetaOnDeserialized] or [MetaOnMemberDeserializationFailure], the reference is not resolved yet. Another more common example is accessing the reference in a ToString() method which then may be used during the building of the Game Config. In this case, KeyObject could be used as follows:
// 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.
...MetaConfigId is an alternative to MetaRef that avoids load-time resolution, and instead the target Item is resolved at Usage Time. By avoiding load-time resolution, the MetaConfigId references and Game Config objects containing them can be deduplicated in memory. This is mostly useful when Optimizing Experiments and reducing the memory pressure from a large number of experiment combinations.
MetaConfigId functions similarly to MetaRef. In our RoomInfo class, to add a reference the next room of the same type, we can add a MetaConfigId<RoomInfo> member.
[MetaSerializable]
public class RoomInfo : IGameConfigData<RoomId>
{
[MetaMember(1)] public RoomId Id { get; private set; }
[MetaMember(2)] public string Name { get; private set; }
[MetaMember(3)] public MetaConfigId<RoomInfo> NextRoom { get; private set; }
public RoomId ConfigKey => Id;
}When you need to access the item referenced by a MetaConfigId, you need the current game config to act as the resolver. The item is then fetched from the game config using the config ID and the type information in the MetaConfigId. Here's an example of getting the item using MetaConfigId.GetItem() in PlayerModel:
RoomInfo GetNextRoom(RoomInfo currentRoom)
{
// Get the referenced item using the current game config
RoomInfo nextRoom = currentRoom.NextRoom.GetItem(GameConfig);
return nextRoom;
}You can always access the current game config through the PlayerModel. In client-only code, such as in UI, you can typically access MetaplayClient.PlayerModel.GameConfig.
Note that if the current game config does not contain the referenced item, MetaConfigId.GetItem() will throw an exception. If you expect that the reference might be invalid at resolve time, you can alternatively use MetaConfigId.TryGetItem(), which will return null if the reference is invalid. When the MetaConfigId member itself is contained within game config data, this is not a concern, because config-internal MetaConfigIds are validated already when you build the game config; you can safely call GetItem() in such cases. TryGetItem() is intended for cases where the MetaConfigId member is stored elsewhere, such as in PlayerModel, and there is a possibility that a future config will no longer have the referred item.
In addition to marking references with MetaRef<T> or MetaConfigId<T>, a plain C# reference to T can also be used. Plain references are resolved immediately when deserialized without a separate resolution phase. This means plain references cannot be used in Game Config types. The resolution requires the Game Config library itself, and this is not yet available during the deserialization.
Plain references can be used in other use places, such as in PlayerModel:
public class PlayerModel
{
...
// Reference to the GameConfig.Troops[key].
// When GameConfig is updated, this value changes too.
[MetaMember(102)] public TroopInfo CurrentTroop;
}To store the Game Config Item as a whole, and not a reference, the type must be wrapped into a GameConfigDataContent<>. For example, to store a copy of the config when the current battle started:
public class PlayerModel
{
...
// A copy of a the GameConfig.Troops[key].
// When GameConfig is updated, this will not change.
[MetaMember(102)] public GameConfigDataContent<TroopInfo> TroopWhenBattleBegan;
}Usually, all GameConfigLibrarys have distinct Item types. However, it is possible to have multiple GameConfigLibrarys 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:
[MetaSerializable]
public class EquipmentId : StringId<EquipmentId> { }
[MetaSerializable]
public class EquipmentInfo : IGameConfigData<EquipmentId>
{
[MetaMember(1)] public EquipmentId Id;
[MetaMember(2)] public int MarketCost;
public EquipmentId ConfigKey => Id;
}
[MetaSerializable]
public class WeaponInfo : EquipmentInfo
{
[MetaMember(101)] public int Damage;
}
[MetaSerializable]
public class ShieldInfo : EquipmentInfo
{
[MetaMember(101)] public int Armor;
}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 reference like MetaRef<EquipmentInfo> could refer Items in either the Weapons or Shields Libraries. Note that a concretely typed MetaRef<WeaponInfo> or MetaRef<ShieldInfo> or similar reference can still be used and would only refer to Weapons or Shields, respectively.