NAV
Unity iOS Android

GameTune

GameTune enables you to use machine learning to optimize your game’s experience for each player in real time.

It comes with easy to use end-to-end integration so you don’t need to have resources to build and deploy your own ML systems. GameTune automatically handles things like data collection and transformation, as well as feature discovery and selection. In addition, GameTune independently trains, deploys, and operates its models.

GameTune is driven by your game’s data but also leverages Unity’s dataset from 1.5 billion devices, allowing you to optimize your game from the get-go. The data provided by Unity includes information such as player’s consumption and play patterns in other games.

GameTune can be used for various problems where the goal is to select the best alternative from A, B, C, … for each user. The best alternative is the one that maximizes retention, revenue, conversion or a custom game-specific reward. For example, game parameters, assets, IAP offers, or advertisement placements can be personalized to match individual preferences of each of your players.

GameTune‚Äôs optimizations are personalized and dynamic for each user. GameTune optimizes for different types of players continuously, in contrast to A/B testing which only finds the best alternative for the average user. It’s easier to setup and maintain than rules based segmentation systems.

How to use GameTune?

GameTune is easy to integrate: just add the SDK into your game and configure your optimization goals in the GameTune dashboard.

The GameTune SDK is required to make the integration with the GameTune service easier and more reliable. It offers:

Example use cases

Below is a list of example questions to help you understand how GameTune could be used in your game. Note that GameTune is not limited these use cases, they are simply meant to illustrate what has been working well in other games using GameTune. For technical implementation of some of these, see the examples section

Optimizing Retention

Level difficulty or other game parameters

Tutorial

Starter pack

Lootbox item or timer

Optimizing Revenue goals

IAP offer

Ad frequency or cool down period

Rewarded ads

Downloads

Unity games

The GameTune SDK supports 2018.3 and newer versions of the Unity Editor.

If you are using an older version of Unity, consider using SDK v2.0.0 for Unity which supports 2017.4 and newer

To import the GameTune SDK to your project:

GameTune Settings

Download the 2.6.0 GameTune SDK for Unity

non-Unity iOS games

Add UnityGameTune.framework to your project.

In your ViewController interface, import UnityGameTune.h:

#import <UnityGameTune/UnityGameTune.h>

Download the 2.6.0 GameTune SDK for iOS

In case your game does not show advertisements, it’s not advised to read a user’s advertising tracking id which by default is used for accessing a user’s Unity data. In this case you should use a special version of the SDK without this functionality:

non-Unity Android games

To import Unity GameTune SDK to your project:

Download the 2.6.0 GameTune SDK for Android

Initialization

GameTune SDK should be initialized when the game starts. GameTune.Initialize initializes the SDK internals and reports to the GameTune service that the user started the game, which is used to track user retention.

GameTune.Initialize(projectId, [initializeOptions], [eventListener], [userAttributesProvider]);
[UnityGameTune initialize:projectId];
UnityGameTune.initialize(activity, projectId);

Parameters

Due to legal requirements in some countries, GameTune may require user consent for game personalization and user tracking. You can initially set a user’s consent status by providing it as part of InitializeOptions. Alternatively, you can update the status by calling SetPrivacyConsent after GameTune was already initialized.

If you haven’t asked for the user’s consent, don’t call SetPrivacyConsent at all. The default is to determine the behaviour based on user’s location.

To set privacy consent with Initialize:

InitializeOptions options = new InitializeOptions();
options.SetPrivacyConsent(true);   // if you asked for the user's consent and they provided it
options.SetPrivacyConsent(false);  // if you asked for the user's consent and they denied it
GameTune.Initialize(projectId, options);
UGTInitializeOptions *options = [[UGTInitializeOptions alloc] init];
[options setPrivacyConsent:YES];  // if you asked for the user's consent and they provided it
[options setPrivacyConsent:NO];  // if you asked for the user's consent and they denied it
[UnityGameTune initialize:projectId withOptions:options];
InitializeOptions options = new InitializeOptions();
options.setPrivacyConsent(true);   // if you asked for the user's consent and they provided it
options.setPrivacyConsent(false);  // if you asked for the user's consent and they denied it
UnityGameTune.initialize(activity, projectId, options);

Update user’s privacy consent status after initialization by calling GameTune.SetPrivacyConsent.

Update privacy consent:

GameTune.SetPrivacyConsent(true);
GameTune.SetPrivacyConsent(false);
[UnityGameTune setPrivacyConsent:YES];
[UnityGameTune setPrivacyConsent:NO];
UnityGameTune.setPrivacyConsent(true);
UnityGameTune.setPrivacyConsent(false);

GameTune Off

You can exclude a segment of users from exposure to GameTune by setting GameTuneOff flag in initialize options. This may be useful for A/B testing purposes. For example, if you want to test the performance of GameTune, you can have one user group with GameTune enabled and one with GameTune disabled.

When GameTuneOff is set to true, SDK skips all network calls to GameTune for the user and returns Control Alternative to all questions.

When GameTuneOff is set to false, SDK functions normally.

InitializeOptions options = new InitializeOptions();
options.SetGameTunefOff(true);
GameTune.Initialize(projectId, options);
UGTInitializeOptions *options = [[UGTInitializeOptions alloc] init];
[options setGameTunefOff:YES];
[UnityGameTune initialize:projectId withOptions:options];
InitializeOptions options = new InitializeOptions();
options.setGameTuneOff(true);
UnityGameTune.initialize(activity, projectId, options);

Questions

Interaction with GameTune works with the Question -> Answer model. You ask a Question and get a machine learning optimized Answer back. In the Question you define what the alternatives, i.e. possible answers, are. GameTune will then select the best alternative for each user.

Creating questions

Question is an object that consists of these properties:

In order to start receiving ML optimized answers from GameTune, you have to configure your Question and the optimization goal in the GameTune dashboard.

Question buttonText = GameTune.CreateQuestion(
    "button_text",
    new string[]{ "Start", "Begin", "Play" },
    SetButtonText
);

void SetButtonText(Answer answer)
{
    string buttonText = answer.Value;
}

Question levelDifficulty = GameTune.CreateQuestion(
    "level_difficulty",
    new string[]{ "easy", "medium", "hard" },
    AnswerType.ALWAYS_NEW,
    SetLevelDifficulty
);

void SetLevelDifficulty(Answer answer)
{
    string difficulty = answer.Value;
}
UGTQuestion *buttonText = [UnityGameTune createQuestion:@"button_text" alternatives:@[@"Start", @"Begin", @"Play"] handler:^(UGTAnswer *answer) {
    [self setButtonText:answer.chosenAlternative.name];
}];

UGTQuestion *levelDifficulty = [UnityGameTune createQuestion:@"level_difficulty" alternatives:@[@"easy", @"medium", @"hard"] answerType:kUnityGameTuneAnswerTypeAlwaysNew handler:^(UGTAnswer *answer) {
    [self setLevelDifficulty:answer.chosenAlternative.name];
}];
Question buttonText = UnityGameTune.createQuestion(
        "button_text",
        new String[]{"Start", "Begin", "Play"},
        (Answer answer) -> setLevelDifficulty(answer.getValue())
);

Question levelDifficulty = UnityGameTune.createQuestion(
        "level_difficulty",
        new String[]{"easy", "medium", "hard"},
        AnswerType.ALWAYS_NEW,
        (Answer answer) -> setLevelDifficulty(answer.getValue())
);

Treatment groups

GameTune assigns users into different treatment groups. The size of each treatment group can be controlled in the GameTune dashboard. This can be changed at any time, even after the launch of the Question.

Control Alternative

The first alternative (“easy” in this example) is treated as the control alternative, and it will be returned by GameTune in the following cases:

Answer Type

Answer type is an optional argument to CreateQuestion that can be either of the two enumerators of AnswerType:

Asking questions

A personalized answer is received from the GameTune service by passing your Questions to the AskQuestions method. The GameTune SDK makes an HTTP request to the GameTune service, invoking the callbacks of Questions (calling SetLevelDifficulty in the example above) with the received Answer.

The latency of getting answers depends on the network connection, ask questions in advance to avoid users waiting for the answer.

AskQuestions supports asking multiple questions at once, e.g. buttonLabel and levelDifficulty in the example are combined into one network request.

GameTune.AskQuestions(levelDifficulty, buttonLabel);
[UnityGameTune askQuestions:levelDifficulty, buttonLabel, nil];
UnityGameTune.askQuestions(levelDifficulty, buttonLabel);

Answer value

Value is the name of the selected Alternative for the current user.

answer.Value;
answer.chosenAlternative.name;
answer.getValue();

Answer ID

Answer ID a unique ID (UUID) for every Answer.

answer.Id;
answer.identifier;
answer.getId();

Answer’s treatment group

TreatmentGroup defines how an Answer was selected for the current user. This can be used, for instance, for tagging the user for measuring performance. Possible values are:

Value Description
ml The user received an answer optimized by the GameTune ML algorithm.
control The user in this group got the Control Alternative.
exploration The user received a randomly selected answer.
restricted The user has limited ad tracking enabled.
gdpr The user is from EU and is GDPR restricted, and consent is unknown or denied
”” Empty string signals that error occurred and an answer was not picked by GameTune.

Treatment group can be read from the answer:

answer.TreatmentGroup;
answer.treatmentGroup;
answer.getTreatmentGroup();

Answer’s model name and version

ModelName and ModelVersion contain the name and version of GameTune’s machine learning model that was used to make the decision. Model information can be used for comparing individual model performance in your internal analytics, but usually these can be ignored. These are available only when TreatmentGroup is ml.

answer.ModelName;
answer.ModelVersion;
answer.modelName;
answer.modelVersion
answer.getModelName();
answer.getModelVersion();

Ask questions timeout

Passing a timeout value for AskQuestions in the initialize options, will guarantee a response within that timeout. The timeout is set in milliseconds. If the response takes longer than that, e.g. due to bad network conditions, Control Alternative will be returned to the user.

Default timeout is 5000 milliseconds.

InitializeOptions options = new InitializeOptions();
options.SetAskQuestionsTimeout(2000);
UGTInitializeOptions *options = [[UGTInitializeOptions alloc] init];
[options setAskQuestionsTimeout:2000];
InitializeOptions options = new InitializeOptions();
options.setAskQuestionsTimeout(2000);

Test mode

If test mode is enabled for a device, all data sent from that device is excluded from model training. This allows you to ignore data that might skew the model, for instance when running automated tests.

Test mode is disabled by default. GameTune returns Control Alternative in test mode.

InitializeOptions options = new InitializeOptions();
options.SetTestMode(true);
UGTInitializeOptions *options = [[UGTInitializeOptions alloc] init];
[options setTestMode:YES];
InitializeOptions options = new InitializeOptions();
options.setTestMode(true);

Alternative attributes

Alternative attributes are optional but useful for GameTune to learn underlying similarities and other associations between the alternatives. These could include e.g. item price, weapon damage, or item alignment.

You can set the attributes by creating Alternative objects and pass them as an array to the CreateQuestion method. For example:

var easy = new Alternative("easy", new Dictionary<string, object>()
{
    { "grunt_count", 10 },
    { "ogre_count", "1" },
    { "special_items_allowed", true }
});

var medium = new Alternative("medium", new Dictionary<string, object>()
{
    { "grunt_count", 15 },
    { "ogre_count", "2" },
    { "special_items_allowed", true }
});

var hard = new Alternative("hard", new Dictionary<string, object>()
{
    { "grunt_count", 20 },
    { "ogre_count", "3" },
    { "special_items_allowed", false }
});

Question levelDifficulty = GameTune.CreateQuestion(
    "level_difficulty",
    new Alternative[] { easy, medium, hard },
    SetLevelDifficulty
);
UGTAlternative *easy = [[UGTAlternative alloc] initWithName:@"easy" andAttributes:@{ @"grunt_count": [NSNumber numberWithInteger:10], @"ogre_count": [NSNumber numberWithInteger:1], @"special_items_allowed": @"true" }];
UGTAlternative *medium = [[UGTAlternative alloc] initWithName:@"medium" andAttributes:@{ @"grunt_count": [NSNumber numberWithInteger:15], @"ogre_count": [NSNumber numberWithInteger:2], @"special_items_allowed": @"true" }];
UGTAlternative *hard = [[UGTAlternative alloc] initWithName:@"hard" andAttributes:@{ @"grunt_count": [NSNumber numberWithInteger:20], @"ogre_count": [NSNumber numberWithInteger:3], @"special_items_allowed": @"false" }];

UGTQuestion *levelDifficulty = [UnityGameTune createQuestion:@"level_difficulty" alternatives:@[easy, medium, hard] handler:^(UGTAnswer *answer) {
    [self setLevelDifficulty:answer.chosenAlternative.name];
}];
Map<String, Object> easyDiffAttributes = new HashMap<>();
tutorialEasyAttributes.put("grunt_count", 10);
tutorialEasyAttributes.put("ogre_count", "1");
tutorialEasyAttributes.put("special_items_allowed", true);
Alternative easy = new Alternative("easy", easyDiffAttributes);

Map<String, Object> mediumDiffAttributes = new HashMap<>();
tutorialMediumAttributes.put("grunt_count", 15);
tutorialMediumAttributes.put("ogre_count", "2");
tutorialMediumAttributes.put("special_items_allowed", true);
Alternative medium = new Alternative("medium", mediumDiffAttributes);

Map<String, Object> hardDiffAttributes = new HashMap<>();
tutorialMediumAttributes.put("grunt_count", 20);
tutorialMediumAttributes.put("ogre_count", "3");
tutorialMediumAttributes.put("special_items_allowed", false);
Alternative hard = new Alternative("hard", hardDiffAttributes);

Question levelDifficulty = UnityGameTune.createQuestion(
        "level_difficulty",
        new Alternative[]{easy, medium, hard},
        (Answer answer) -> setLevelDifficulty(answer.getValue())
);

Events

In order to understand player’s behaviour related to the chosen alternative for a Question, GameTune requires sending Use and Reward events at appropriate times.

Gametune feedback loop

These events are required in correct order to have a functional GameTune feedback loop for the machine learning model to learn about your players:

Feedback loop

As more events flow through the system, the model is trained again with more data and it becomes more accurate. For model to be trained, a minimum number of 1000 used Questions and 100 Rewards is required.

Use

Use must be called once the Answer is changing the game. That is, the selected Answer was actually displayed to the user and one or multiple Reward events are expected from the user. Use enables GameTune to learn if a user has seen the part of the game that was changed as a result for asking the Question. This allows GameTune to only include data from users that were affected by a Question in the model training set.

answer.Use();
[answer use];
answer.use();

When to call Use?

For example if asking a level difficulty Question, call Use when user started the level i.e. the Answer changed the difficulty of the level. If the game is not affected by the answer, Use should not be called. E.g. if you ask a question about button color when the game starts, but the player never sees the button.

If it depends on a Question’s alternatives when Use should be called, the Answer should be used at the earliest moment when the game has the possibility to change. For example, if a Question is about whether to show an achievement reward to the user after 2, 3 or 5 levels played, Use should be called after the user has played 2 levels regardless of which Answer the user got.

Using an answer without a reference

Sometimes you may not have a reference to the Answer object you want to Use, which may happen, for instance, if an Answer is used in a different game session. In this case you can call the Use method on the question itself, providing the chosen Alternative name as a parameter. Then the SDK can internally map it to the latest matching answer.

Question levelDifficulty = GameTune.CreateQuestion(
    "level_difficulty",
    new string[]{ "easy", "medium", "hard" },
    AnswerType.ALWAYS_NEW,
    SetLevelDifficulty
);

levelDifficulty.Use("easy");
UGTQuestion *levelDifficulty = [UnityGameTune createQuestion:@"level_difficulty" alternatives:@[@"easy", @"medium", @"hard"] answerType:kUnityGameTuneAnswerTypeAlwaysNew handler:^(UGTAnswer *answer) {
    [self setLevelDifficulty:answer.chosenAlternative.name];
}];
[levelDifficulty use:@"easy"];
Question levelDifficulty = UnityGameTune.createQuestion(
        "level_difficulty",
        new String[]{"easy", "medium", "hard"},
        AnswerType.ALWAYS_NEW,
        (Answer answer) -> setLevelDifficulty(answer.getValue())
);

levelDifficulty.use("easy");

Reward

Reward event is used for reporting that a user complete the desired action of the optimization target of the question. GameTune learns to maximize your optimization target using data from Rewards and their attributes. The reward is connected to a Question in the GameTune dashboard.

Once you’ve configured the Question, you can copy and paste the code snippet from the dashboard to your game code and modify it to meet your needs. Make sure the reward names and attributes between dashboard and game code are in sync.

Retention reward is collected automatically when Initialize is called, thus sending a Reward is not required with retention targets.

See the use case examples of what kind of rewards are expected.

Optimization targets

Optimization target for each question is defined in the GameTune dashboard. When you create a new question, select one of the optimization target presets.

Configure reward events

Click on “Reward Configuration” and make sure that reward names matches to what you have in your game code. Depending on your optimization strategy, you may need to add attribute to the reward that describes its value. For example in a case where your reward includes a price:

var attributes = new Dictionary<string, object>() { { "value", 99 } };
GameTune.RewardEvent("purchased", attributes);

User attributes

The GameTune SDK automatically collects information about the player and the device. In addition, you should provide information of what the user has done inside your game through user attributes. With user attributes GameTune can make better decisions for your users.

User attributes describe the player’s current state in the game and what they have done in the past. It should have information that may help GameTune to differentiate players from each other and that may be relevant for the question asked.

User attributes are a collection of key-value pairs, where values may be numbers, booleans or strings.

Defining user attributes

Implement a user attributes provider, which should make sure the user attributes represent the user’s current state.

class MyUserAttributesProvider : IUserAttributesProvider {
    public Dictionary<string, object> GetUserAttributes() {
        Dictionary<string, object> attributes = new Dictionary<string, object>()
        {
            { "coins", 3500 },
            { "level", 323 },
            { "deaths", 257 },
            { "ua_source": "unity_ads"},
            { "win_lose_ratio", 1.23 }
        }
        return attributes;
    }
}

InitializeOptions options = new InitializeOptions();
IUserAttributesProvider userAttributesProvider = new MyUserAttributesProvider();
GameTune.Initialize(projectId, options, null, userAttributesProvider);
@interface ViewController : UIViewController<UnityAdsDelegate, UGTEventListener, UGTUserAttributesProvider>()
@end

@implementation ViewController

// ...

- (NSDictionary *)getUserAttributes {
    NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];

    // Populate user attributes

    return attributes;
}

// ...

@end

- (IBAction)initializeButtonTapped:(id)sender {
    UGTInitializeOptions *options = [[UGTInitializeOptions alloc] init];
    [UnityGameTune initialize:projectId withOptions:options withEventListener:self withUserAttributesProvider:self];
}
class MyUserAttributesProvider implements IUnityGameTuneUserAttributesProvider {
    public Map<String, Object> getUserAttributes() {
        Map<String, Object> userAttributes = new HashMap<>();

        // Populate user attributes

        return userAttributes;
    }
}

InitializeOptions options = new InitializeOptions();
IUnityGameTuneUserAttributesProvider userAttributesProvider = new MyUserAttributesProvider();
UnityGameTune.initialize(activity, projectId, options, null, userAttributesProvider);

Setting user attributes manually

This method of updating user attributes is deprecated. User attributes should be set before:

Set multiple attributes

Dictionary<string, object> userAttributes = new Dictionary<string, object>()
{
    { "coins", 3500 },
    { "kills", 323 },
    { "deaths", 257 },
    { "win_lose_ratio", 1.23 }
};

GameTune.SetUserAttributes(userAttributes);
NSMutableDictionary *userAttributes = [[NSMutableDictionary alloc] init];
[userAttributes setValue:[NSNumber numberWithInteger:3500] forKey:@"coins"];
[userAttributes setValue:[NSNumber numberWithInteger:323] forKey:@"kills"];
[userAttributes setValue:[NSNumber numberWithInteger:257] forKey:@"deaths"];
[userAttributes setValue:[NSNumber numberWithDouble:1.23] forKey:@"win_lose_ratio"];
[UnityGameTune setUserAttributes:userAttributes];
Map<String, Object> userAttributes = new HashMap<String, Object>();
userAttributes.put("coins", 3500);
userAttributes.put("kills", 323);
userAttributes.put("deaths", 257);
userAttributes.put("win_lose_ration", 1.3);

UnityGameTune.setUserAttributes(userAttributes);

Or add each Key-Value pair separately

GameTune.SetUserAttribute("total_xp", 24550);
GameTune.SetUserAttribute("control_scheme", "tactical");
GameTune.SetUserAttribute("fb_login", false);
[UnityGameTune setUserAttribute:[NSNumber numberWithInteger:24550] forKey:@"total_xp"];
[UnityGameTune setUserAttribute:@"control_scheme" forKey:@"tactical"];
[UnityGameTune setUserAttribute:@"false" forKey:@"fb_login"];
UnityGameTune.setUserAttribute("total_xp", 24550);
UnityGameTune.setUserAttribute("control_scheme", "tactical");
UnityGameTune.setUserAttribute("fb_login", false);

Examples of useful user attributes

describe the player, how they’ve played

in-game resources

describe how good the player is, how they play

what you know about the player from other sources & social

User data that GameTune automatically collects

GameTune automatically collects these attributes about the user:

Analytics

You may want to store information about GameTune related events with analytics service such as DeltaDNA.

To make it easy and avoid potential discrepancies in the data, you can implement Event Listener and define it when initializing GameTune SDK. In the callbacks you can forward the GameTune event data into your own systems.

public interface IEventListener
{
    void OnAppStartEvent(AppStartEvent appStartEvent);
    void OnUseEvent(UseEvent useEvent);
    void OnRewardEvent(RewardEvent rewardEvent);
    void OnQuestionEvent(QuestionEvent questionEvent);
}
@protocol UGTEventListener<NSObject>

- (void)onAppStartEvent:(UGTAppStartEvent *)appStartEvent;

- (void)onUseEvent:(UGTUseEvent *)useEvent;

- (void)onRewardEvent:(UGTRewardEvent *)rewardEvent;

- (void)onQuestionEvent:(UGTQuestionEvent *)questionEvent;

@end
public interface IUnityGameTuneEventListener {

    void onAppStartEvent(AppStartEvent appStartEvent);
    void onUseEvent(UseEvent useEvent);
    void onRewardEvent(RewardEvent rewardEvent);
    void onQuestionEvent(QuestionEvent questionEvent);
}

Event Listener callback methods are invoked when an event has succesfully reached GameTune service. There are four types of events.

OnAppStartEvent

OnAppStartEvent is called when GameTune SDK has been initialized.

Properties:

OnUseEvent

OnUseEvent is called when answer is used and:

Properties of UseEvent:

OnRewardEvent

OnRewardEvent is called every time the Reward is sent.

Properties of RewardEvent:

OnQuestionEvent

OnQuestionEvent is called when Question is asked.

Properties of QuestionEvent:

Examples

Game speed

While some players like to start a game easy, others prefer to be challenged from the get-go. By using GameTune and its access to Unity data about the new player, we can select the optimal first time experience for each user.

1. Configure the Question

Go to the GameTune dashboard and add the Question. Select “Retention, day 1” as the optimization target.

In the game, the current version of game speed settings is called medium, which is added as Control Alternative. Add a new Alternative fast, which is meant for players that are familiar with the genre. Also add slow, which makes the game play easy and slow.

2. Initialize GameTune

GameTune is initialized when the game starts:

using UnityEngine.GameTune;
GameTune.Initialize(projectId);

This signals to GameTune that the player played the game, so GameTune gets user retention information.

3. Ask the Question

Next the Question and its Alternatives are defined and asked:

  Question speed = GameTune.CreateQuestion (
      "speed",
      new string[]{ "medium", "fast", "slow" },
      SpeedAnswerHandler // implemented in a later step
  );
  GameTune.AskQuestions(speed);

Notice that here “medium” was listed first. That means that “medium” is the Control Alternative player gets e.g. if they don’t have network or have opted out for personalization.

4. Handle the Answer

Next SpeedAnswerHandler is implemented, which contains the actual logic for selecting the speed for the level:

  private void SpeedAnswerHandler(Answer answer)
  {
      // do something with answer.Value (either the 'fast', 'medium' or 'slow')
  }

5. Use the answer

GameTune needs to know if the user started to play the level or did they churn before the Answer changed the game. This is done by using the answer:

answer.Use();

6. Release the game

After the game with GameTune integration is released, GameTune doesn’t yet have any data about how different speeds work. Until enough data is collected, GameTune randomly selects which speed to use - this is called exploration. Once enough data is collected, a machine learning model is trained automatically and the optimization starts.

As more users play the game, the ML model is continuously trained with more data. The model becomes more accurate and learns to select the optimal level difficulty for users with similar attributes.

In-app Purchase offer

The game has coins that players can use to buy in-game items. There’s three available special deals: the user can purchase either 100 coins for $0.99, 500 coins for $2.99 or 1000 coins for $4.99. There’s a modal that is displayed to promote one of these deals. But which one of these special deals should be shown to the user?

1. Configure the Question

GameTune can be used for selecting the optimal offer that leads to more dollars spent per user. In the GameTune dashboard, create new question with optimization target set to “IAP optimization”. That means that GameTune optimizes for conversion of purchase first, and if user has bought something already then to maximize revenue. Internally, GameTune optimizes the probability of conversion multiplied by the value specified in the alternative attribute.

2. Ask the Question

Define the Alternatives and the Question:

var smallOffer = new Alternative("small", new Dictionary<string, object>()
{
    { "price", 0.99 },
    { "coins", 100 },
});
var midOffer = new Alternative("mid", new Dictionary<string, object>()
{
    { "price", 2.99 },
    { "coins", 500 },
});
var largeOffer = new Alternative("large", new Dictionary<string, object>()
{
    { "price", 4.99 },
    { "coins", 1000 },
});
Question offer = GameTune.CreateQuestion (
    "iap_offer",
    new Alternative[] { smallOffer, midOffer, largeOffer },
    OfferAnswerHandler
);

After this, the Question is asked before the offer modal is about to be shown:

GameTune.AskQuestions(offer);

3. Handle offer answer

OfferAnswerHandler should change the game based on the Answer:

  private void OfferAnswerHandler(Answer answer)
  {
    // Offer to be shown should be equal to answer.Value

    // 1. Show the modal

    // 2.After modal is shown, call answer.Use().
    answer.Use()
  }

4. Send the reward

If the user purchases the offer, send a Reward event:

// if user purchases the 0.99 offer
var attributes = new Dictionary<string, object>() { { "price", 0.99 } };
GameTune.RewardEvent("purchase", attributes);

This way GameTune can learn which offer is optimal for each user to maximize revenue per user.

Maximizing ad impressions

GameTune can be utilized to balance churn and revenue from displaying ads. This could be done for example by choosing how often an interstitial advertisement is shown.

1. Configure the Question in the dashboard

Go to the GameTune dashboard and add the Question. Select “Ad impressions” as the optimization goal. Define the alternatives determining after how many levels to show an ad: every, every_3rd and every_5th.

2. Ask the Question

Copy and paste the code from the dashboard’s “Ask Question” instructions to your game code:

  Question adFrequency = GameTune.CreateQuestion (
      "ad_frequency",
      new string[]{ "every", "every_3rd", "every_5th" },
      MyAnswerHandler
  );

Ad frequency question is asked every time the user starts the game. Use should be called after every level, as that is the first moment when the game would change with the most aggressive frequency. Implement the answer handled as explained in the previous examples.

3. Send the reward

Reward should be sent once the user watches an ad. This way GameTune learns to select the alternative that maximizes the number of ad watches over 7 days. GameTune also learns to minimize churn, as a user churning would lead to no more rewards from that user.

GameTune.RewardEvent("interstitial_ad_watched");

Customize UI

There are different variations of the in-game notification promoting a new game feature. GameTune can be used to optimize for conversion of clicking the notification. The optimization goal is selected as “Conversion” in the GameTune dashboard.

1. Ask the question

There’s different combinations of the copy text and positioning of the notification. The different alternatives of the offer are defined:

var trNewCopy = new Alternative("tr_new", new Dictionary<string, object>()
{
    { "position", "top-right" },
    { "copy", "new" },
});
var trOldCopy = new Alternative("tr_old", new Dictionary<string, object>()
{
    { "position", "top-right" },
    { "copy", "old" },
});
var brNewCopy = new Alternative("br_new", new Dictionary<string, object>()
{
    { "position", "bottom-right" },
    { "copy", "new" },
});
var brOldCopy = new Alternative("br_old", new Dictionary<string, object>()
{
    { "position", "bottom-right" },
    { "copy", "old" },
});

Question notification = GameTune.CreateQuestion (
    "notification",
    new Alternative[] { trOldCopy, trNewCopy, brNewCopy, brOldCopy },
    MyAnswerHandler
);

The question of which notification to show to the user is asked when the game starts. Thus it can be combined with the difficulty question:

GameTune.AskQuestions(difficulty, notification);

2. Handle the Answer

MyAnswerHandler should be implemented:

private void MyAnswerHandler(Answer answer)
{
  // Notification to be shown should be equal to answer.Value, for example "br_new".
}

3. Use the Answer

When the notification is shown to the user, call answer.Use(), so GameTune knows that the answer was actually displayed to the user.

answer.Use();

4. Send the reward

If the user clicks on the notification, a reward should be sent:

GameTune.RewardEvent("notification_clicked");

By sending these events, GameTune will learn to select the optimal notification for each user, and maximizes the conversion of opening the notification.

Full example code

Full example on how to use GameTune

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.GameTune;

public class LevelManager : MonoBehaviour, IUserAttributesProvider {

    private const string UnityProjectId = "Your unity project id here";
    private Question _levelDifficultyQuestion;
    private Question _buttonLabelQuestion;
    private Answer _levelDifficultyAnswer;
    private string _levelDifficulty;

    private int _levelsStarted;

    // button that executes StartLevel()
    public Button PlayButton;
    public Text PlayButtonText;

    void Start ()
    {
        PlayButton.interactable = false;

        _levelDifficultyQuestion = GameTune.CreateQuestion(
            "levelDifficulty",
            new string[]{ "easy", "medium", "hard" },
            LevelDifficultyAnswerHandler
        );

        _buttonLabelQuestion = GameTune.CreateQuestion(
            "buttonLabel",
            new string[]{ "Start", "Begin", "Play" },
            ButtonLabelAnswerHandler
        );

        Dictionary<string, object> userAttributes = new Dictionary<string, object>()
        {
            { "coins", 3500 },
            { "kills", 323 },
            { "deaths", 257 },
            { "win_lose_ratio", 1.23 },
            { "fb_login", false },
            { "control_scheme", "tactical" }
        };

        GameTune.SetUserAttributes(userAttributes);

        InitializeOptions initOptions = new InitializeOptions();
        initOptions.SetPrivacyConsent(true);

        GameTune.Initialize(UnityProjectId, initOptions, null, this);
        GameTune.AskQuestions(_levelDifficultyQuestion, _buttonLabelQuestion);
    }

    private void LevelDifficultyAnswerHandler(Answer answer)
    {
        _levelDifficultyAnswer = answer;
        _levelDifficulty = "difficulty_" + _levelDifficultyAnswer.Value;
    }

    private void ButtonLabelAnswerHandler(Answer answer)
    {
        PlayButtonText.text = answer.Value;

        answer.Use();
        PlayButton.interactable = true;
    }

    public void StartLevel()
    {
        // update levels started attribute. Will be used when asking questions next time.
        _levelsStarted += 1;

        if (_levelDifficultyAnswer != null)
        {
            // set level difficulty
            // ...
            // level difficulty set according to the answer, notifying GameTune
            _levelDifficultyAnswer.Use();
        }

        SceneManager.LoadScene("level10");
    }

    public Dictionary<string, object> GetUserAttributes()
    {
        Dictionary<string, object> userAttributes = new Dictionary<string, object>()
        {
            { "levels_started", _levelsStarted }
        };

        return userAttributes;
    }
}
// .h file
#import <UIKit/UIKit.h>
#import <UnityGameTune/UnityGameTune.h>

@interface ViewController : UIViewController<UGTUserAttributesProvider>

@end
// .m file
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UGTQuestion *buttonLabel;
@property (nonatomic, strong) UGTQuestion *levelDifficulty;
@property (nonatomic, strong) UGTAnswer *buttonLabelAnswer;
@property (nonatomic, strong) UGTAnswer *levelDifficultyAnswer;

@property (weak, nonatomic) IBOutlet UIButton *startButton;

@end

static int _levelsStarted;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Create questions. Do this only once for each question.
    self.buttonLabel = [UnityGameTune createQuestion:@"button_label" alternatives:@[@"Start", @"Begin", @"Play"] handler:^(UGTAnswer *answer) {
        self.buttonLabelAnswer = answer;
        // set the button label, and send use event
        [self.startButton setTitle:answer.chosenAlternative.name forState:UIControlStateNormal];
        [self.buttonLabelAnswer use];
    }];
    self.levelDifficulty = [UnityGameTune createQuestion:@"level_difficulty" alternatives:@[@"easy", @"medium", @"hard"] handler:^(UGTAnswer *answer) {
        // Save answer for later use
        self.levelDifficultyAnswer = answer;
    }];

    // Set user attributes
    NSMutableDictionary *userAttributes = [[NSMutableDictionary alloc] init];
    [userAttributes setValue:[NSNumber numberWithInteger:3500] forKey:@"coins"];
    [userAttributes setValue:[NSNumber numberWithInteger:323] forKey:@"kills"];
    [userAttributes setValue:[NSNumber numberWithInteger:257] forKey:@"deaths"];
    [userAttributes setValue:[NSNumber numberWithDouble:1.23] forKey:@"win_lose_ratio"];
    [userAttributes setValue:[NSNumber numberWithBool:NO] forKey:@"fb_login"];
    [userAttributes setValue:@"tactical" forKey:@"control_scheme"];
    [UnityGameTune setUserAttributes:userAttributes];

    // Initialize
    [UnityGameTune initialize:@"your unity project id here"];

    // Ask questions. Will get answers once GameTune is initialized.
    [UnityGameTune askQuestions:self.buttonLabel, self.levelDifficulty, nil];
}

- (IBAction)buttonTapped:(id)sender {
    // Use levelDifficultyAnswer to start level
    [self startLevel:self.levelDifficultyAnswer.chosenAlternative.name];
}

- (void)startLevel:(NSString *)difficulty {
    // Update levels started attribute
    _levelsStarted += 1;

    // Send use event for levelDifficultyAnswer
    [self.levelDifficultyAnswer use];

    // start the level with difficulty
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (NSDictionary *)getUserAttributes {
    NSMutableDictionary *userAttributes = [[NSMutableDictionary alloc] init];
    [userAttributes setValue:[NSNumber numberWithInteger:_levelsStarted] forKey:@"levels_started"];

    return userAttributes;
}

@end
package com.unity3d.gametune.app;

import com.unity3d.gametune.Answer;
import com.unity3d.gametune.Question;
import com.unity3d.gametune.UnityGameTune;
import com.unity3d.gametune.misc.Utilities;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.Button;

public class UnityGameTuneApp extends Activity implements IUnityGameTuneUserAttributesProvider {
    final private String unityProjectId = "your unity project id here";

    private Answer levelDifficultyAnswer;
    private Answer buttonLabelAnswer;
    private int levelsStarted = 0;

    // setting Alternatives for Questions
    final private String[] levelDifficultyAlternatives = new String[] {
        "short",
        "medium",
        "hard"
    };

    final private String[] buttonLabelAlternatives = new String[] {
        "Start",
        "Begin",
        "Play"
    };

    // setting Questions
    private Question levelDifficulty = UnityGameTune.createQuestion("levelDifficulty",
        levelDifficultyAlternatives, (Answer answer) -> handleDifficulty(answer));

    private Question buttonLabel = UnityGameTune.createQuestion("buttonLabel",
        buttonLabelAlternatives, (Answer answer) -> handleButton(answer));

    // setting Answer handlers for Questions
    private void handleDifficulty(Answer answer) {
        levelDifficultyAnswer = answer;
    }

    private void handleButton(Answer answer) {
        buttonLabelAnswer = answer;
        Button levelStartButton = (Button) findViewById(R.id.unity_start_button);

        // setting button label according to the answer from GameTune
        levelStartButton.setText(buttonLabelAnswer.getValue());

        Utilities.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                levelStartButton.setEnabled(true);
            }
        });

        // button label changed according to the answer, notifying GameTune
        buttonLabelAnswer.use();

        levelStartButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // user clicked the button, sending reward event
                UnityGameTune.rewardEvent("button_clicked");
                startLevel(levelDifficultyAnswer);
            }
        });
    }

    private void startLevel(Answer difficultyAnswer) {
        // checking if answer actually exists
        if (difficultyAnswer != null) {
            // do something that determines level difficulty
            // ...
            difficultyAnswer.use();
            levelsStarted += 1;
        } else {
            // do something that determines default level difficulty
            // ...
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.unity_gametune_test_layout);

        InitializeOptions initOptions = new InitializeOptions();
        initOptions.setPrivacyConsent(true);

        // initializing GameTune SDK and asking Questions
        UnityGameTune.initialize(this, unityProjectId, initOptions, null, this);

        Map<String, Object> userAttributes = new HashMap<String, Object>();
        userAttributes.put("coins", 3500);
        userAttributes.put("kills", 323);
        userAttributes.put("deaths", 257);
        userAttributes.put("win_lose_ration", 1.3);
        userAttributes.put("fb_login", false);
        userAttributes.put("control_scheme", "tactical");
        UnityGameTune.setUserAttributes(userAttributes);

        UnityGameTune.askQuestions(levelDifficulty, buttonLabel);
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    public Map<String, Object> getUserAttributes() {
        Map<String, Object> userAttributes = new HashMap<>();

        userAttributes.put("levels_started", levelsStarted);

        return userAttributes;
    }
}

Unity example game

Modified version of the familiar EndlessRunnerSampleGame and the guide on how GameTune can be integrated into a game like this.

Example game - a GitHub repository containing the game’s source code

Integration guide - a wiki page containing the iformation about the approach and technical details of GameTune integration

Release notes

v2.6.0

v2.5.0

v2.4.2

v2.4.0

v2.3.1

v2.3.0

v2.2.0

v2.1.2

v2.0.0

v1.9.4

v1.9.0

v1.8.2

v1.8.1

v1.8.0

v1.7.1

v1.7.0