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:
Here you can disable GameTune for your project. Please note that this action is final and cannot be reversed.
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
- Your game is not directed at children under the age of 13 in the United States (COPPA).
- We recommend your game to have a least 1000 DAU per alternative to ensure good level of personalization.
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:
- Helpers for accessing the GameTune service and making network requests
- Functionality for getting the player’s data in a correct format (device information, user identifiers etc.)
- Caching of previous Answers
- Handling errors and timeouts
Implementing GameTune
Overview
The steps below describe the overall process required to receive machine learning-optimized responses from the GameTune service.
- Create a question and alternatives in the GameTune dashboard.
- Initialize the SDK with your project ID.
- Implement the question and alternatives in script, based on the code snippets you copy-paste from the dashboard.
- Ask the question.
- Modify the game based on the player’s action.
- Use the answer.
- Send rewards.
Creating questions
To create your questions in the Unity dashboard:
- 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.
- In the GameTune overview page, select Create Question.
- 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.
- Give your question a name. This name is used in your game scripts and cannot have spaces or special characters.
- Define alternatives for the question.
- 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:
- Declare the
GameTune
namespace at the top of your script.
using UnityEngine.GameTune;
- 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:
- 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
- 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
- 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
- Level speed, number of obstacles or effectiveness of weapons
- Example alternatives: easy, medium, hard
- Optimization target: Retention or Sum of rewards
Tutorial
- Details and length of the tutorial
- Example alternatives: none, short, medium, long
- Optimization target: D1/D3 Retention
Starter pack
- Resources, in-game items given to the player when starting the game
- Example alternatives: weak, medium, strong
- Optimization target: Retention or Sum of rewards
Rewards and in-game resource balancing
- Items received from a lootbox, cooldown time until the lootbox can be opened again
- What is a good moment to gift the user, balance hoarding and churn prevention
- Example alternatives: 1 diamonds, 3 coins, 5 coins
- Optimization target: Retention or Sum of rewards
Signposting and feature surfacing
- Timing, when a feature is introduced to the player or reduce/increase signposting
- Example alternatives: more, less signposting
- Optimization target: Retention
IAP offer
- Content and the price of the offer, duration the offer is available
- Example alternatives: 20 gems for $2, 50 gems for $4 and rare item for $10
- Optimization target: IAP optimization
IAP surfacing
- What is the right moment to introduce IAP purchases for the first time buyer
- Example alternatives: level-up 3, session 5, in-game coins left 3
- Optimization target: IAP optimization
Storefront UI
- What is the item being promoted to the user in the store, order of items, layout
- Example alternatives: promote sword, axe or 10 coins
- Optimization target: IAP optimization
Ad frequency or cool down period
- Number of ad impressions displayed during the session or cooldown time between ads
- Example alternatives: low, medium, high
- Optimization target: Sum of rewards
Ad surfacing point
- What is the right moment to show an ad to create positive experience for the player
- Example alternatives: level 1, level 3, level 5, after win, after lose
- Optimization target: Sum of rewards
Rewarded ads
- What is the reward a user gets by watching an ad? E.g. number of coins, extra lives, multiply boost.
- Select cooldown period when reward is available
- Balance rewarded and interstitial ads
- Example alternatives: 1 coin, 3 coins, 1 extra life, 2x boost
- Optimization target: Sum of rewards
Downloads
Unity games
The GameTune SDK supports 2018.3 and newer versions of the Unity Editor.
To import the GameTune SDK to your project:
- Unity Editor: Assets -> Import Package -> Custom Package… -> UnityGameTune.unitypackage
- Access GameTune SDK API through the GameTune namespace
- If your game does not show ads of any kind, uncheck Include AdSupport on iOS in Project Settings -> GameTune Settings (it is included by default)
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:
- Place
unity-gametune.aar
under libs in your app or a directory where you store third-party libraries - In build.gradle of your app add
implementation fileTree(dir: 'libs', include: ['unity-gametune.aar'])
under dependencies
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
- projectId - Unity Project ID of your game
- initializeOptions - (optional) Options to control the behaviour of GameTune
- eventListener - (optional) A listener for analytics events
- userAttributesProvider - (optional) An object for populating user attributes
Privacy consent handling
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 privacy consent
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:
-
name - a string that describes the problem you’re trying to solve, e.g.
level_difficulty
. The name must be unique within the game. -
alternatives - an array of Answers or strings, each corresponding to a particular solution for the problem, e.g.
{"easy", "medium", "hard"}
. -
answer type - an optional setting that defines whether the SDK should make a new API request ignoring cached data each time a question is asked, or return a previously received answer from the local cache if it is available.
-
answer handler - a callback function that is invoked when the answer is retrieved from GameTune.
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: Users in this group will get the Control Alternative. This group can be used for comparing the performance of GameTune to a baseline defined by you.
- ML optimized: Users in this group will get an answer optimized by the GameTune ML algorithm.
- Exploration: Users in exploration group will get a randomly selected answer. After initial data collection, distribution is adjusted by the performance of the Alternatives i.e. well performing Alternatives are selected more.
- GDPR/Restricted/empty: Users in this group will get the Control Alternative due to restrictions or errors
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:
-
There was an error. For example, the user has no network connection or the answer timed out due to a slow network connection.
-
The user was assigned to the control group by GameTune.
-
Device had test mode on
-
User did not give privacy consent
-
Question is not configured in GameTune dashboard.
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:
-
ALWAYS_NEW - Return an Answer by making a network request to GameTune API every time the Question is asked. The answer may or may not change depending on the data of the user or the treatment group the user was assinged to, e.g. if a user has been put into Control or GDPR/Restricted group, he will always get the control alternative.
-
NEW_UNTIL_USED - Return an Answer by making a network request only until the Answer for the Question is used. After that the SDK will always return that same Answer for the user using the value stored in the local cache.
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:
Initialize
is called when the game is started. It’s expected thatInitialize
is called once for every user that starts the game.Question
is asked before the Answer should change the game. Correct moment to ask the Question is as late as possible to have up to date user attributes, but early enough to make sure user gets an Answer even if the network is slow.Use
the Answer at the earliest moment game has the opportunity to change based on one of the Answers. For example with “Ad frequency” Questions, Use should be called the earliest moment an advertisement could be displayed. This also applies if the Answer dictates not to do something, e.g. not showing an advertisement at all.Reward
is sent when a user completes the desired action of the optimization target. For some use cases, it’s expected that a user sends a reward multiple times during a period of time. In these cases, the goal of the model is to maximize the sum of rewards over time.
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.
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:
- Enable reward validation in your project settings “Unity Dashboard > GameTune > Settings”
- App Store: you need to specify the bundle identifier of the game you used in the store
- Google Play: you need to provide the bundle identifier of the game in the store and your Google Play License Key which is available from the Google Play Developer Console
- Send a special reward event using
InAppPurchaseEvent
method of theGameTune
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)
productID
- your In-App Purchase product ID, for examplecom.unity.sampleproduct
. Can be accessed from purchase event arguments:arguments.purchasedProduct.definition.id
price
- a reference price for your product. If the product is available for purchase in multiple currencies, use only one currency when submitting an IAP reward event.receipt
- a receipt for the transaction, accessible from purchase event arguments:arguments.purchasedProduct.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:
- 1000 used Answers per Alternative, e.g. if a Question has three alternatives, 3000 used Answers are required.
- 100 Rewards or if optimizing for retention, 100 users coming back on the specified day
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.
- Sum of rewards maximizes the sum of Rewards' value over time. You can use this when Rewards have different values or you want to optimize for total LTV, such as combination of ads and price-tiered IAPs.
- Retention maximizes user retention for the selected day, meaning the probability of a user coming back on the given day.
- IAP optimization chooses the optimal IAP to offer for the user. Internally it optimizes conversion for the first time buyers and value for repeat buyers.
- Conversion allows you to maximize the probability of a user doing an action in the game, i.e. the likelihood of sending the Reward at least once.
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
- current state in the game: level, score, etc.
- last level/average game play: number of losses, time spent
- number of levels/games user has played in total
- example:
current_level: 3, score: 344, moves_used_previous_level: 34, moves_used_average: 23.59, failures_previous_level: 1, failures_average_per_level: 0.244
In-game resources
- in-game resource balance (coins, gems, lives) to indicate what the player has used and when she might run out of resources
- example:
coins: 23, boosters: 2, coins_used: 459, boosters_used: 34
Engagement
- unlocks, features used that indicates what the user has already done and what remains to be done
- example:
is_clan_member: true, prestige: true, missions_completed: 4, vip: true
Player profile, playing habits and preferences
- Prefers pvp or single player, clans, events joined, uses customization features
- example:
pvp_plays: 2, single_player_plays: 344, clan_plays: 0, events_joined: 0
Player skill
- ELO rating, experience, skill level
- clan role
- streaks (win/lose)
- example:
win_streak: 3, lose_streak: 0, clan_role: "master", elo_rating: 1782
Player source and social activities
- user acquisition source
- connected to Facebook, Google Play, etc
- number of friends from FB/etc, number of invites
- example:
ua_source: fb, friends_invited: 3, is_logged_in: true
Ad & IAP behaviour
- IAP: total purchases in USD, last item purchased, how many times opened store, in-game currency spent
- ads: number of ads watched in total/rewarded/interstitial, number of ads clicked (indication that tried installing)
- example:
items_purchased: 4, store_opened: 34, last_iap: "sword", coins_used: 459, ads_watched_rewarded: 13, ads_watched_interstitial: 59
Cross-game & insights
- stats from other games: what other games the player is playing and how much
- example:
other_games: "foo|bar|baz", avg_session_count: 4.63
User data that GameTune automatically collects
GameTune automatically collects these attributes about the user:
- device information: platform, device, language, etc.
- context: country, time of the day
- number of sessions user has played
- time since session start, last session ended
- previous session length (total/mean)
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:
- UnityProjectId: Unity project id of the game (string);
appStartEvent.UnityProjectId;
[appStartEvent unityProjectId];
appStartEvent.getUnityProjectId();
OnUseEvent
OnUseEvent
is called when answer is used and:
- Answer wasn’t yet used when Answer Type is set to
NEW_UNTIL_USED
- Answer Type is set to
ALWAYS_NEW
Properties of UseEvent:
- UnityProjectId: Unity project id of the given game (string);
- AnswerId: Unique ID for the Answer (string);
- QuestionName: Name of the Question (string);
- TreatmentGroup: Treatment group (string);
- ChosenAlternative: Name of the Alternative chosen for the user (string);
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:
- UnityProjectId: Unity project id of the given game (string);
- Name: Name of the Reward sent (string);
- Attributes: Attributes of the reward (Dictionary, NSDictionary, Map)
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:
- UnityProjectId: Unity project id of the given game (string);
- Name: Name of the Question (string);
- Alternatives: Array of Alternatives for the Question (Alternative[])
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:
player_progression
- determines player’s current progress, e.g. number of completed levelsplayer_xp
- describes how much of the total XP the player has managed to collect, helps to determine their skill levelplayer_high_score
- attributes that is directly linked with the player’s skill and determination, helps to identify completionists and the most hardcore players
Examples of Attributes that help in understadning the player’s monetization potential:
iap_bought_total
- how much money in IAP the player has spent so farcurrent_dollar
- how much of the in-game currency the player has at the momentiap_clicked_total
- how many times the player clicked the IAP, shows the intention to spend money
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 -
:
Then go to Integration
tab of the Question, you should see all the events:
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
- Improved stability of handling of user data processing consent
[2.7.1] - 2020-10-22
- Improved API to send an In-App Purchase events
- Improved stability on Android API level 30
v2.7.0
- Added In-App Purchase validation API
v2.6.0
- Added ModelName and ModelVersion fields in Answer
- Removed Signature field from Answer
v2.5.0
- Added UserAttributeProvider interface that is use for automatic User Attributes invocation
v2.4.2
- Removed deprecated UIWebView class in iOS
v2.4.0
- Renamed GDPR consent to generic privacy consent
v2.3.1
- Removed unnecessary link.xml file
v2.3.0
- Adds analytics events and the event listener interface
v2.2.0
- Adds capability to “turn off” GameTune, which disables all calls to GameTune for a user
v2.1.2
- Adds additional iOS Framework that does not use AdSupport library. Developers can choose to include AdSupport in their iOS builds (if they show ads) or exclude it (if they don’t show ads of any kind)
v2.0.0
- Changed name to GameTune
v1.9.4
- Fixed Android null question handling
v1.9.0
- Allowed changing Alternatives during run time
- Bumped supported Unity versions from 5.6.5p3 and newer to 2017.4 and newer
v1.8.2
- Fixed issue when launching game with updated SDK (game launched earlier with older sdk e.g. 1.5.0) without network connection, might brake native api calls
v1.8.1
- Fixed issue when device initially offline would not get answers with treatment group after going back online
v1.8.0
- Improved internal structure and dependencies.
v1.7.1
- Fixed issue were User Attributes would not cache sometimes.
v1.7.0
- Added a parameter
AnswerType
toCreateQuestion
. This indicates whether new answer should be returned every time same question is asked (ALWAYS_NEW
) or should use previously cached answer (NEW_UNTIL_USED
). - Question can be used directly with
question.use(aValue)
without the need to ask the question. This requires developer to store the value received in the answer that was asked before.
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 |