Appearance
Appearance
While making a game with hard-coded player-rewarding logic is fine, typical free-to-play games have massive, data-driven economies with complex Reward loops. Using MetaReward
s allows you to create and edit in-game Rewards through the configs system, creating a much more flexible and future-proof workflow. It not only becomes easier to iterate upon Rewards during development but also to add new content during your game's lifecycle as part of your LiveOps operation process. As an added benefit, Rewards using the MetaReward
s system become automatically visible to the LiveOps Dashboard, and all it takes is a couple of lines of code to display them correctly in player inboxes, in-game mail, and other dashboard functionalities.
MetaReward
and Derived Classes Metaplay SDK has abstract base classes called MetaReward
and MetaPlayerReward
from which you can derive your Reward classes. MetaPlayerReward
has a Consume(TModel playerModel, IRewardSource source)
function that the client should call when applying Rewards to the player.
Here are two examples of defining custom Reward classes:
using Metaplay.Core.Model;
using Metaplay.Core.Rewards;
namespace Game.Logic
{
/// <summary>
/// Convenience class for rewards that are applied to the PlayerModel class.
/// </summary>
public abstract class PlayerReward : MetaPlayerReward<PlayerModel>
{
}
/// <summary>
/// A Reward with gold that gets added to the player.
/// </summary>
[MetaSerializableDerived(1)]
public class RewardGold : PlayerReward
{
// You can define any payloads you need for your game.
// In this case gold Rewards only have an "amount".
[MetaMember(1)] public int Amount { get; private set; }
RewardGold() { }
public RewardGold(int amount) { Amount = amount; }
// Consume function simply increments the player's gold resource.
// You could add additional game logic to respect resource caps, etc.
public override void Consume(PlayerModel playerModel, IRewardSource source)
{
playerModel.Avatar.NumGold += Amount;
}
}
/// <summary>
/// A 'weapon' Reward that unlocks and/or level up the player's weapons.
/// </summary>
[MetaSerializableDerived(2)]
public class RewardWeapon : PlayerReward
{
// Instead of making a separate class for every weapon, we use a more complex payload.
[MetaMember(1)] public WeaponTypeId WeaponId { get; private set; }
[MetaMember(2)] public int Amount { get; private set; }
RewardWeapon() { }
public RewardWeapon(WeaponTypeId weaponId, int amount)
{
WeaponId = weaponId;
Amount = amount;
}
// Weapon Rewards in this example are such that the first one
// is an unlock and the rest are upgrades.
public override void Consume(PlayerModel playerModel, IRewardSource source)
{
// Make sure the weapon is unlocked
if (!playerModel.Weapons.HasKey(WeaponId))
playerModel.UnlockWeapon(WeaponId);
// Upgrade the weapon
playerModel.Weapons[WeaponId].Level += Amount;
}
}
}
You can start using the custom Reward types in your game configs by registering custom parsers for them. See the example below for a practical guide.
After implementing the custom parsers, you can add members of type PlayerReward
into your game config classes, and the SDK will invoke the custom parsers automatically.
Warning
Game Configs must not be mutated! If you use MetaReward
s as part of your game configs, make sure to never modify their contents. Just like mutating any other parts of the game configs, mutating the MetaReward
can cause non-deterministic behavior and unpredictable bugs. This is best done by making your MetaReward
classes read-only.
// Derive a custom parser class from ConfigParserProvider to
// let Metaplay know of our custom parsers.
public class MyConfigParsers : ConfigParserProvider
{
// Callback that is invoked during initialization
public override void RegisterParsers(ConfigParser parser)
{
// Register a parsing method for the custom PlayerReward type
parser.RegisterCustomParseFunc<PlayerReward>(ParsePlayerReward);
}
// Method for parsing a PlayerReward type
static PlayerReward ParsePlayerReward(ConfigParser parser, ConfigLexer lexer)
{
// This parses the following Reward syntaxes,
// for RewardGold and RewardWeapon, respectively:
// 123 Gold
// 123 Weapon/Sword
// (where 123 is an example integer value and Sword
// is an example WeaponTypeId)
int amount = lexer.ParseIntegerLiteral();
string rewardType = lexer.ParseIdentifier();
switch (rewardType)
{
case "Gold":
return new RewardGold(amount);
case "Weapon":
{
lexer.ParseToken(ConfigLexer.TokenType.ForwardSlash);
WeaponTypeId weaponTypeId = parser.Parse<WeaponTypeId>(lexer);
return new RewardWeapon(weaponTypeId, amount);
}
default:
throw new ParseError($"Unhandled PlayerReward type in config: {rewardType}");
}
}
}
Here's an example of adding Rewards to a loot table style of config library.
LootId #key | Rewards[0] | Rewards[1] | Rewards[2] |
---|---|---|---|
SmallChest | 150 Gold | ||
MediumChest | 300 Gold | 1 Weapon/Sword | |
LargeChest | 300 Gold | 1 Weapon/Sword | 1 Weapon/Bow |
Once the configs containing the Rewards are available, you can access them like you would any regular config items. Following the previous section's example of configs, here's how you could give a player a specific reward:
[ModelAction(ActionCodes.PlayerOpenChest)]
public class PlayerOpenChest : PlayerAction
{
public LootTypeId LootId { get; private set; }
PlayerOpenChest() { }
public PlayerOpenChest(LootTypeId lootId) { LootId = lootId; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
List<PlayerReward> lootRewards = player.GameConfig.Loot[LootId].Rewards;
...
if (commit)
{
// Consume all the rewards. This adds them to the PlayerModel.
foreach (PlayerReward reward in lootRewards)
reward.InvokeConsume(player, source: null)
// Inform the client that the chest rewards were received
player.ClientListener.OnChestOpened(lootRewards);
}
return ActionResult.Success;
}
}
🎉 Ready to go!
Your MetaRewards will automatically be available as mail attachments in the LiveOps Dashboard UI. If you want to add them as displayable items in more instances of the dashboard, such as players' inboxes, just follow this section of the guide.
Here's how to add your player resources and Rewards to the dashboard integration, including fetching them from the game configs. Dashboard components that deal with Rewards use the information defined in your gameSpecific.ts
file in the Backend/Dashboard/src
folder:
export function GameSpecificPlugin (app: App) {
setGameSpecificInitialization(async (initializationApi) => {
...
// Custom rewards (shown in many places like the player's inbox).
initializationApi.addPlayerRewards([
{
$type: 'Game.Logic.RewardGold',
getDisplayValue: (reward) => `💰 Gold x${reward.amount}`,
},
{
$type: 'Game.Logic.RewardWeapon',
getDisplayValue: (reward) => `⚔️ ${reward.weaponId} x${reward.amount}`,
}
])
}
}
Once you have populated information about your Reward types in the integration, the rewards are automatically shown in the LiveOps Dashboard wherever they are used!
Just like we added custom player resources and Rewards to the dashboard, you can do much more to integrate it further into your game. Take a look at Customizing the LiveOps Dashboard Frontend to get started.
Now that you have some Rewards configured, it's time to give them away!