NAV
Unity iOS Android

Update on the GameTune Beta

Instructions for shutting off the GameTune SDK

A new release without the GameTune SDK is not immediately required since the service can be turned off remotely directly from the GameTune dashboard. Your players will receive the control alternative defined for your questions. To disable GameTune, navigate to the GameTune settings of your project:

GameTune settings

Here you can disable GameTune for your project. Please note that this action is final and cannot be reversed.

Disable GameTune

Once GameTune is disabled, we strongly recommend removing the SDK and GameTune related code from the game as soon as possible. For help moving off of GameTune, we are happy to assist you at gametune-support@unity3d.com.

GameTune

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

Easy integration

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.

Leverage Unity’s data

GameTune is driven by your game’s data but also leverages Unity’s dataset from billions of 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 is flexible

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.

Enables personalized game experiences

GameTune’s optimizations are personalized and dynamic for each user. GameTune enables you to build dynamic experiences, where a game parameter can change over time based on the player’s state or progression.

Continuous optimization

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 GameTune optimization works

Smart exploration

GameTune begins exploring by choosing all alternatives in equal proportions. Once rewards are received the alternatives can be compared by how they perform on average. As the received events increase, so does GameTune’s confidence about the expected performance of each alternative. Once the confidence increases, better performing alternatives are automatically chosen more often to maximize the rewards in the long run.

Personalization

Once there is enough collected data, GameTune can start to personalize. This means, a selected alternative for the player is chosen by a sophisticated machine learning model that takes in the available data about the player and the device.

GameTune Requirements

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 easy and smooth. The SDK offers:

Implementing GameTune

Overview

The steps below describe the overall process required to receive machine learning-optimized responses from the GameTune service.

  1. Create a question and alternatives in the GameTune dashboard.
  2. Initialize the SDK with your project ID.
  3. Implement the question and alternatives in script, based on the code snippets you copy-paste from the dashboard.
  4. Ask the question.
  5. Modify the game based on the player’s action.
  6. Use the answer.
  7. Send rewards.

Creating questions

To create your questions in the Unity dashboard:

  1. Open the Unity Dashboard, go to GameTune and select your project. If your project isn’t already available in the Unity Dashboard, select Create Project.
  2. In the GameTune overview page, select Create Question.
  3. Select your optimization target in the configuration wizard. Your selection influences how the machine learning model is trained and what it optimizes for. For this example, select IAP optimization.
  4. Give your question a name. This name is used in your game scripts and cannot have spaces or special characters.
  5. Define alternatives for the question.
  6. In the Create Question window, select Create Question.

Once you’ve created your question in the dashboard, implement it in your game script.

GameTune dashboard automatically generates example code snippets for the question, answers and rewards (if applicable). These code snippets are available in the question settings page.

Initializing the SDK

To set up your game script to use GameTune:

  1. Declare the GameTune namespace at the top of your script.
    using UnityEngine.GameTune;
    
  2. Initialize the SDK.
    void Start() {
        // replace id with your game's Unity Project ID (UPID) found in project settings
        GameTune.Initialize("1484cec3-5613-4f74-98b3-1101c9501aa8");
    }
    

Implementing questions

To implement the question and alternatives in your game script:

  1. Use the GameTune.CreateQuestion method to implement a question. See the code snippet from the GameTune dashboard. The example below creates a question called offer, with three possible alternatives.
    public void CreateQuestions()
        {
            // keep name "price", but change price quantity to the correct one
    
            Alternative smallOffer = new Alternative("smallOffer", new Dictionary<string, object>()
            {
                { "price", 0.99 },
                { "coins", 100 }
            });
    
            Alternative midOffer = new Alternative("midOffer", new Dictionary<string, object>()
            {
                { "price", 2.99 },
                { "coins", 500 }
            });
    
            Alternative largeOffer = new Alternative("largeOffer", new Dictionary<string, object>()
            {
                { "price", 4.99 },
                { "coins", 1000 }
            });
    
            Question offer = GameTune.CreateQuestion(
                "iap_offer",
                new Alternative[] { smallOffer, midOffer, largeOffer },
                OfferAnswerHandler
            );
        }
    

Asking questions

  1. Use the GameTune.AskQuestions method to pass your questions to the GameTune service.
    void Start()
    {
        GameTune.AskQuestions(offer);
    }
    

The SDK makes an HTTP request with the question to the GameTune service. The GameTune service returns answers to the SDK and invokes answer handling callbacks.

Modifying game behavior

  1. Create a method to modify the game based on the player’s action.
    private void OfferAnswerHandler(Answer answer)
        {
            if (answer.Value == "smallOffer")
            {
                // show the 0.99 offer bundle
            }
            else if (answer.Value == "midOffer")
            {
                // show the 2.99 offer bundle
            }
            else if (answer.Value == "largeOffer")
            {
                // show the 4.99 offer bundle
            }
        }
    

Using Answers

If the answer to a question affects the game, call the Question.Use or answer.Use method in your AnswerHandler method to use the answer to the question.

If you have a reference to the answer you received, you can call Use on the answer object directly.

private void OfferAnswerHandler(Answer answer)
{
    // after showing the offer to the player
    answer.Use();
}

Note: If the answer is not used, GameTune omits the data for this player, and considers that player did not reach the point where the game was changed.

Sending reward events

Use the GameTune.RewardEvent method to inform GameTune that the user has completed the desired action; in this example, the user has purchased an offer.

To configure reward events, go to your question’s Settings page in the Unity Dashboard and select Reward Configuration. You can add, edit and delete rewards here.

Add the reward event to your script, for example:

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

Note: Make sure you use the same reward names and attributes in the Dashboard and the game code.

Implementation example

The code block below shows an example script for the previous steps. This example provides a question and alternatives for IAP optimization.

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

public class IAPOffer : MonoBehaviour
{
    public string projectId;
    public Question offer;

    void Start()
    {
        GameTune.Initialize(projectId);
        GameTune.AskQuestions(offer);
}

    public void CreateQuestions()
    {

        Alternative smallOffer = new Alternative("smallOffer", new Dictionary<string, object>()
        {
            { "price", 0.99 },
            { "coins", 100 }
        });

        Alternative midOffer = new Alternative("midOffer", new Dictionary<string, object>()
        {
            { "price", 2.99 },
            { "coins", 500 }
        });

        Alternative largeOffer = new Alternative("largeOffer", new Dictionary<string, object>()
        {
            { "price", 4.99 },
            { "coins", 1000 }
        });

        Question offer = GameTune.CreateQuestion(
            "iap_offer",
            new Alternative[] { smallOffer, midOffer, largeOffer },
            OfferAnswerHandler
        );
    }

    // implement your Answer Handler callback methods
    private void OfferAnswerHandler(Answer answer)
    {
        // show the offer based on the answer
        if (answer.Value == "smallOffer")
        {
            // show the 0.99 offer bundle
        }
        else if (answer.Value == "midOffer")
        {
            // show the 2.99 offer bundle
        }
        else if (answer.Value == "largeOffer")
        {
            // show the 4.99 offer bundle
        }

        // Tell GameTune that answer changed the game experience
        answer.Use();
        offer.Use("smallOffer");

    }

    private void ProcessPurchase(Answer answer) {
        if (answer.Value == "smallOffer")
        {
            // Give user 100 coins.
        }
        else if (answer.Value == "midOffer")
        {
            // Give user 500 coins.
        }
        else if (answer.Value == "largeOffer")
        {
            // Give user 1000 coins.
        }

        // Send reward to GameTune
        var attributes = new Dictionary<string, object>() { { "price", 0.99 } };
        GameTune.RewardEvent("purchased", attributes);
    }
}

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

Level difficulty or other game parameters

Tutorial

Starter pack

Rewards and in-game resource balancing

Signposting and feature surfacing

IAP offer

IAP surfacing

Storefront UI

Ad frequency or cool down period

Ad surfacing point

Rewarded ads

Downloads

Unity games

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

To import the GameTune SDK to your project:

GameTune Settings

Download the 2.8.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.8.0 GameTune SDK or 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.8.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 the 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 an 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 automatically assigns users into different treatment groups.

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. You can use it to have some control over network operations of the SDK. It can be one of the following values:

AnswerType.ALWAYS_NEW;
AnswerType.NEW_UNTIL_USED;
UnityGameTuneAnswerType.kUnityGameTuneAnswerTypeAlwaysNew;
UnityGameTuneAnswerType.kUnityGameTuneAnswerTypeNewUntilUsed;
AnswerType.ALWAYS_NEW;
AnswerType.NEW_UNTIL_SED;

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 a privacy regulated region, 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.

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.

Reward Weights

There are cases when some rewards are more valuable to you than others e.g. a watched ad could be less valuable than an expensive IAP. You can assign different values to each reward you send to GameTune via reward attributes. You can find or change the name of the attribute used to determine the value of a reward under the “Optimization settings” section of the “Settings” tab. By default, i.e. if you do not assign any value to rewards you send, all rewards are considered equal and have value of 1.

Reward Weight Settings

Keep in mind, that you do not need to provide real monetary value of a reward if you do not want to or do not know it in advance. Instead you can use the value of a reward as a weight to indicate its importance. E.g. for a watched ad you could use reward value of 1 but for an expensive IAP it could be set to 5.

IAP rewards validation

With IAP optimization Questions, you need to send a reward when player purchases something. It is a common problem that players fraud the purchase events and this can lead to bad training data for the GameTune model. To prevent this from happening, you can configure GameTune to validate IAP reward events. To do that you need to:

  1. Enable reward validation in your project settings “Unity Dashboard > GameTune > Settings”
  2. Send a special reward event using InAppPurchaseEvent method of the GameTune SDK. If you’re using the Unity IAP Plugin you can do it as follows:
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
        {
            GameTune.InAppPurchaseEvent(e.purchasedProduct.definition.id, // Product ID
                                        0.99, // Reference price
                                        e.purchasedProduct.receipt); // Receipt as provided by the plugin

            return PurchaseProcessingResult.Complete;
        }

If you do not use Unity IAP Plugin, you can construct arguments to InAppPurchaseEvent manually.

Include the following parameters in the method call:

InAppPurchaseEvent(string productID, double price, string receipt)

If you have any questions about IAP validation and its status in your game, please contact our support.

How GameTune model training works

After completing the GameTune integration, GameTune starts to collect event data from your game’s players automatically. In the beginning, GameTune doesn’t yet know how your Alternatives work for the players. Thus it starts “exploring” the Alternatives. This means randomly selecting which Alternative is chosen for the user.

Once enough data is collected, the model is trained and optimization can start. The first stage model for smart exploration is trained when the following amount of data is received:

Once 25K used Answers are received, a second stage model capable of personalization is trained.

Model training repeats every time enough data is received but is limited to once a day.

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 help GameTune to differentiate players from each other and that are 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. GameTune will call the implemented user attributes provider automatically when needed.

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

Examples of useful user attributes

The purpose of user attributes is to describe the player, their playing and spend habits, so that GameTune can understand the difference between player’s states. Below you can find suggestions of useful user attributes.

Current status

In-game resources

Engagement

Player profile, playing habits and preferences

Player skill

Player source and social activities

Ad & IAP behaviour

Cross-game & insights

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:

appStartEvent.UnityProjectId;
[appStartEvent unityProjectId];
appStartEvent.getUnityProjectId();

OnUseEvent

OnUseEvent is called when answer is used and:

Properties of UseEvent:

useEvent.UnityProjectId;
useEvent.AnswerId;
useEvent.QuestionName;
useEvent.TreatmentGroup;
useEvent.ChosenAlternative;
[useEvent unityProjectId];
[useEvent answerId];
[useEvent questionName];
[useEvent treatmentGroup];
[useEvent chosenAlternative];
useEvent.getUnityProjectId();
useEvent.getAnswerId();
useEvent.getQuestionName();
useEvent.getTreatmentGroup();
useEvent.getChosenAlternative();

OnRewardEvent

OnRewardEvent is called every time the Reward is sent.

Properties of RewardEvent:

rewardEvent.UnityProjectId;
rewardEvent.Name;
Dictionary<string, object> attributes = rewardEvent.Attributes;
[rewardEvent unityProjectId];
[rewardEvent name];
NSDictionary *attributes = [rewardEvent attributes];
rewardEvent.getUnityProjectId();
rewardEvent.getName();
Map<String, Object> attributes = rewardEvent.getAttributes();

OnQuestionEvent

OnQuestionEvent is called when Question is asked.

Properties of QuestionEvent:

questionEvent.UnityProjectId;
questionEvent.Name;
Alternative[] alternatives = questionEvent.Alternatives;
[questionEvent unityProjectId];
[questionEvent name];
NSArray *alternatives = [questionEvent alternatives];
questionEvent.getUnityProjectId();
questionEvent.getName();
Alternative[] attributes = questionEvent.getAlternatives();

Examples

Game difficulty

You have a casual basketball game, where the player must hold down and release the slider that controls the power of the throw. Time it perfectly and you score, release too soon or too late and you miss. Pretty simple, new players shouldn’t experience difficulties jumping into the action, yet you found the day 1 retention lackluster. You think the challenge for many might lie in the speed of the power slider. For some it’s too fast and they get frustrated immediately. At the same time, you don’t want to compromise the experience for the players who enjoy the game as is and perhaps, try to find even more challenging power slider settings that may suit the most demanding players.

1. Pick Alternatives

There are many ways to approach this problem. One way is to set your current slider speed as the baseline and pick one to two more alternative speed configurations from each side of the base speed. So, if we consider the current speed as 0, then we go 20% faster, 20% slower, 40% faster and 40% slower. We can code them as 0, faster_20, slower_20, faster_40, slower_40, avoiding special characters. It’s a good starting point. We can always remove or add new alternatives later, after evaluating the initial results.

2. Configure the Question

Go to the GameTune dashboard and add the Question. Select “Retention, day 1” as the optimization target and select the name, for example, slider_speed

We have deciced that the current version of slider speed settings is called 0, which is added as Control Alternative. Add the rest of the alternatives faster_20, slower_20, faster_40, slower_40.

3. Initialize GameTune

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

4. Ask the Question

Create and ask the Question on every session start:

Question question = GameTune.CreateQuestion (
    "slider_speed",
    new string[]{ "0", "faster_20", "slower_20", "faster_40", "slower_40" },
    SpeedAnswerHandler
);

GameTune.AskQuestions(question);

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

5. Handle the Answer

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

private void SpeedAnswerHandler(Answer answer)
{
    // change slider speed with accordance to answer.Value
}

6. 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. Use the answer immediately after changing the slider speed:

answer.Use();

7. Send User Attributes

User Attributes add contextual data to GameTune ML. Their are unique for every game, thus help to shape models to suit your game better. Set User Attributes that describe the player the best way possible.

Examples of Attributes that describe the player’s performance:

Examples of Attributes that help in understadning the player’s monetization potential:

8. Verify your integration

Build and run your game on the device (iOS on Android). Got Questions page in dashboard, you should see Answers count increased from -:

Questions page

Then go to Integration tab of the Question, you should see all the events:

Integration tab

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

Keep visiting Overview and Report tabs in dashboard regularly, it will be continuously updating, as more data comes in and the model matures.

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

[2.8.0] - 2020-10-27

[2.7.1] - 2020-10-22

v2.7.0

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

Apple privacy survey

Starting December 8, 2020, iOS publishers must define what data their apps collect, including the data collected by integrated third-party SDKs such as GameTune. For your convenience, GameTune provides information on its data collection practices below.

Important: The data disclosures below are for the GameTune SDK only. You are also responsible for providing any additional disclosures for your app, including other third-party SDKs used in your app.

For more information on Apple’s data collection disclosure policies, including terminology definitions, please see the Apple documentation.

Contact info data Collected? Linked to user? Purpose
Name
For example, first or last name.
No Not applicable Not applicable
Email Address
Including, but not limited to a hashed email address.
No Not applicable Not applicable
Phone Number
Including, but not limited to a hashed phone number.
No Not applicable Not applicable
Physical Address
Such as home address, physical address, or mailing address.
No Not applicable Not applicable
Other User Contact Info
Any other information that can be used to contact the user outside the app.
No Not applicable Not applicable
Health and Fitness data Collected? Linked to user? Purpose
Health
Health and medical data, including but not limited to from the Clinical Health Records API, HealthKit API, MovementDisorderAPIs, or health-related human subject research or any other user-provided health or medical data.
No Not applicable Not applicable
Fitness
Fitness and exercise data, including but not limited to the Motion and Fitness API.
No Not applicable Not applicable
Financial data Collected? Linked to user? Purpose
Payment Info
Such as form of payment, payment card number, or bank account number.
No Not applicable Not applicable
Credit Info
Such as a credit score.
No Not applicable Not applicable
Other Financial Info
Such as salary, income, assets, debts, or any other financial information.
No Not applicable Not applicable
Location data Collected? Linked to user? Purpose
Precise Location
Information that describes the location of a user or device with the same or greater resolution as a latitude and longitude with three or more decimal places.
No Not applicable Not applicable
Coarse Location
Information that describes the location of a user or device with lower resolution than a latitude and longitude with three or more decimal places, such as approximate location services.
Yes Linked to user Analytics and product personalization
Sensitive data Collected? Linked to user? Purpose
Sensitive Info
Such as racial or ethnic data, sexual orientation, pregnancy or childbirth information, disability, religious or philosophical beliefs, trade union membership, political opinion, genetic information, or biometric data.
No Not applicable Not applicable
Contact data Collected? Linked to user? Purpose
Contacts
Such as a list of contacts in the user’s phone, address book, or social graph.
No Not applicable Not applicable
User content Collected? Linked to user? Purpose
Emails or Text Messages
Including subject line, sender, recipients, and contents of the email or message.
No Not applicable Not applicable
Photos or Videos
The user’s photos or videos.
No Not applicable Not applicable
Audio Data
The user’s voice or sound recordings.
No Not applicable Not applicable
Customer Support
Data generated by the user during a customer support request.
No Not applicable Not applicable
Other User Content
Any other user-generated content.
No Not applicable Not applicable
Browsing data Collected? Linked to user? Purpose
Browsing History
Information about the content the user has viewed that is not part of the app, such as websites.
No Not applicable Not applicable
Search data Collected? Linked to user? Purpose
Search History
Information about searches performed in the app.
No Not applicable Not applicable
Identifier data Collected? Linked to user? Purpose
User ID
Such as screen name, handle, account ID, assigned user ID, customer number, or other user- or account-level ID that can be used to identify a particular user or account.
Yes Linked to user Analytics and product personalization
Device ID
Such as the device’s advertising identifier, or other device-level ID.
Collects data Linked to user Analytics and product personalization
Purchase data Collected? Linked to user? Purpose
Purchase History
An account’s or individual’s purchases or purchase tendencies.
Yes Linked to user Analytics and product personalization
Usage data Collected? Linked to user? Purpose
Product Interaction
Such as app launches, taps, clicks, scrolling information, music listening data, video views, saved place in a game, video, or song, or other information about how the user interacts with the app.
Yes Linked to user Analytics and product personalization
Advertising Data
Such as information about the advertisements the user has seen.
Yes Linked to user Analytics and product personalization
Other Usage Data
Any other data about user activity in the app.
Yes Linked to user Analytics and product personalization
Diagnostic data Collected? Linked to user? Purpose
Crash Data
Such as crash logs.
No Not applicable Not applicable
Performance Data
Such as launch time, hang rate, or energy use.
Yes Linked to user Analytics and product personalization
Other Diagnostic Data
Any other data collected for the purposes of measuring technical diagnostics related to the app.
May collect data* Yes Analytics

*GameTune may collect events related to errors, timeouts, and request latency to help us identify issues with the integration, or when publishers enable certain features.

Other data Collected? Linked to user? Purpose
Other Data Types
Any other data types not mentioned.
No Not applicable Not applicable