deltaDNA and ChilliConnect work well together to provide a rich set of Analytics, Live-Ops and CRM capabilities.
There are multiple communication paths between deltaDNA, ChilliConnect, the game client and respective cloud services to facilitate a wide range of use cases.
This tutorial contains examples of the following common scenarios.
- Common Player Identity
- Client Based Event Collection
- Cloud Based Event Collection
- Cloud Based Game configuration
- Client Based CRM
- Cloud Based CRM
- Out Of Game CRM
Supporting project files
- Project source code on GitHub
- deltaDNA project containing event schema, reporting, segementation and campaigns. Please email [email protected] if you require access to this game account
- Playable Demo on Unity Community Hub
It makes life a lot easier if deltaDNA and ChilliConnect use the same identity to refer to a player.
The deltaDNA SDK can use any identity you provide, so it makes sense to use the ChilliConnectId to refer to the player. The ChilliConnectId is provided in the response from the ChilliConnect CreatePlayer method. Store it locally in the game client then use it on all subsequent occasions to Start the deltaDNA SDK. It will then be used for all deltaDNA analysis, segmentation and personalisation as well as appearing in the ChilliConnect UI for player inventory and currecny management etc..
DDNA.Instance.StartSDK(chilliConnectId);
Most of the gameplay events that you generate will originate from the game client, as the player does things in the game that you want to record. Examples of client side events would include the player levelling up, completing a mission etc..
These events are recorded on the game client, cached locally and uploaded to deltaDNA automatically by the SDK at regular 1 minute intervals.
This example records a missionStarted event to reveal the details of a mission the player has just started.
// Record DDNA MissionStarted event
DDNA.Instance.RecordEvent(new GameEvent("missionStarted")
.AddParam("missionName", "Mission " + currentLevel.ToString("D3"))
.AddParam("missionID", currentLevel.ToString("D3"))
.AddParam("userLevel", player.playerLevel)
.AddParam("isTutorial", false)
.AddParam("coinBalance", player.playerCoins)
.AddParam("food", levels[currentLevel - 1].food)
.AddParam("poison", levels[currentLevel - 1].poison)
.AddParam("missionCost", levels[currentLevel - 1].cost)
.AddParam("missionReward", levels[currentLevel - 1].reward)
.AddParam("timelimit", levels[currentLevel - 1].timelimit))
.Run();
}
There are occasions when you may want to submit an event to deltaDNA from ChilliConnect Cloud code. This will happen if there is something that server knows about the player that the client doesn't know or cannot be trusted with. In this scenatio, the deltaDNA REST API can be used to send events to deltaDNA from the ChilliConnect Cloud.
In this example a chilliConnectLogin event is sent to deltaDNA from ChilliConnect Cloud Code when the player logs in to ChilliConnect. This is achieved by creating a ChilliConnect Cloud Code Event Script and setting it to trigger when the ChilliConnect logInUsingChilliConnect v2.0 API method is called.
The login event script:
- Retrieves the deltaDNA URL and Environment details from a common cloud code module
- Constructs a JSON event and parameters
- Posts the event to deltaDNA
- Handles the response and logs any errors.
// An example script that sends a "chilliConnectLogin" event to deltaDNA
// from ChillConnect Cloud Code, when the player logs in.
var ddna = require("deltadna_endpoints");
ChilliConnect.Logger.info("Getting DDNA Environments: ");
var sdk = ChilliConnect.getSdk("2.15.0");
var player = sdk.PlayerAccounts.getPlayerDetails();
var coinsBalance = sdk.Economy.getCurrencyBalance(["COINS"]).Balances[0].Balance;
var userLevel = sdk.Economy.getCurrencyBalance(["USERLEVEL"]).Balances[0].Balance;
try {
var ddnaCollectUrl = ddna.getCollectUrl();
if (ddnaCollectUrl !== null) {
var parameters = {
coinBalance : coinsBalance,
userLevel : userLevel
};
var body = {
eventName: "chilliConnectLogin",
userID: player.ChilliConnectID,
eventParams : parameters
};
var request = ChilliConnect.Http.Request.setJson(body);
var response = request.post(ddnaCollectUrl);
if(response) {
.
.
.
For convenience, the deltaDNA URL and environment keys for this game are loaded from a module created in ChilliConnect Cloud Code
The Endpoints module:
- defines the deltaDNA Collect & Engage URL endpoints
- defines the deltaDNA Environment Keys
- defines the current Environment to use (DEV or LIVE)
- Provides a helper function to get the complete URL to POST Collect events or Engage campaign requests to.
The endpoint and environment key values for your game can found on your game details page in deltaDNA
// Add the URL and Environments from your 'Game Details' page on deltaDNA
const collect = "https://collect16104ttrlc.deltadna.net/collect/api";
const engage = "https://engage16104ttrlc.deltadna.net";
const devKey = "00938463175409625971347052615752";
const liveKey = "00938473891762545150089251015752";
const mode = "DEV";
.
.
.
// Exports
module.exports.collectUrl = collect;
module.exports.engageUrl = engage;
module.exports.devEnvironmentKey = devKey;
module.exports.liveEnvironmentKey = liveKey;
module.exports.mode = mode;
module.exports.getCollectUrl = getCollectUrl ;
module.exports.getEngageUrl = getEngageUrl ;
It is common practice for the configuration of game variables, currencies, store, inventory and levels to be loaded from a remote CDN so they can be easily updated without requiring a game update.
In this example, the player's coin blance and highest level reached are implemented as server authoratative currencies and loaded at the start of the game from the ChilliConnect Catalog as Currencies.
chilliConnect.Economy.GetCurrencyBalance(new GetCurrencyBalanceRequestDesc()
, (request, response) =>
{
// Handle Currency Response from ChilliConnect
Debug.Log("Currency fetched: ");
foreach (var item in response.Balances)
{
switch (item.Key)
{
case "COINS":
playerCoins = item.Balance;
break;
case "USERLEVEL":
playerLevel = item.Balance;
break;
}
}
UpdatePlayerStatistics();
}
, (request, error) => Debug.LogError(error.ErrorDescription));
The same approach is used to configure the game's levels. They are loaded as a JSON object decsribing the cost, reward and difficulty settings for each level. The JSON level configiuration is implemented as a Metadata Catalog Item in ChilliConnect and downloaded at the start of each game session.
Debug.Log("Fetching metadata to configure game levels");
chilliConnect.Catalog.GetMetadataDefinitions(new GetMetadataDefinitionsRequestDesc()
, OnMetaDataFetched
, (request, error) => Debug.LogError(error.ErrorDescription)
);
// Handle Game Configuration repsonse from ChilliConnect
private void OnMetaDataFetched(GetMetadataDefinitionsRequest request, GetMetadataDefinitionsResponse response)
{
Debug.Log("Metadata fetched: ");
levels = new List<Level>();
foreach (MetadataDefinition metadataItem in response.Items)
{
var levelList = metadataItem.CustomData.AsDictionary().GetList("levels");
foreach (var level in levelList)
{
Level l = new Level();
if (level.AsDictionary().ContainsKey("food"))
l.food = level.AsDictionary().GetInt("food");
if (level.AsDictionary().ContainsKey("poison"))
l.poison = level.AsDictionary().GetInt("poison");
if (level.AsDictionary().ContainsKey("cost"))
l.cost = level.AsDictionary().GetInt("cost");
if (level.AsDictionary().ContainsKey("reward"))
l.reward = level.AsDictionary().GetInt("reward");
if (level.AsDictionary().ContainsKey("timelimit"))
l.timelimit = level.AsDictionary().GetInt("timelimit");
levels.Add(l);
}
}
Debug.Log("Levels Loaded " + levels.Count);
.
.
.
There will be occasions when you want to change an aspect of the game configuration for a subset of players, without necessarily wanting to impact all players.
Perhaps you want to change the difficulty of a particular level for new players, present a special offer to verteran players or promote a limited time event.
You may even want to A/B test different content and configurations to determine the optimal settings for different player segments.
In this example our analysis has identified that the 3rd level is too difficult for new players, so we have setup a deltaDNA Event triggered campaign to target players if they fail mission 3, on subsequent attemts it:
- reduces the cost of the level to the player
- reduces the number of items of food the player has to eat
- increases the level reward.
The campaign sends the following Game Parameter Action to the player. targeting New Players and is triggered if a missionFailed event with the missionID parameter equal to 003 is recorded. The following code in the client records all mission fails when the player dies.
public void PlayerDied()
{
player.Kill();
missionSummary.Show(PlayerManager.State.DEAD);
// Record DDNA MissionFailed event
DDNA.Instance.RecordEvent(new GameEvent("missionFailed")
.AddParam("missionName", "Mission " + currentLevel.ToString("D3"))
.AddParam("missionID", currentLevel.ToString("D3"))
.AddParam("userLevel", player.playerLevel)
.AddParam("isTutorial", false)
.AddParam("coinBalance", player.playerCoins)
.AddParam("foodRemaining", player.foodRemaining)
.AddParam("food", levels[currentLevel - 1].food)
.AddParam("poison", levels[currentLevel - 1].poison)
.AddParam("missionCost", levels[currentLevel - 1].cost)
.AddParam("missionReward", levels[currentLevel - 1].reward)
.AddParam("timelimit", levels[currentLevel - 1].timelimit))
.Run();
.
.
.
A common callback method has been specified to handle Game Parameter responses from Event Triggered Campaigns
// Event Triggered Campaigns configuration settings
DDNA.Instance.Settings.MultipleActionsForEventTriggerEnabled = true;
DDNA.Instance.NotifyOnSessionConfigured(true);
DDNA.Instance.OnSessionConfigured += (bool cachedConfig) => GetDDNAGameConfig(cachedConfig);
// Register Handlers for Event Triggered Campaign responses
DDNA.Instance.Settings.DefaultGameParameterHandler = new GameParametersHandler(gameParameters =>
{
MyGameParameterHandler(gameParameters);
});
DDNA.Instance.Settings.DefaultImageMessageHandler = new ImageMessageHandler(DDNA.Instance, imageMessage =>
{
MyImageMessageHandler(imageMessage);
});
It can handle all the parameters we may want to remotely override.
private void MyGameParameterHandler(Dictionary<string, object> gameParameters)
{
Debug.Log("Received deltaDNA gameParameters from event triggered campaign" + DeltaDNA.MiniJSON.Json.Serialize(gameParameters));
foreach (string key in gameParameters.Keys)
{
// Coin Balalnce Modifier
if (key == "coins")
{
int c = System.Convert.ToInt32(gameParameters[key]);
RewardReceived("coins", "Event Triggered Campaign reward", c);
}
// Level configuration modifiers
if (key == "food" || key == "poison" || key == "missionCost" || key == "missionReward" || key == "timelimit")
{
int v = System.Convert.ToInt32(gameParameters[key]);
Debug.Log(string.Format("Mission {0} {1} configuration changed to {2}", currentLevel, key, v));
missionModified(currentLevel, key, v);
switch (key)
{
case "food":
levels[currentLevel - 1].food = v;
break;
case "poison":
levels[currentLevel - 1].poison = v;
break;
case "missionCost":
levels[currentLevel - 1].cost = v;
break;
case "missionReward":
levels[currentLevel - 1].reward = v;
break;
case "timelimit":
levels[currentLevel - 1].timelimit = v;
break;
}
}
}
}
We even record a missionModified event so we can easily analyse the impact of any changes to a level.
public void missionModified(int missionID, string modifierType, int modifierAmount)
{
DDNA.Instance.RecordEvent(new GameEvent("missionModified")
.AddParam("missionID", missionID.ToString("D3"))
.AddParam("userLevel", player.playerLevel)
.AddParam("coinBalance", player.playerCoins)
.AddParam("food", levels[missionID-1].food)
.AddParam("poison", levels[missionID-1].poison)
.AddParam("missionCost", levels[missionID-1].cost)
.AddParam("missionReward", levels[missionID-1].reward)
.AddParam("timelimit", levels[missionID-1].timelimit))
.Run();
}
There may be occasions when you want to modify the game or personalize the player experience remotely in a server autoritative manner. You can do this by making deltaDNA Engage Campaign requests from your ChilliConnect Cloud Code and using the response to update some aspect of the game.
A typical scenario might involve a Cloud Code Event Script communicating with the deltaDNA decision point campaign API to retrieve Ad placement or IAP promo information.
However, we will show a more complex example to demonstrate additional commumication steps.
- The game client will call a ChilliConnect Cloud Code script.
- The Cloud Code script will make a request to the deltaDNA Engage Decision Point API.
- The Cloud Code script will pass the response back to the game client.
- The game client will parse and act on the response.
This scenario passes Ad Placemement info back to the game client so we can remotely control whether the player sees IAP Promo or Rewarded Ad placements, depending on the player segmentation conditions we remotely define in decision point campaigns.
e.g. A Game Parameter Action that would show the player Rewarded Ads worth 10 Coins on each failed level up to 10 times per session.
And a set of Decision Point Campaigns to promote different placements based on player segment.
The following code in the game client runs the remote Cloud Code script and deals with the response.
private void RemoteCampaign(string decisionPoint, string parameters)
{
var scriptParams = new Dictionary<string, SdkCore.MultiTypeValue>();
scriptParams.Add("decisionPoint", decisionPoint);
scriptParams.Add("locale", "en_GB");
scriptParams.Add("platform", DDNA.Instance.Platform);
if (!string.IsNullOrEmpty(parameters)) scriptParams.Add("parameters", parameters);
var runScriptRequest = new RunScriptRequestDesc("ENGAGE_DECISION_POINT_CAMPAIGN");
runScriptRequest.Params = scriptParams;
Debug.Log("Running Engage Campaign Script for decisionPoint : " + decisionPoint);
chilliConnect.CloudCode.RunScript(runScriptRequest
, (request, response) => {
var engageResponse = response.Output.AsDictionary();
if (engageResponse.ContainsKey("parameters"))
{
var p = engageResponse["parameters"].AsDictionary();
foreach (var i in p)
{
if (i.Key == "placementType")
placementManager.type = i.Value.AsString();
if (i.Key == "placementPosition")
placementManager.position = i.Value.AsString();
if (i.Key == "placementFrequency")
placementManager.frequency = i.Value.AsInt();
if (i.Key == "placementSessionCap")
placementManager.limit = i.Value.AsInt();
if (i.Key == "placementType")
placementManager.type = i.Value.AsString();
if (i.Key == "placementPromoID")
placementManager.promoID = i.Value.AsInt();
}
}
}
, (request, error) => Debug.LogError(error.ErrorDescription));
}
The Cloud Code script is implemented as am API script that requires 3 input parameters.
Like the event recording script it uses a common module to get the deltaDNA Engage API URL and Environment Key. It then build an Engage reuquest, POSTs it and passes the response back to the game client.
try {
var ddnaEngageUrl = ddna.getEngageUrl();
if (ChilliConnect.Request.decisionPoint && ChilliConnect.Request.platform && ddnaEngageUrl) {
var parameters = {};
var body = {
decisionPoint: ChilliConnect.Request.decisionPoint,
platform : ChilliConnect.Request.platform,
userID: player.ChilliConnectID,
version: "4",
parameters : parameters
};
ChilliConnect.Logger.info(ddnaEngageUrl);
var request = ChilliConnect.Http.Request.setJson(body);
var response = request.post(ddnaEngageUrl);
if(response) {
ChilliConnect.Logger.info(response.getBody());
return(response.getJSON());
}
}
}
deltaDNA Out Of Game campaigns are used for sending Push Notifications, Emails and Webhook requests via external API. They are ideal for messaging and remotely gifting currency or inventory items to a player when they are offline.
Out Of Game campaigns support powerful player segmentation, timing and localisation capabilities and can be used to automate CRM campaigns.
In this example we will remotely gift 100 coins to new players at 6:30pm in their local time-zone the day after they start playing. We will also send them a push notification informing them of the gift and encouraging them back to the game.
This is achieved by setting up an Engage Out Of Game Campaign that uses a Webhook action to make a POST to a ChilliConnect External Cloud Script. The Cloud script parses the data it receives, updates the player's currency balance and sends an event back to deltaDNA indicating that the gift was delivered.
The Webhook Action contains 3 parameters to specify the reward name type and amount. It contains the userID and environmentID to ensure the gift is given to the correct player and it contains the URL, encoding and HTTP method to be used to send the data to ChilliConnect Cloud Script.
An External Cloud Code script on ChilliConnect contains the code to deliver the gift. It also provides the URL that the Webhook Action should send data.
The code parses the Webhook request to make sure all the required parameters are present, then updates the player's currency balance.
ChilliConnect.Logger.info("Starting deltaDNA Out Of Game Campaign request");
var response = ChilliConnect.External.Response;
try {
var coinsBalance;
var userLevel ;
var player ;
// parse external POST for required parameters
var userID = ChilliConnect.External.Request.getQueryParam("userID");
var environmentID = ChilliConnect.External.Request.getQueryParam( "environmentID" );
var rewardType = ChilliConnect.External.Request.getQueryParam("rewardType");
var rewardAmount = Number(ChilliConnect.External.Request.getQueryParam("rewardAmount"));
var rewardName = ChilliConnect.External.Request.getQueryParam("rewardName");
// Check the request has a userID and an enviroment key that matches our game and a reward
if (userID && environmentID && (environmentID == ddna.devEnvironmentKey || environmntID == ddna.liveEnvironmntKey) && rewardType && rewardAmount) {
ChilliConnect.Logger.info("userID :" + userID + " environmentID : " + environmentID);
ChilliConnect.Logger.info("Reward :" + rewardAmount + " " + rewardType);
// Apply reward
if (rewardType == "COINS") {
// Increment Player Coin balance
sdk.PlayerAccounts.asPlayer( userID,
function() {
sdk.Economy.addCurrencyBalance("COINS", rewardAmount );
player = sdk.PlayerAccounts.getPlayerDetails();
coinsBalance = sdk.Economy.getCurrencyBalance(["COINS"]).Balances[0].Balance;
userLevel = sdk.Economy.getCurrencyBalance(["USERLEVEL"]).Balances[0].Balance;
}
);
An event is then sent to deltaDNA to record that the gift was successfully delivered to the player. This is done in the same way as the player login event discussed above.
// Send Feedback event to DDNA to record the fact that the reward was applied
var ddnaCollectUrl = ddna.getCollectUrl();
if (ddnaCollectUrl !== null) {
var parameters = {
coinBalance : coinsBalance,
userLevel : userLevel,
rewardName : rewardName,
rewardType : rewardType,
rewardAmount : rewardAmount
};
var body = {
eventName: "outOfGameReward",
userID: player.ChilliConnectID,
eventParams : parameters
};
var request = ChilliConnect.Http.Request.setJson(body);
var collectResponse = request.post(ddnaCollectUrl);
Finally, the WebHook action defined in the deltaDNA interface is added to a campaign that targets a segment containing players that installed the previous day.
The campaign is set to deliver the Webhook action and an iOS or Android push notification to the targetted players, at 6:30pm in their local timezone on the day after they started playing. A gameStarted event within the following 24 hours is used as a conversion event by which we can measure the success of the campaign in encouraging the player to return to the game.
More complex logic could be built into the campaign for example.
- A multi-step campaign could have delivered onboarding messages and gifts over several days.
- Conversion actions could be used for conditional rewards to further incentivize the player "Welcome to Snake, we've just gifted you 100 coins for playing yesterday, how will you spend them? Play today and receive a 200 coin gift tomorrow."
- Different rewards and messages can be A/B Tested
This tutorial has shown some common scenarios where deltaDNA and ChilliConnect are used to remotely manage the game ecosystem, player personalization and CRM.
Live-ops and CRM techniques are evolving rapidly, hopefully this tutorial shows you some of the building blocks that you will need to construct your next great idea, but be sure to get in touch with [email protected] if you are unsure how to implement it.