Appearance
Appearance
Metaplay SDK's In-Game Offers system is a powerful way to manage your real-money purchases within the game. With it, you are able to flexibly combine different In-App Products and Rewards into purchasable In-Game Offers and organise those offers into Offer Groups that are visible in the game. Since all of this is data driven and defined in your game configs, you will be able to quickly iterate and release new versions over-the-air without any client updates!
This quick guide shows the steps to integrate Offers into your game. You'll configure a few Offers and group them into an Offer Group. Then you'll implement client UI to visualize the Offers and allow the player to purchase them.
Parts of the integration are game-specific; for those parts this guide will refer to hypothetical examples which you should adjust to be appropriate for your game.
After completing the quick guide, you may want to have a look at the Advanced Usage section, which describes the various parameters supported by Offers and Offer Groups. It also explains how the Offers feature itself can be extended beyond the functionality provided by the Metaplay SDK.
Offers
Config Sheet Let's create a new Game Config sheet for the individual Offers. You can put this data in a Google Sheet or a .csv file, or whatever tool you use for authoring Game Configs in your project.
OfferId #key | DisplayName | Description | InAppProduct | Rewards[0] | MaxPurchasesPerPlayer |
---|---|---|---|---|---|
GemOffer1 | Gem Offer 1 | Small gem offer | Offer5Usd | 100 Gems | 5 |
GemOffer2 | Gem Offer 2 | Large gem offer | Offer10Usd | 200 Gems | 5 |
This sheet defines two simple gem Offers, each of which can be purchased at most 5 times by a player. This example uses a "Gems" Reward type; your game might have some other type of Reward.
Offers by themselves do not do anything; they need to exist in Offer Groups to be shown to players. Next, let's define an Offer Group.
OfferGroups
Config Sheet Similarly to the previous step, let's create a new sheet for Offer Groups.
GroupId #key | DisplayName | Description | Placement | Priority | Offers[0] | Offers[1] | Lifetime | Cooldown |
---|---|---|---|---|---|---|---|---|
GemOffers | Gem Offers | Various gem offers | Shop | 1 | GemOffer1 | GemOffer2 | 5h | 12h |
This defines an Offer Group that contains the Offers defined in the previous step. The GemOffers Group is specified to appear in a Placement called Shop
- we'll come to Placements later when we talk about the client implementation. The Group stays visible for 5 hours at a time, or until all Offers in it have been sold out. After expiring, it has a 12-hour cooldown before it can be shown again.
Offers need to refer to In-App Products to give them a price point. When we defined our Offers earlier, we referenced Offer5Usd
and Offer10Usd
for exactly this reason. Now we need to make sure that those IAPs are defined in the game's IAP config. If your game already has suitable IAPs configured, you can refer to those in the Offers
sheet instead and skip this step; otherwise, let's define the IAPs now.
We'll assume your game already has an InAppProducts
config. Below is a content example for the new offer IAPs. You can adjust fields such as identifiers and prices from this example sheet below as appropriate for your game.
Note the HasDynamicContent
field, which for In-App Products used in Offers is always TRUE
. Note also the empty reward fields, such as NumGems
: In-App Products used in Offers only exist as "placeholder" products, and the real rewards are defined in the Offers
sheet.
ProductId #key | Name | Price | HasDynamicContent | NumGems | DevelopmentId | GoogleId | AppleId |
---|---|---|---|---|---|---|---|
(.,. pre-existing IAPs ...) | ... | ... | ... | ... | ... | ... | ... |
Offer5Usd | 5-dollar offer | 5 | TRUE | dev.Offer5Usd | com.company.game.offer_5_usd | com.company.game.Offer5Usd | |
Offer10Usd | 10-dollar offer | 10 | TRUE | dev.Offer10Usd | com.company.game.offer_10_usd | com.company.game.Offer10Usd |
To represent the Offers and Offer Groups in the C# code, define the corresponding libraries in SharedGameConfig
:
public class SharedGameConfig : SharedGameConfigBase
{
[GameConfigEntry("Offers")]
[GameConfigEntryTransform(typeof(DefaultMetaOfferSourceConfigItem))]
public GameConfigLibrary<MetaOfferId, DefaultMetaOfferInfo> Offers { get; private set; }
[GameConfigEntry("OfferGroups")]
[GameConfigEntryTransform(typeof(DefaultMetaOfferGroupSourceConfigItem))]
public GameConfigLibrary<MetaOfferGroupId, DefaultMetaOfferGroupInfo> OfferGroups { get; private set; }
}
If you don't define a custom class derived from GameConfigBuild
, the SDK will build all libraries in your SharedGameConfig
class, so you don't need to explicitly introduce these libraries to the builder. Simply run the game config build now to produce a new config archive that includes the Offers
and OfferGroups
libraries built from the sheets.
If you have a custom GameConfigBuild
-derived class, consider if you need to update it to build the Offers
and OfferGroups
libraries.
Now is a good time to check that the integration so far is successful. The Unity editor UI for the MetaplaySDK
game object (or any other object in your game that has MetaplaySDKBehavior
) includes a handy visualization of the player's active Offers. Inspect the MetaplaySDKBehavior
-equipped object while the client is running, and you should see the the configured Offers, contained within the Offer Group:
💡 Note
Since the in-app purchase flow involves game-specific integration, the "Buy" button won't work out of the box. It can be made to work by implementing dynamic purchase tracking in the PendingDynamicPurchaseContentAssigned
handler of your IPlayerModelClientListenerCore
, as in the Idler reference project. Step 7: Implement Purchase Triggering will show how to implement purchasing in your actual game UI.
The editor UI from the previous step is useful for initial testing, but we also want to wire the Offers to the actual game UI. The visualization of Offers is highly game-specific. You might have multiple Placements, each in its own part of the game's UI, or you might, as in these examples, only have a single Placement.
The following code examples assumes a simple shop UI which shows the active Offer Group in the Shop
Placement. Bear in mind that, due to targeting conditions and limits, there might not always be an active Offer Group for a Placement.
The ShopScript
is responsible for showing the correct Offers based on the active Offer Group, and the ShopOfferScript
is responsible for managing the UI of a single Offer.
// ShopScript.cs
// Object references.
public ShopOfferScript OfferPrefab; // Prefab for individual offers.
public Transform OfferList; // Parent for offer gameobjects.
public Text GroupStatusText; // Info about the status of the active group.
// Keep track of current active offer group and its offer gameobjects.
MetaOfferGroupInfoBase _activeGroupInfo = null;
List<ShopOfferScript> _activeGroupOfferObjects = new List<ShopOfferScript>();
void UpdateOffers()
{
PlayerModel player = MetaplayClient.PlayerModel;
// Here, we only show offer groups with the placement Shop.
OfferPlacementId shopPlacement = OfferPlacementId.FromString("Shop");
// Find the active offer group in the Shop placement, if any.
MetaOfferGroupModelBase activeGroup = player.MetaOfferGroups
.GetActiveStates(player)
.FirstOrDefault(group => group.ActivableInfo.Placement == shopPlacement);
MetaOfferGroupInfoBase activeGroupInfo = activeGroup?.ActivableInfo;
// If active group has changed since last update, create new offer gameobjects.
if (activeGroupInfo?.GroupId != _activeGroupInfo?.GroupId)
{
// \note activeGroup and activeGroupInfo can still be null here!
_activeGroupInfo = activeGroupInfo;
// Destroy offer gameobjects of the previously-active group.
foreach (ShopOfferScript offerItem in _activeGroupOfferObjects)
Destroy(offerItem.gameObject);
// Spawn offer gameobjects for the newly-active group (if any).
_activeGroupOfferObjects = new List<ShopOfferScript>();
if (activeGroupInfo != null)
{
foreach (MetaOfferInfoBase offerInfo in activeGroupInfo.Offers.MetaRefUnwrap())
{
ShopOfferScript offerItem = Instantiate(OfferPrefab, parent: OfferList);
offerItem.OfferGroupId = activeGroupInfo.GroupId;
offerItem.OfferId = offerInfo.OfferId;
_activeGroupOfferObjects.Add(offerItem);
}
}
}
// Update the UI for the active offer group.
if (activeGroup != null)
{
// Show group expiration timer.
MetaActivableState.Activation activation = activeGroup.LatestActivation.Value;
string expirationText = "";
if (activation.EndAt.HasValue)
expirationText = $"Expires in {activation.EndAt.Value - player.CurrentTime}";
GroupStatusText.text = expirationText;
GroupStatusText.gameObject.SetActive(true);
// Show or hide each individual offer based on whether it is
// currently active or not.
// Even if the offer group is active, individual offers in it
// might be inactive due to offer-specific targeting.
foreach (ShopOfferScript offerItem in _activeGroupOfferObjects)
offerItem.gameObject.SetActive(activeGroup.OfferIsActive(offerItem.OfferId, player));
}
else
{
// There is no active offer group.
GroupStatusText.gameObject.SetActive(false);
}
}
// ShopOfferScript.cs
// Object references.
public Text InfoText; // Info about the offer and its status.
// Parameters assigned by ShopScript when it spawns this offer object.
public MetaOfferGroupId OfferGroupId; // The Group this offer exists in.
public MetaOfferId OfferId;
void Update()
{
PlayerModel player = MetaplayClient.PlayerModel;
MetaOfferGroupInfoBase offerGroupInfo = player.GameConfig.OfferGroups[OfferGroupId];
MetaOfferInfoBase offerInfo = player.GameConfig.Offers[OfferId];
// If the offer group has expired, don't update UI.
if (!player.MetaOfferGroups.IsActive(OfferGroupId, player))
return;
// Get the status of the offer within the group.
// This contains info such as how many times the offer has been purchased.
MetaOfferStatus offerStatus = player.MetaOfferGroups.GetOfferStatus(player, offerGroupInfo, offerInfo);
// Display the offer's name and remaining purchase count
string infoText = offerInfo.DisplayName;
int? purchasesRemaining = offerStatus.PurchasesRemainingInThisActivation;
if (purchasesRemaining.HasValue)
infoText += $" ({purchasesRemaining.Value} remaining)";
InfoText.text = infoText;
}
Let's augment ShopOfferScript
with the ability to purchase Offers. Note that this section assumes that normal Metaplay-integrated IAP management mechanisms have already been implemented in the game, as described in the Implementing the Validation Flow section in Getting Started with In-App Purchases.
Handle the player's click on the purchase button:
// ShopOfferScript.cs
public void OnClickBuy()
{
PlayerModel player = MetaplayClient.PlayerModel;
MetaOfferGroupInfoBase offerGroupInfo = player.GameConfig.OfferGroups[OfferGroupId];
MetaOfferInfoBase offerInfo = player.GameConfig.Offers[OfferId];
// Start preparing the purchase of the offer.
MetaplayClient.PlayerContext.ExecuteAction(new PlayerPreparePurchaseMetaOffer(
offerGroupInfo,
offerInfo,
// Specify info for analytics about the context of the purchase.
// If it isn't needed, it can also be left null for now.
new GamePurchaseAnalyticsContext(
placement: offerGroupInfo.Placement.ToString(),
group: OfferGroupId.ToString())));
// Start waiting for the server's confirmation
// of the above preparation action.
_purchaseIsPending = true;
// Start showing the purchase spinner or other indicator.
ShopUI.Instance.ShowPurchaseSpinner();
}
And augment the Update
method to wait for the server to confirm that the Offer has been assigned as the dynamic content for the IAP, and then initiate the purchase:
// ShopOfferScript.cs
public void Update()
{
PlayerModel player = MetaplayClient.PlayerModel;
MetaOfferGroupInfoBase offerGroupInfo = player.GameConfig.OfferGroups[OfferGroupId];
MetaOfferInfoBase offerInfo = player.GameConfig.Offers[OfferId];
// Poll the server's confirmation of the dynamic purchase content assignment.
// Note that this piece of code is executed regardless of whether the offer
// is currently active. The offer might have expired while we were waiting
// for the server's response, but that's ok - after the offer has been confirmed
// by the server, it doesn't need to be active anymore for the purchase to
// succeed.
if (_purchaseIsPending)
{
InAppProductId productId = offerInfo.InAppProduct.Ref.ProductId;
if (player.PendingDynamicPurchaseContents.TryGetValue(productId, out PendingDynamicPurchaseContent pendingContent)
&& pendingContent.Status == PendingDynamicPurchaseContentStatus.ConfirmedByServer)
{
// Initiate the actual IAP purchase, using whatever
// IAP management mechanism is normally used by the game.
// This should be the same mechanism as is used
// for any other Metaplay-validated purchases.
InitiatePurchase(productId);
// Stop tracking. Normal IAP validation flow will take it from here.
_purchaseIsPending = false;
}
}
... code that was already added earlier ...
}
The game needs to run a "refresh" action in order to activate new Offers. By default, Offers are refreshed only at the beginning of each game session. You'll probably want to refresh offers also during a session, such as whenever the player enters the shop UI:
// ShopScript.cs
void OnEnable()
{
// Player entered the shop; refresh offers.
// To avoid unnecessary workload on the server, only execute the refresh
// action if there's actually something to do, and only instruct it to
// refresh the offers that can be refreshed.
MetaOfferGroupsRefreshInfo refreshInfo = MetaplayClient.PlayerModel.GetMetaOfferGroupsRefreshInfo();
if (refreshInfo.HasAny())
MetaplayClient.PlayerContext.ExecuteAction(new PlayerRefreshMetaOffers(refreshInfo));
}
Refreshing only affects the activation of new Offers and Offer Groups. In contrast, the expiration of Offers is handled automatically and does not require refreshes.
⚠️ Note
If there are lots of configured Offer Groups, the evaluation of GetMetaOfferGroupsRefreshInfo
might be fairly expensive. For that reason, it is best to refresh only infrequently, such as in this example, rather than every frame.
The LiveOps Dashboard contains useful visualizations of Offers that will help in validating that they're functioning properly.
The Offers page shows all configured Offer Groups and Offers:
Clicking on View offer group will show detailed information about the Group:
On the Manage Player page, in the Segments & Targeting tab, an Offer Groups card shows the player-specific state of each Offer Group:
The example Offers
and OfferGroups
sheets in the Quick Guide use only a subset of the supported fields. The full sets of fields are described here.
Fields supported in Offers
:
OfferId
: The unique identifier of the Offer.
DisplayName
and Description
: Human-readable name and description strings shown in the LiveOps Dashboard.
InAppProduct
: A reference into the InAppProducts
sheet; specifies the SKU to use when purchasing the offer.
Rewards
: A list of MetaPlayerReward
s defining the contents of the Offer.
MaxActivations
: Total limit on how many times the Offer can be activated for a player. An offer is "activated" when the containing Offer Group is activated (i.e. is shown to the player) and also the Offer-specific conditions (if any) are fulfilled. Leave empty for no limit.
MaxPurchasesPerPlayer
: Total limit on how many times the Offer can be purchased by a player. This limit applies across all purchases of the Offer, even when it appears in multiple Offer Groups. Leave empty for no limit.
MaxPurchasesPerOfferGroup
: Total limit on how many times the Offer can be purchased via a single Offer Group. This limit applies across different activations of the same Offer Group, but does not apply across different Offer Groups. Leave empty for no limit.
MaxPurchasesPerActivation
: Limit on how many times the Offer can be purchased during a single activation. The per-activation purchase count resets when the activation of the Offer Group expires. Leave empty for no limit.
Segments
: The list of Player Segments to which this Offer is shown. If this list is non-empty, the player must belong to at least one of the Segments in order to be shown the Offer. If this list is empty, the Offer does not have Segment targeting.
PrecursorId
, PrecursorConsumed
, PrecursorDelay
: Used for defining dependencies from other Offers. With these, you can define "chains" of Offers: Initially, StarterPack1 is offered. Later, StarterPack2 is only offered if the player purchased StarterPack1.
The Precursor*
fields are parallel lists, and each (Id, Consumed, Delay)
tuple specifies a dependency on another Offer, identified by Id
, of the following form:
Id
must have been previously shown to the player...Consumed
is TRUE
) or wasn't (if Consumed
is FALSE
) purchased by the player...Delay
has elapsed since the it expired.IsSticky
: Whether the offer will remain available in the offer group if the offer's conditions become unfulfilled during the group's activation.
true
by default, meaning that as long as the offer's conditions were fulfilled when the offer group became active, the offer will remain available for the duration of the containing group's activation, even if the offer-specific conditions become unfulfilled in the meantime.false
, the offer will become unavailable if its offer-specific conditions (segments and additional dependencies) become unfulfilled while the offer is active.Fields supported in OfferGroups
:
GroupId
: The unique identifier of the Group.DisplayName
and Description
: Human-readable name and description strings shown in the LiveOps Dashboard.Placement
: The Placement id for the Group. At most one Group can be active at a time in each Placement. Other than that, the Offers feature does not impose further meaning on what a Placement means.Priority
: When multiple Groups are available for the same Placement, the one with the lowest Priority number is selected. Think of these as ordinal numbers in a priority ordering: 1 means "1st", 2 means "2nd", etc.Offers
: The list of Offers in this Group. Note that not all Offers are necessarily always shown when the Group is shown, in case the Offers have individual Segments
and Precursor
conditions.Segments
: The list of Player Segments to which this Group is shown. If this list is non-empty, the player must belong to at least one of the Segments in order to be shown the Group. If this list is empty, the Group does not have Segment targeting. Note that individual Offers can have targeting in addition to the Group; in order for an Offer to be shown, both the Group's and the Offer's targeting must be fulfilled.Lifetime
: How long a Group is shown until it expires, e.g. 5h
for 5 hours. If omitted, the lifetime follows the calendar schedule, if any; if lifetime and schedule are both omitted then there is no time-based expiration. Besides expiration, a Group will deactivate if all Offers in it are sold out.Cooldown
: How long must elapse after a Group has expired until it can be shown again. Empty means no cooldown.Schedule.TimeMode
: Either Utc
or Local
. Local
means the schedule obeys the player's local time.Schedule.Start.Date
and Schedule.Start.Time
: The start date and time of the first occasion of the schedule. For example Schedule.Start.Date
could be 2021-11-04
and Schedule.Start.Time
could be 15:00
.Schedule.Duration
: How long a single occasion lasts. For example 5h 30m
, i.e. 5 hours and 30 minutes. The supported units are y
, mo
, d
, h
, m
, and s
for years, months, days, hours, minutes, and seconds, respectively.Schedule.Recurrence
: Interval between occasions. For example 7d
, i.e. weekly. If the schedule should not repeat, leave this empty. The supported units are the same as for Schedule.Duration
, but here multiple units cannot be mixed.Schedule.NumRepeats
: For how many occasions the schedule repeats. Leave empty for no limit.MaxOffersActive
: The number of offers within this group that can be made active for a player simultaneously. When a player would be otherwise eligble for activating a larger number of offers, the set of active offers is limited by only activating the first MaxOffersActive
offers, in the order that they are declared in the Offers
list. Previously omitted offers can become active within the group if offers in the active set get deactivated during the offer group activation period, for example due to player no longer matching the segmentation criteria of a non-sticky offer.IncludeSoldOutOffers
: Whether to include sold-out offers when the offer group becomes active. true
by default, meaning that sold-out offers can still become active and count against the MaxOffersActive
limit, even though they're not purchasable. This is likely appropriate if you intend to still display sold-out items in the UI. However, if an offer group would contain only sold-out items, it will not be activated.false
, sold-out offers will not be included when the offer group becomes active. Note, however, that offers that become sold-out during the current activation can still remain active until the end of the activation.The core definitions for Offers and Offer Groups are enough to get you started, but you'll probably want to augment them with more game-specific data. For example, you might want to associate an icon with each Offer, and a theme color with each Offer Group.
To add custom fields in Offers' and Offer Groups' configs, you'll need to define a few custom classes inheriting from SDK-side base classes, and tell the SDK to use those custom classes instead of the SDK-side default implementations.
The following examples add an "Icon" field in the Offer configs, and a "ThemeColor" field in the Offer Group configs.
First, extend the Offer config classes:
// OfferInfo.cs
// This is the actual runtime config data class.
[MetaSerializableDerived(1)]
public class GameOfferInfo : MetaOfferInfoBase
{
// Example custom field.
[MetaMember(1)] public IconId Icon;
public GameOfferInfo(){ }
public GameOfferInfo(GameOfferSourceConfigItem source)
: base(source)
{
Icon = source.Icon;
}
}
// This is an intermediate class which is only used at config build time.
// Its purpose is to help in parsing the configs, since
// MetaOfferSourceConfigItemBase<> contains some fields that are non-trivially
// mapped into MetaOfferInfoBase.
public class GameOfferSourceConfigItem : MetaOfferSourceConfigItemBase<GameOfferInfo>
{
// Example custom field.
public IconId Icon;
public override GameOfferInfo ToConfigData(GameConfigBuildLog buildLog)
{
return new GameOfferInfo(this);
}
}
Then, the Offer Group config classes:
// OfferGroupInfo.cs
// Like in the previous snippet, this is the actual runtime config data class.
[MetaSerializableDerived(1)]
[MetaActivableConfigData("OfferGroup")]
public class GameOfferGroupInfo : MetaOfferGroupInfoBase
{
// Example custom field.
[MetaMember(1)] public Color ThemeColor;
public GameOfferGroupInfo(){ }
public GameOfferGroupInfo(GameOfferGroupSourceConfigItem source)
: base(source)
{
ThemeColor = source.ThemeColor;
}
}
// Like in the previous snippet, this is an intermediate class which is only
// used at config build time.
public class GameOfferGroupSourceConfigItem : MetaOfferGroupSourceConfigItemBase<GameOfferGroupInfo>
{
// Example custom field.
public Color ThemeColor;
public override GameOfferGroupInfo ToConfigData(GameConfigBuildLog buildLog)
{
return new GameOfferGroupInfo(this);
}
}
Then, modify your SharedGameConfig
to use these new config classes instead of the default ones:
public class SharedGameConfig : SharedGameConfigBase
{
[GameConfigEntry("Offers")]
[GameConfigEntryTransform(typeof(GameOfferSourceConfigItem))] // Previously, was DefaultMetaOfferSourceConfigItem
// Previously, item type was DefaultMetaOfferInfo
public GameConfigLibrary<MetaOfferId, GameOfferInfo> Offers { get; private set; }
[GameConfigEntry("OfferGroups")]
[GameConfigEntryTransform(typeof(GameOfferGroupSourceConfigItem))] // Previously, was DefaultMetaOfferGroupSourceConfigItem
// Previously, item type was DefaultMetaOfferGroupInfo
public GameConfigLibrary<MetaOfferGroupId, GameOfferGroupInfo> OfferGroups { get; private set; }
}
If you have a custom class derived from GameConfigBuild
that explicitly mentions the config item types, you should update it accordingly:
DefaultMetaOfferSourceConfigItem
by GameOfferSourceConfigItem
DefaultMetaOfferInfo
by GameOfferInfo
DefaultMetaOfferGroupSourceConfigItem
by GameOfferGroupSourceConfigItem
DefaultMetaOfferGroupInfo
by GameOfferGroupInfo
Finally, the type of PlayerModelBase.MetaOfferGroups
involves the concrete Offer Group config type, and thus also its type needs to be customized:
// OfferGroup.cs
// Here, the only things that differ from DefaultPlayerMetaOfferGroupsModel
// are the tag number for [MetaSerializableDerived], and the type parameter
// given to PlayerMetaOfferGroupsModelBase.
[MetaSerializableDerived(1)]
[MetaActivableSet("OfferGroup")]
public class PlayerGameOfferGroupsModel : PlayerMetaOfferGroupsModelBase<GameOfferGroupInfo>
{
protected override MetaOfferGroupModelBase CreateActivableState(GameOfferGroupInfo info, IPlayerModelBase player)
{
return new DefaultMetaOfferGroupModel(info);
}
protected override MetaOfferPerPlayerStateBase CreateOfferState(MetaOfferInfoBase offerInfo, IPlayerModelBase player)
{
return new DefaultMetaOfferPerPlayerState();
}
}
And that type needs to be passed to PlayerModelBase
when deriving it in the game-specific PlayerModel
:
// PlayerModel.cs
...
public class PlayerModel :
PlayerModelBase<
PlayerModel,
PlayerStatisticsCore,
// Add this. Previously, DefaultPlayerMetaOfferGroupsModel
// was being used as a default.
PlayerGameOfferGroupsModel
>
{
...
}
Now, you should be able to use the custom fields in the config sheets and access them in the game code. In some contexts, it may be necessary to cast from MetaOfferInfoBase
to GameOfferInfo
and from MetaOfferGroupInfoBase
to GameOfferGroupInfo
.
To customize the behavior of Offers and Offer Groups beyond what can be achieved with the built-in parameters, you can extend their state models and logic in a similar manner to how the configuration was extended in the previous section, Extending the Offer Classes.
The top-level type to customize is the type of PlayerModelBase.MetaOfferGroups
, which derives PlayerMetaOfferGroupsModelBase
and by default is of type DefaultPlayerMetaOfferGroupsModel
. The previous section Extending the Offer Classes shows how to customize it. Within that model, there are a few sub-models that can be customized as needed:
MetaOfferGroupModelBase
. Instances are created by the CreateActivableState
method of the top-level PlayerMetaOfferGroupsModelBase
-deriving class.MetaOfferPerPlayerStateBase
. Instances are created by the CreateOfferState
method of the top-level PlayerMetaOfferGroupsModelBase
-deriving class.MetaOfferPerGroupStateBase
. Instances are created by the CreateOfferState
method of the Offer Group model, i.e. the MetaOfferGroupModelBase
-deriving class.Removing Offer Groups or individual Offers in them from the game is done by removing the entries from the game config and publishing an updated config. The associated state for tracking the activations and purchases in the Player Model will become orphaned for removed Offer Groups. To protect against mishaps in game config publishing, the game server doesn't immediately remove this orphaned state from player models after a new config has been published, but internally retains the orphan state until a configurable delay period from the latest game config activation has passed. The delay is controlled by the Working with Runtime Options Player.PurgeStateForRemovedConfigItemsDelay
.
Offer Groups were added in R15, so if you're using an older version of the Metaplay SDK you might want to migrate your old offers over to benefit from the extra functionality that they provide. A possible path to do so could be along these lines:
Metaplay's Idler reference project contains an example of such migration code in LegacyShopOffers.cs
. Idler previously had a simple shop offer implementation which was later migrated to Metaplay SDK's Offers.
That said, your use case may be more complex than Idler's, so this approach may need to be adjusted as necessary. Don't hesitate to contact us for support!