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 and you don’t need to have resources to build and deploy your own ML systems. GameTune does automatic feature discovery, selection, transformations, and trains a model with game specific data and deploys and operates that model.

For making an optimized real time decision for each individual player who is currently playing your game, GameTune uses the user’s current context in your game with historical information. GameTune is driven by your game’s data but is also leveraging Unity’s dataset from 1.5 billion devices, so you get access to utilize Unity data for optimizing your game from the get-go.

GameTune can be used for many kinds of decision problems where the goal is to select best alternative from A, B, C, … for each the user. The best alternative is the one that maximizes retention, revenue, conversion or a custom game-specific reward. For example, game parameters, assets, UI, and settings can be personalized to match individual preferences.

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.

Below is a list of example GameTune questions to help you understand how it could be used in your game. These questions have been working well for games using GameTune, but you do not have to limit yourself to these questions and you can still optimize anything specific to your game. 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

Starting from SDK version 2.2.0 Unity Editor versions supported are 2018.3 and newer. If you are on a lower version (2017.4 and newer) of Unity, consider using SDK v2.0.0 for Unity

To import the GameTune SDK to your project:

GameTune Settings

Download the 2.2.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.2.0 GameTune SDK for iOS with AdSupport library

Download the 2.2.0 GameTune SDK for iOS without AdSupport library

non-Unity Android games

To import Unity GameTune SDK to your project:

Download the 2.2.0 GameTune SDK for Android

SDK Overview

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

Architecture Diagram

Initialization

GameTune SDK should be initialized when user starts the game. In addition to initializing the SDK this reports to GameTune that user started the game in order to track user retention.

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

Parameters

Due to legislation, GameTune may require user consent for personalization and tracking. To provide information on the user’s GDPR consent status, create a new InitializeOptions object and call SetGdprConsent with either true or false on the created object.

If you haven’t asked for the user’s consent, don’t call SetGdprConsent at all. GameTune will then behave as if users in GDPR restricted countries had opted out, and users elsewhere opted in.

To set GDRP consent with Initialize:

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

To update GDRP consent:

GameTune.SetGdprConsent(true);
GameTune.SetGdprConsent(false);
[UnityGameTune setGdprConsent:YES];
[UnityGameTune setGdprConsent:NO];
UnityGameTune.setGdprConsent(true);
UnityGameTune.setGdprConsent(false);

Additionally, it is possible to update the GDPR consent status after initialization by calling SetGdprConsent.

User ID

If userId is included with initialization options, it will be included in the answer signature as the sub field. See more information about the signature.

InitializeOptions options = new InitializeOptions();
options.SetUserId(userId);
GameTune.Initialize(projectId, options);
UGTInitializeOptions *options = [[UGTInitializeOptions alloc] init];
[options setUserId:userId];
[UnityGameTune initialize:projectId withOptions:options];
InitializeOptions options = new InitializeOptions();
options.setUserId(userId);
UnityGameTune.initialize(activity, projectId, options);

GameTune Off

It is easy to exclude a segment of users from exposure to GameTune by setting up GameTuneOff flag in initialize options.

When GameTuneOff is set to true, SDK skips all network calls to GameTune server and returns default answers to 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);

Asking 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 the name for the question, which identifies the optimization point in the game (e.g. level_difficulty). Then you define what the alternatives are (i.e. possible answers) for that question.

Creating questions

Question contains information about a decision problem you are trying to solve with the help of GameTune. It’s an object that consists of these properties:

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

In order to start receiving ML optimized answers from GameTune, you have to create your Questions in GameTune dashboard.

In the dashboard navigate to your project, then to Optimization/GameTune/. On the GameTune page choose to Create your first Question (or Add Question if you have created one already) and provide the same Question name as in your code.

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())
);

Answer Type

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

Alternative attributes

Alternative attributes are 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, your game is deciding which weapon to show to the player: Demonbane, Excalibur, Grimtooth, or Orcrist (or one of the 20 other weapons in the game). The first two are “lawful”, the last two are “chaotic”; and mean damages are 23, 17, 12.5, and 26, respectively. The machine learning model can then learn correlations between damage and reward. Similarly, there are only a few alignments such as lawful or chaotic, but there can be dozens of weapons, so the model sees more data per alignment than per weapon. Thus alignment preferences can be learnt faster and more accurately than individual weapon preferences.

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())
);

Asking questions

Once questions are created, the personalized answer is received by passing your Questions to the AskQuestions method. The GameTune SDK will create an HTTP request to the GameTune service, which will return an Answer back to the SDK, which will invoke callbacks of Questions (calling SetLevelDifficulty in the example above).

The speed of getting answers depends on the network connection, so we recommend asking 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.

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

Handling answers

Once an Answer is ready, the Answer object is passed as an argument to your AnswerHandler function.

Answer value

Value is the selected Alternative for the current user, i.e. the name of the alternative.

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

Answer ID

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

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

Treatment groups

TreatmentGroup contains information about the treatment the user was exposed to when GameTune provided the answer. This can be used e.g. for tagging the user in your analytics in order to measure ML’s performance. TreatmentGroup will be a string with one of the following values:

Value Description
ml The user received an answer optimized by the GameTune ML algorithm.
control The user in this group will get the default answer. This group can be used for comparing the performance of GameTune or to manage risk by starting the experiment with most users getting the default answer.
exploration The user received a randomly selected answer. This can be used similarly to A/B testing i.e. for measuring the performance of each alternative in general population.
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 signature

Every answer is signed by GameTune, so the answer’s authenticity can be checked. The signature verifies that a specific question was answered by GameTune for a specific user at a specific time.

Signatures are JWT tokens signed with GameTune’s public key. In order to verify signatures and read tokens’ contents, retrieve the public key from the GameTune service

See an example of verifying the signature.

answer.Signature;
answer.signature;
answer.getSignature();

Ask questions timeout

You may pass a timeout for AskQuestions in the initialize options, which will guarantee a response within that timeout. The timeout is set in milliseconds. If the response takes longer than that e.g. in bad network conditions, control Answer 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

With test mode, data sent to the backend will be omitted when training the model. This is useful if you are running e.g. tests in your CI which calls the GameTune service, so the data generated by these tests won’t affect the model.

Test mode is disabled by default. GameTune returns control Alternative (first in the array) 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);

Events

In order to get as accurate metrics on the player’s reaction to the chosen alternative we require the developer to call two additional methods.

Use

Use must be called once the the answer was actually displayed to the user and reward should be expected for it. This enables GameTune to learn if user left the game between the question was asked and when it actually affected the game. For example if making a level diffulty decision, call Use when user started the level. If the game is not affected by the answer, e.g. if you ask a question about button color when the game starts, but the player never sees a button, Use should not be called.

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

If you don’t have a reference to the answer you are going to use at any given time, you can call the Use method on the question itself, providing the chosen Alternative that you expose to your user, so 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 user did the desired action that is the optimization goal of the question. The Reward event is how you provide feedback to the GameTune for optimizing your key performance indicator (KPI). GameTune learns to maximize your optimization goal using data from these events and their attributes. For retention optimization reward is collected automatically when Initialize is called, so sending a reward event is not required.

Example 1

For example if you want GameTune to optimize the probability of the user clicking a button, then call a Reward Event for a button click.

GameTune.RewardEvent("button_clicked");
[UnityGameTune rewardEvent:@"button_clicked"];
UnityGameTune.rewardEvent("button_clicked");

Example 2

This second example is about optimizing which item to display to the user, to optimize for revenue. In this case your KPI might be the total dollar value of IAP purchases. When user sees the offer, call Use and add the following two reward events.

Every time when the user clicks the offer (small reward when plannit to buy)

GameTune.RewardEvent("offer_clicked", attributes);
[UnityGameTune rewardEvent:@"offer_clicked" withAttributes:attributes];
UnityGameTune.rewardEvent("offer_clicked", attributes);

Once a successful purchase was made, report large reward

GameTune.RewardEvent("offer_purchased", attributes);
[UnityGameTune rewardEvent:@"offer_purchased" withAttributes:attributes];
UnityGameTune.rewardEvent("offer_purchased", attributes);

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.

Question type selection

Custom questions

For more detailed control of the optimization target you can select the custom question type. First select if you want to optimize for retention, or reward (such as revenue).

Retention optimization

For retention optimization you can select which day you want the user to come back and what is the window i.e. if you select 3 for the day and window is 2, positive reward is calculated for users that came back on day 3, 4 or 5.

Revenue optimization strategies

Select revenue as your optimization target if you want to maximize revenue or other numeric value, e.g., number of ads watched, or in-game currency spent.

Configure reward events

Click on “Add reward” and make sure that reward name 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>() { { "price", 0.99 } };
GameTune.RewardEvent("purchase", attributes);

You should configure the reward event in the dashboard like this:

Reward configuration

User attributes

The GameTune SDK automatically collects information about the player’s context: platform, device, language, country, time of the day, etc. In addition, you can provide information of what the user has done in your game through user attributes, and with that data the GameTune ML algorithm can make better decisions.

User attributes are a collection of Key-Value pairs. The GameTune SDK stores them internally and attaches those to every event. We recommend you set the attributes right before asking questions, sending use and reward events. However, you can set user attributes as many times and at any point you want. Calling it multiple times will override values for the existing attributes.

Pass this information in form of Key-Value pairs collection

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

Game speed

A game designer has noticed that many users are churning during the game’s first level which is currently using medium speed configuration. At the same time they’ve received feedback that game is too easy in the beginning for players that have played similar games in the genre.

The current version of game speed settings is called medium. The designer creates a new variant fast, which is meant for players that are familiar with the genre. They also created a slow settings which make the game play really easy and slow.

By using GameTune with Unity data about the new player, the hypothesis is that we can select the optimal first time experience for each user. The ultimate goal for this experiment is to increase the average day one retention, meaning that more players should come back to the game next day. The optimization goal is set in the GameTune dashboard by selecting “retention, day 1” as the optimization target.

Initialize GameTune

The first thing you do is to initialize GameTune when the game starts:

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

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

Ask speed question

Next the different variants of the tutorial are defined:

  Question speed = GameTune.CreateQuestion (
      "speed",
      new string[]{ "medium", "fast", "slow" },
      MyAnswerHandler
  );

Notice that here “medium” was listed first. That means that “medium” is the default experience player gets e.g. if they don’t have network or have opted out for personalization. After the question is defined, the question which speed to use for the current player is asked:

GameTune.AskQuestions(speed);

Handle answer

Next MyAnswerHandler is implemented, which contains the actual logic for selecting either the ‘fast’, ‘medium’ or ‘slow’ speed for the first level:

  private void MyAnswerHandler(Answer answer)
  {
      // do something with the answer.Value
  }

Use answer

It’s also important to inform GameTune when the player starts the tutorial, so that GameTune knows that the answer it provided now actually changed the game experience. This is done by “using” the answer:

answer.Use();

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.

Certain group of users will always get the default experience, which is medium in this case. This is called the control treatment group, and it can be used to measure the performance of users in this group vs. those that are in the ML optimized group. As more users play the game, the ML model is continuously trained with more data, and the model becomes more accurate and can 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 once per day to promote one of these deals. But which one of these offers should be shown to the user? We can use GameTune to maximize the revenue, by selecting the optimal offer that leads to more dollars per user.

GameTune was already initialized when the game added the tutorial optimization, so the first thing to do is to create new experience in the GameTune dashboard, with optimization target set to “revenue”.

Ask offer question

Next the different variants of the offer are defined:

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 which offer to show to the user is asked before the offer modal is about to be shown:

GameTune.AskQuestions(offer);

Handle offer answer

OfferAnswerHandler should be implemented similarly:

  private void OfferAnswerHandler(Answer answer)
  {
    // Offer to be shown should be equal to answer.Value and show the modal.
    // After modal is shown, call answer.Use().
  }

Send reward

If the user purchases the offer, this information should be sent to GameTune via a Reward event:

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 “price” per user.

Configure the optimization target

Go to the GameTune dashboard and add the question. Select revenue as the goal for optimization. In the next step select IAP offer.

Maximizing ad impressions

GameTune can be utilized to maximize ad impressions the user is exposed to while balancing churn. This could be done for example by choosing how often an interstitial advertisement is shown.

Ask ad frequency questions

In this example we choose how often the ad is displayed, every means that ad is shown after every level, every_3rd means every 3rd level and every_5th after every 5th level.

  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. answer.Use() should be called before the user sees the first ad.

Send rewards

Once the user watches an ad, send the reward ad_watched. 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("ad_watched");

Configure the optimization target

Go to the GameTune dashboard and add the question. Select revenue as the goal for optimization. In the next step select Ad impressions.

Customize UI

One of the designers has made different variations of the in-game notification promoting a new game feature, and wants to optimize for conversion of clicking the notification. The optimization goal is selected as conversion in the dashboard.

Ask notification 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);

Handle answer

MyAnswerHandler should be implemented similarly:

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

Use 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();

Send reward

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

GameTune.RewardEvent("notification_clicked");

This creates a feedback loop and GameTune learns to show the optimal notification for each user to maximize 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 {

    private const string UnityProjectId = "Your unity project id here";
    private Question _levelDifficultyQuestion;
    private Question _buttonLabelQuestion;
    private Answer _levelDifficultyAnswer;
    private Answer _buttonLabelAnswer;
    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.SetGdprConsent(true);
        initOptions.SetUserId("123G-A67D-FY8G");

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

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

    private void ButtonLabelAnswerHandler(Answer answer)
    {
        string btnLabelSignature = answer.Signature;
        _buttonLabelAnswer = answer;

        PlayButtonText.text = _buttonLabelAnswer.Value;

        // button label changed according to the answer, notifying GameTune
        _buttonLabelAnswer.Use();
        PlayButton.interactable = true;
    }

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

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

        SceneManager.LoadScene("level10");
    }
}
// .h file
#import <UIKit/UIKit.h>
#import <UnityGameTune/UnityGameTune.h>

@interface ViewController : UIViewController

@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;
    [UnityGameTune setUserAttribute:[NSNumber numberWithInteger:_levelsStarted] forKey:@"levels_started"];

    // 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.
}

@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 {
    final private String unityProjectId = "your unity project id here";

    private Answer levelDifficultyAnswer;
    private Answer buttonLabelAnswer;

    // 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) {
        String difficultySignature = answer.getSignature();
        levelDifficultyAnswer = answer;
    }

    private void handleButton(Answer answer) {
        String buttonLabelSignature = answer.getSignature();
        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();
        } else {
            // do something that determines default level difficulty
            // ...
        }
        // start level and execute after it completed:
        // Map<String, Object> attributes = new HashMap<>();
        // attributes.put("number", 5);
        // attributes.put("moves", 10);
        // attributes.put("completion_time", 24);
        // UnityGameTune.rewardEvent("level_completed", attributes);
    }

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

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

        // your internal User ID
        initOptions.setUserId("2345-34-5353");

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

        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();
    }
}

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.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