Appearance
Appearance
Wordle Project Part 1 - This guide continues the project developed on Tutorial: Game Logic. You can either build it following the guide or import the project from samples/WordleGameLogic
inside the SDK.
Game Configs overview - This tutorial focuses on Metaplay's Configs system. Taking a look at Introduction to Game Configs should be helpful towards completing this guide.
Game Configs (or just Configs) specify the game’s economy, configuration, and design data. It is typically defined in spreadsheet format like a CSV file and can be hosted on the cloud, like on a Google Sheets spreadsheet.
A Game Config Item (or just Item) is a single configurable set of data representing one specific type of in-game element. It corresponds to a single row on a given Game Config Library sheet. An Item could be, for example, an enemy and its health and damage or a shop-exclusive item bundle, its price, and contents.
A Game Config Library (or just Library) is a collection of Config Items, typically ones specified in the same spreadsheet.
Game Config Archives are binary files in which the SDK packages all the config data. Archives are the atomic unit of transfer to clients using CDN.
While following this guide, you'll set up a Configs system that works with dynamic values on a spreadsheet instead of hardcoded items. This way, your game's designers can edit and balance your game without needing to open Unity or edit the game's code directly.
Here's a quick layout of the steps ahead:
The source code for this project, as well as Tutorial: Game Logic and Tutorial: Cheat-Proof Gameplay is available as a Unity project in the Samples/Wordle
directory in the SDK.
You can switch between this and the previous guide's version of the project using the Tutorial
button on Unity's menu.
Use the Part 1: Game Logic
version as a base to start this project and code along or take a look at Part 2: Game Configs
for the final result.
Here are the solutions we defined before:
List<string> Solutions = new List<string> { "MODEL", "GAMES", "SERVE", "LOGIC" };
We'll remove this from the code and add the solutions to a CSV file.
For the new implementation, we'll turn Solutions
into a Config Library. Each Library is composed of Items, each of which should contain at least an id and a value.
INFO
Pro tip! You can see an example and explanation of Config Libraries and corresponding classes in the 2. Add our Item Classes to Code section of Introduction to Game Configs.
We need to tell the SDK what to look for so we can process the CSV file correctly and access the configs.
To do this, we'll create a library inside our game's SharedGameConfig
, as well as add the GetSolutionForRound
method we had before in PlayerSubmitGuess
. Don't forget to remove the original GetSolutionForRound
from PlayerActions.cs
!
public class SharedGameConfig : SharedGameConfigBase
{
[GameConfigEntry("Solutions")]
public GameConfigLibrary<int, SolutionInfo> Solutions { get; protected set; }
public string GetSolutionForRound(int roundIndex)
{
return Solutions[roundIndex % Solutions.Count].Word;
}
}
And so, you can add a corresponding C# class to your Config files:
// GameConfigLibrary requires a class that implements IGameConfigData<> to store its data
// even if it's a single field. Using it adds an extra layer of safety to the game and
// helps us make sure make sure no bad data is being used.
[MetaSerializable]
public class SolutionInfo : IGameConfigData<int>, IGameConfigPostLoad
{
[MetaMember(1)] public int Id { get; private set; } // Unique id of the solution
[MetaMember(2)] public string Word { get; private set; } // Word for solution
public int ConfigKey => Id;
void IGameConfigPostLoad.PostLoad()
{
// Validate the Word to avoid bad data leaking into the game
MetaDebug.Assert(!string.IsNullOrEmpty(Word), "SolutionInfo {0} has empty Word", Id);
MetaDebug.Assert(Word.Length == 5, "SolutionInfo {0} Word is not 5 characters", Id);
// Convert to uppercase as that is what the game logic expects
Word = Word.ToUpperInvariant();
}
}
The way to access the solutions from gameplay code remains similar, with the exception that instead of using GetSolutionForRound
, we'll now use GameConfig.GetSolutionForRound()
from the PlayerModel
.
GameConfig.GetSolutionForRound(RoundIndex);
Another thing to note is that reading the solutions from the Configs introduces the possibility of the solutions being changed while a player is between sessions. To account for that, we'll store the current solution once while initializing a fresh PlayerModel
and update it when we advance rounds, instead of checking the Configs directly in PlayerSubmitGuess
.
public class PlayerModel : ...
{
[MetaMember(209)] public string CurrentSolution { get; set; } = "";
...
protected override void GameInitializeNewPlayerModel(MetaTime now, ISharedGameConfig gameConfig, EntityId playerId, string name)
{
// Setup initial state for new player
PlayerId = playerId;
PlayerName = name;
if (CurrentSolution == "")
{
CurrentSolution = GameConfig.GetSolutionForRound(RoundIndex);
}
}
}
[ModelAction(ActionCodes.PlayerAdvanceRound)]
public class PlayerAdvanceRound : PlayerAction
{
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (player.RoundStatus == RoundStatus.Playing)
return ActionResult.StillPlaying;
if (commit)
{
// Bump to next round & clear game state
player.RoundIndex++;
player.CurrentSolution = player.GameConfig.GetSolutionForRound(player.RoundIndex);
player.RoundStatus = RoundStatus.Playing;
player.CurrentWord = "";
player.GuessedWords.Clear();
player.GuessResults.Clear();
}
return ActionResult.Success;
}
}
For the game logic above to work, we need to fetch the Configs from the CSV file and build them into Libraries. Most of that happens automatically with the SDK's default config builder.
We just need to invoke the builder with the appropriate parameters and then write the resulting Game Config Archives into files. Archives are the binary files the SDK can publish to the cloud and deliver to game clients over-the-air.
By using new FileSystemBuildSource(FileSystemBuildSource.Format.Csv)
and GameConfigSourceFetcherConfigCore.WithLocalFileSourcesPath("Assets/LocalGameConfigSource")
, this script tells the game config builder that the Solutions
library should be produced from the Assets/LocalGameConfigSource/Solutions.csv
spreadsheet file.
public static class UnityGameConfigBuilder
{
const string SharedGameConfigPath = "Assets/StreamingAssets/SharedGameConfig.mpa";
const string StaticGameConfigPath = "Backend/Server/GameConfig/StaticGameConfig.mpa";
[MenuItem("Config Builder/Build GameConfig", isValidateFunction: false)]
public static async Task BuildFullGameConfigAsync()
{
// Build full config (Shared + Server)
DefaultGameConfigBuildParameters buildParams = new DefaultGameConfigBuildParameters()
{
DefaultSource = new FileSystemBuildSource(FileSystemBuildSource.Format.Csv)
};
IGameConfigSourceFetcherConfig fetcherConfig = GameConfigSourceFetcherConfigCore.Create().WithLocalFileSourcesPath("Assets/LocalGameConfigSource");
ConfigArchive fullConfigArchive = await StaticFullGameConfigBuilder.BuildArchiveAsync(MetaTime.Now, parentId: MetaGuid.None, parent: null, buildParams: buildParams, fetcherConfig: fetcherConfig);
// Extract SharedGameConfig from full config & write to disk
var sharedConfigArchive = ConfigArchive.FromBytes(fullConfigArchive.GetEntryByName("Shared.mpa").Bytes);
Debug.Log($"Writing {SharedGameConfigPath} with {sharedConfigArchive.Entries.Count} entries:\n{string.Join("\n", sharedConfigArchive.Entries.Select(entry => $" {entry.Name} ({entry.Bytes.Length} bytes): {entry.Hash}"))}");
await ConfigArchiveBuildUtility.WriteToFileAsync(SharedGameConfigPath, sharedConfigArchive);
// Write full StaticGameConfig to disk
Debug.Log($"Writing {StaticGameConfigPath} with {fullConfigArchive.Entries.Count} entries:\n{string.Join("\n", fullConfigArchive.Entries.Select(entry => $" {entry.Name} ({entry.Bytes.Length} bytes): {entry.Hash}"))}");
await ConfigArchiveBuildUtility.WriteToFileAsync(StaticGameConfigPath, fullConfigArchive);
// If in editor, refresh AssetDatabase to make sure Unity sees changed files
AssetDatabase.Refresh();
}
}
You can now build the Configs by using the Config Builder menu.
We did it! Now you have a custom game data pipeline you can easily extend to fit whatever data your game needs. Try experimenting for a bit and then rebuilding the configs.
Right now, the main drawback we have is that our game still trusts the client with the solution. This leaves the game vulnerable to attempts to cheat or hack the game. This is addressed in Tutorial: Cheat-Proof Gameplay, the next tutorial in the series.
Besides that, many more useful features rely on the Configs system. Here are some of them!
MetaReward
classes. Doing this facilitates adding new items through Configs and making them visible to the LiveOps Dashboard. You can learn more about it in Implementing MetaRewardsMetaRewards
. Check out Getting Started with In-App Purchases for more information.PlayerSegments
to separate your player base into subsets and have features targeted to specific audiences. Take a look at Implementing Player Segments for details.And last but not least, a great way to make your team's workflow even more dynamic is to host the Config spreadsheets on a cloud service like Google Sheets. Here's a tutorial on how to get started: Google Sheets Integration.