Appearance
Appearance
Stub article
This article is a stub and only contains a cursory overview of the topic. See the Idler example project for a complete reference.
Using Game Configs is split into two phases: building the Game Config Archive from source data as a preprocessing step, and importing the built Archive into the game at runtime. If you're not familiar with the Game Configs concepts, please read the Working with Game Config Data first. This page addresses the customization of the build steps for game-specific features.
FullGameConfig
is the main output from the Game Config Build Pipeline. It can contain the game's configuration, economy, and other relevant data. FullGameConfig
consists of SharedGameConfig
contains the data visible to both the client and the serverServerGameConfig
contains the parts that are only visible to the serverSharedGameConfig
and ServerGameConfig
— outputted by the Game Config Builder.SharedGameConfig
— used to distribute to client devices via CDN.GameConfig
in the future.GameConfigBuild
class is responsible for building the FullGameConfig
Archive: GameConfigBuildParameters.DefaultSource
(or additional custom Sources in a custom parameters type) when calling StaticFullGameConfigBuilder.BuildArchiveAsync
.IGameConfigSourceFetcherConfig
to BuildArchiveAsync
to concretely configure the build sources (e.g. specify spreadsheet ids or local directory paths).GameConfigBuildIntegration
and override GetAvailableBuildSources
to make sources available in dashboard UI.GameConfigBuildSource
classes.The build process consists of the following steps, each of which are described in more detail below:
GoogleSheetFetcher
class).BuildGameConfigKeyValueStructure<TStructure>()
parses simple key-value data from input SpreadsheetContent
into a regular class TStructure
must derive from GameConfigKeyValue
.BuildGameConfigLibrary<TKey, TInfo>()
builds a GameConfigLibrary<TKey, TInfo>
from SpreadsheetContent
and assigns it to the target Game Config.BuildGameConfigLibraryWithTransform<TKey, TInfo, TSourceItem>()
parses the input into a set of TSourceItem
s and then transforms them into TInfo
s using a user-defined conversion method, and then builds a GameConfigLibrar<TKey, TInfo>
out of them. SpreadsheetContent
stores raw spreadsheet content with original row/column numbers (from CSV files or Google Sheets).GameConfigHelper.ParseCsvToSpreadsheet()
for converting CSV to SpreadsheetContent
.GoogleSheetFetcher
returns SpreadsheetContent
by default.// Shared Game Config: Visible to both client and server.
public class SharedGameConfig : SharedGameConfigBase
{
// Metadata about languages supported by the game.
[GameConfigEntry("Languages")]
public GameConfigLibrary<LanguageId, LanguageInfo> Languages { get; private set; }
// Information about game-specific things.
[GameConfigEntry("TroopKinds")]
public GameConfigLibrary<TroopKind, TroopInfo> TroopKinds { get; private set; }
// Note: PopulateConfigEntries override is only needed if there are entries with a custom format.
public override void PopulateConfigEntries(GameConfigImporter importer)
{
base.PopulateConfigEntries(importer);
// ... Import some library in a custom manner ...
}
// Optional validation
public override void BuildTimeValidate(GameConfigValidationResult validationResult)
{
base.BuildTimeValidate(validationResult);
// ... Custom validation code that happens at config build time ...
}
}
// Server Game Config: Only visible to the server.
public class ServerGameConfig : ServerGameConfigBase
{
// Metadata about configured A/B experiments.
// Note: this is used by the SDK and is not needed until you wish to integrate the A/B experiment feature.
[GameConfigEntry(PlayerExperimentsEntryName)]
public GameConfigLibrary<PlayerExperimentId, PlayerExperimentInfo> PlayerExperiments { get; private set; }
// Include game-specific hidden information here
}
The example below enables game config building from the LiveOps Dashboard by providing the possible Google Sheets to build from.
public class MyGameConfigBuildIntegration : GameConfigBuildIntegration
{
public override IEnumerable<GameConfigBuildSource> GetAvailableBuildSources(string sourceProperty)
{
if (sourceProperty == nameof(GameConfigBuildParameters.DefaultSource))
{
return new[]
{
new GoogleSheetBuildSource("Development sheet", "1wXKv4SPD7BZBwTRc7YfAU7oVjuWFDZZFbHZ8PKyCatM"),
};
}
return base.GetAvailableBuildSources(sourceProperty);
}
}
The example below shows how to override the game config build steps for a specific entry. This is only needed when the default build pipeline behavior needs to be replaced.
// Declare build parameters
[MetaSerializableDerived(1)]
public class MyGameConfigBuildParameters : GameConfigBuildParameters
{
// Add game-specific build parameters here
}
// Declare a Game Config Builder with game-specific types as generic arguments.
public class MyGameConfigBuild : GameConfigBuildTemplate<SharedGameConfig, ServerGameConfig, MyConfigBuildParameters>
{
// GetEntryBuilder can override the default building behavior per config entry (i.e. library or GameConfigKeyValue).
protected override ConfigEntryBuilder? GetEntryBuilder(Type configType, string entryName)
{
// Custom handling when needed for specific libraries.
if (configType == typeof(SharedGameConfig) && entryName == "SomeLibrary")
{
<!-- markdownlint-disable-next-line MPL006 -->
// TODO: flesh out this example
return CustomEntryBuildSingleSource<List<IGameConfigData>>(
fetchFunc: GetGenericFetchFunc("SomeLibrary", BuildParameters.SomeCustomSource),
buildFunc: (IGameConfigBuilder shared, List<IGameConfigData> items) =>
{
List<VariantConfigItem<SomeId, SomeInfo>> variantItems = ... produce VariantConfigItems from items ...;
shared.AssignLibraryBuildResult("SomeLibrary", variantItems, sourceMapping: null);
});
}
// Default behavior should typically work for most entries
return base.GetEntryBuilder(configType, entryName);
}
}
ConfigParser
// This example registers custom parsers for the game-specific types `PlayerReward`
// and `PlayerPropertyId`. This can be done by inheriting from the `ConfigParserProvider`
// class and implementing its `RegisterParsers()` method. Metaplay automatically detects
// the inheriting classes.
public class ConfigParsers : ConfigParserProvider
{
// Register custom parsers for use in GameConfigBuilder
public override void RegisterParsers(ConfigParser parser)
{
parser.RegisterCustomParseFunc<PlayerReward>(ParsePlayerReward);
parser.RegisterCustomParseFunc<PlayerPropertyId>(ParsePlayerPropertyId);
}
// Custom parser for PlayerReward type. Useful for specifying contents of
// IAP packs, rewards from events, and anything else that the player can gain
// as a result of playing the game.
static PlayerReward ParsePlayerReward(ConfigLexer lexer)
{
int amount = lexer.ParseIntegerLiteral();
string rewardType = lexer.ParseIdentifier();
switch (rewardType)
{
case "Gems":
return new RewardGems(amount);
case "Gold":
return new RewardGold(amount);
default:
throw new ParseError($"Unhandled PlayerReward type in config: {rewardType}");
}
}
// Custom parser for PlayerPropertyIds, which are used to implement custom
// segmentation rules for the players.
static PlayerPropertyId ParsePlayerPropertyId(ConfigParser parser, ConfigLexer lexer)
{
// First try builtin parsing in case it's an SDK-defined PlayerPropertyId type.
if (ConfigParser.TryParseCorePlayerPropertyId(lexer, out PlayerPropertyId propertyId))
return propertyId;
// Game-specific PlayerPropertyIds.
string type = lexer.ParseIdentifier();
switch (type)
{
case "Gems":
return new PlayerPropertyIdGems();
case "Gold":
return new PlayerPropertyIdGold();
case "Producer":
{
lexer.ParseToken(ConfigLexer.TokenType.ForwardSlash);
MetaRef<ProducerInfo> producer = parser.Parse<MetaRef<ProducerInfo>>(lexer);
return new PlayerPropertyIdProducerLevel(producer);
}
case "LastKnownCountry":
return new PlayerPropertyLastKnownCountry();
case "AccountCreatedAt":
return new PlayerPropertyAccountCreatedAt();
case "AccountAge":
return new PlayerPropertyAccountAge();
case "TimeSinceLastLogin":
return new PlayerPropertyTimeSinceLastLogin();
}
throw new ParseError($"Invalid PlayerPropertyId in config: {type}");
}
}
The game config parser supports the following builtin types:
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! |
F32Vec2 and F64Vec2 | (1.23, 4.56) | |
F32Vec3 and F64Vec3 | (1.23, 4.56, 7.89) | |
IntVector2 | (123, 456) | |
double | -123.321 | 64-bit floating point, WARNING: not deterministic! |
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 | |
List<T> | See below | Either vertical (MyList[] ), horizontal (MyList[0] ), or single-cell comma-separated collection |
T[] | See below | Either vertical (MyArray[] ), horizontal (MyArray[0] ), or single-cell comma-separated collection |
Nested types | Member.SubMember | Can also parse into collection members (MyArray[0].Member ) |
Examples of vertical collection syntax:
MyId #key | IntArray[] | Nested[].Name | ... |
---|---|---|---|
FirstItem | 10 | First string | |
20 | Second string | ||
30 | |||
SecondItem | 100 | Only one string | |
200 |
Examples of horizontal collection syntax:
MyId #key | IntArray[0] | IntArray[1] | IntArray[2] | Nested[0].Name | Nested[1].Name | ... |
---|---|---|---|---|---|---|
FirstItem | 10 | 20 | 30 | First string | Second string | |
SecondItem | 100 | 200 | Only one string |
Collections can also be represented in a single cell using a comma-separate list of values:
MyId #key | IntArray | ... |
---|---|---|
FirstItem | 10, 20, 30 |
There is also special case single-cell syntax []
for specifying an empty collection. This is useful for A/B variants to clear the baseline collection to an empty one. It can be combined with other collection syntaxes in the same sheet, but only one can be used for a single row.
/Variant | MyId #key | IntArray | IntArray[0] | IntArray[1] | ... |
---|---|---|---|---|---|
FirstItem | 10 | 20 | |||
MyExperiment/MyVariant | [] |
The Idler Game Config build implementation supports incrementally updating only named entries in the currently active Game Config Archive. The Idler GameConfig Build
window in the Unity editor contains controls for choosing which Game Config Entries to update.
When a partial build is triggered, the currently active Game Config Archive is fetched from the server as the basis of the build operation and only the chosen Google Sheets are fetched and imported on top of it. To protect from overwriting simultaneous edits to other parts of the Game Config, the server rejects publishing a partially built config if the currently active config has changed after the Game Config was built.
INFO
Partial building requires support from the Game Config build integration. Please speak to us if partial building would help your workflow!