Use cases

Cloud Code as a backend service offers many possibilities, including writing simple scripts that aid in cheat prevention, implementing game mechanics, or interacting with other Unity services. This section outlines some of the scenarios for which you might want to use Cloud Code scripts.

Simple server time anti-cheat

Games are often built with some form of a timing element. Some examples of timing elements include:

  • An in-game item is only available for purchase from an auction that expires at a certain date.
  • A building cannot be built in the world of an RTS game until a cooldown has been reached.
  • A non-player character (NPC) only appears after the first few days of starting the game.

The simplest way to build this logic is to use the device time. However, using the device time can come with its own set of challenges.

In the auction example, it would be very difficult to synchronize the end of the bidding across multiple devices in multiple time zones. Doing so might result in some players having more time to outbid others or gather additional resources for the item. Furthermore, players would be able to cheat in the game by changing the clock on their device and thus increasing the time left on the auction. The unintended effects of relying on device time are not limited to malicious behavior (such as in the example of speeding up the cooldown) in a world-building game. Players might unintentionally end up with the wrong time on their device which might result in an NPC appearing at the wrong time, spoiling the user experience.

These examples do not prevent all malicious behavior. Cloud Code cannot prevent players from altering the code of their game clients which might enable them to interact with other Unity services directly. The writing of fully-fledged server-authoritative code is a planned feature for Cloud Code.

Basic example

Cloud Code offers a central "server time", you can use in your game logic. The following example shows a simple way to use Cloud Code's central server time:

Copy

JavaScript

/*
 * Return the current UNIX timestamp of the server and its UTC string representation
 */

module.exports = async () => {
    // Get the current server timestamp
    const unixTimestamp = Date.now();

    // Convert the timestamp to a JavaScript Date object
    const currentDate = new Date(unixTimestamp);

    // Create a Universal Coordinate Time (UTC) string
    const utcString = currentDate.toUTCString();

    // Return both the timestamp and the formatted string back to the game client
    return {
        timestamp: unixTimestamp,
        formattedDate: utcString,
    };
};

Leveraging Cloud Save

You can take the world-building example a step further by including Cloud Save. You can set the cooldown before a building becomes available as a UNIX timestamp in a key in Cloud Save:

Copy

JavaScript

/*
 * Set the cooldown for a building with id "MUSEUM_BUILDING" in Cloud Save
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');

// The Cloud Save key for the building
const MUSEUM_KEY = 'MUSEUM_BUILDING';

// The cooldown before the "MUSEUM_BUILDING" is allowed to be built again.
const MUSEUM_COOLDOWN = 30;

module.exports = async ({ context, logger }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    // Get the current server timestamp and calculate when the cooldown is finished (in milliseconds)
    const currentTimestamp = Date.now();
    const expiresAt = currentTimestamp + MUSEUM_COOLDOWN * 1000;

    // Save the data in Cloud Save
    try {
        await cloudSaveApi.setItem(projectId, playerId, { key: MUSEUM_KEY, value: expiresAt });
    } catch (error) {
        let errorMessage;

        if (error.response) {
            // If the error is from the Cloud Save server
            errorMessage = JSON.stringify({
                response: error.response.data,
                status: error.response.status,
                headers: error.response.headers,
            });
        } else {
            // If the error is from the script
            errorMessage = JSON.stringify(error.message);
        }

        logger.error(
            `Could not save the key ${MUSEUM_KEY} in Cloud Save. Got error ${errorMessage}`
        );
        // Return an error to the game client
        throw Error(`An error occurred when setting the cooldown of the ${MUSEUM_KEY}`);
    }

    // Return the current server time and when the cooldown expires
    return {
        serverTimestamp: currentTimestamp,
        cooldown: {
            durationSeconds: MUSEUM_COOLDOWN,
            expiresAt: expiresAt,
        },
    };
};

When a user attempts to place the building in their world, you can use a Cloud Code script to verify that the cooldown has finished:

Copy

JavaScript

/*
 * Validate that the cooldown for a building with id "MUSEUM_BUILDING" in Cloud Save has expired
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');

// The Cloud Save key for the building
const MUSEUM_KEY = 'MUSEUM_BUILDING';

module.exports = async ({ context, logger }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    const currentTimestamp = Date.now();
    let cooldown;

    // Get the data from Cloud Save
    try {
        let { data: response } = await cloudSaveApi.getItems(projectId, playerId, [MUSEUM_KEY]);
        cooldown = response?.results?.[0]?.value ?? 0;
    } catch (error) {
        let errorMessage;

        if (error.response) {
            // If the error is from the Cloud Save server
            errorMessage = JSON.stringify({
                response: error.response.data,
                status: error.response.status,
                headers: error.response.headers,
            });
        } else {
            // If the error is from the script
            errorMessage = JSON.stringify(error.message);
        }

        logger.error(
            `Could not get the key ${MUSEUM_KEY} in Cloud Save. Got error ${errorMessage}`
        );
        // Return an error to the game client
        throw Error(`An error occurred when getting the cooldown of the ${MUSEUM_KEY}`);
    }

    // Return a response to the client which indicates if the building can be built and if not, how long is left on the cooldown
    const timeRemaining = (cooldown - currentTimestamp) / 1000;
    const hasExpired = timeRemaining < 0 ? true : false;

    return {
        serverTimestamp: currentTimestamp,
        cooldown: {
            hasExpired: hasExpired,
            timeRemaining: hasExpired ? 0 : timeRemaining,
        },
    };
};

Parameterize the scripts and include Remote Config

Script parameters are a useful feature of Cloud Code that can make the script more generic and reusable for different building types. You can also use Lodash to more easily iterate over objects and arrays.

You can make the script even more configurable by including Remote Config. You can create an integer setting for each building in your game using a ${BUILDING_ID}_COOLDOWN key. Then, you can set the values for each cooldown and save the configuration.

After saving the Remote Config settings, you can set the cooldown of a building from a Cloud Code script:

Copy

JavaScript

/*
 * Use a script parameter "builidngId" for fetching the cooldown settings for a building from Remote Config.
 * Calculate when the cooldown for a building will expire and set the timestamp in Cloud Save.
 * Note: The Remote Config settings need to be saved before they become available to a Cloud Code script.
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');
const { SettingsApi } = require('@unity-services/remote-config-1.1');
const _ = require('lodash-4.17');

module.exports = async ({ params, context, logger }) => {
    // Initialize Cloud Save and RemoteConfig API clients using the player credentials
    const { projectId, playerId, environmentId } = context;
    const cloudSaveApi = new DataApi(context);
    const remoteConfig = new SettingsApi(context);

    const currentTimestamp = Date.now();
    let configuredCooldown;
    let expiresAt;

    try {
        // Get the cooldown value for the "buildingId" from RemoteConfig
        const remoteConfigCooldownId = `${params.buildingId}_COOLDOWN`;

        const remoteConfigResponse = await remoteConfig.assignSettingsGet(
            projectId,
            environmentId,
            'settings',
            [remoteConfigCooldownId]
        );

        // Validate the value from RemoteConfig, for example "MUSEUM_BUILDING_COOLDOWN"
        configuredCooldown =
            remoteConfigResponse?.data?.configs?.settings?.[remoteConfigCooldownId];
        if (!configuredCooldown || !_.isNumber(configuredCooldown)) {
            throw Error(`Invalid value for ${remoteConfigCooldownId}`);
        }

        // Calculate when the cooldown expires for the "buildingId"
        expiresAt = currentTimestamp + configuredCooldown * 1000;

        // Save the data in Cloud Save
        await cloudSaveApi.setItem(projectId, playerId, {
            key: params.buildingId,
            value: expiresAt,
        });
    } catch (error) {
        let errorMessage;

        if (error.response) {
            // If the error is from one of the API clients
            errorMessage = JSON.stringify({
                response: error.response.data,
                status: error.response.status,
                headers: error.response.headers,
            });
        } else {
            // If the error is from the script
            errorMessage = JSON.stringify(error.message);
        }

        logger.error(
            `An error occurred when trying to set the cooldown for building id ${params.buildingId}. Got error: ${errorMessage}`
        );
        // Return an error to the game client
        throw Error(`An error occurred when setting the cooldown of the ${params.buildingId}`);
    }

    // Return the current server time and when the cooldown expires
    return {
        serverTimestamp: currentTimestamp,
        cooldown: {
            durationSeconds: configuredCooldown,
            expiresAt: expiresAt,
        },
    };
};

You can also validate the cooldown using the script's building ID parameter:

Copy

JavaScript

/*
 * Validate that the cooldown for a building with id passed as a Script parameter "buildingId" in Cloud Save has expired
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');

module.exports = async ({ params, context, logger }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    const currentTimestamp = Date.now();
    let cooldown;

    // Get the data from Cloud Save
    try {
        let { data: response } = await cloudSaveApi.getItems(projectId, playerId, [
            params.buildingId,
        ]);
        cooldown = response?.results?.[0]?.value ?? 0;
    } catch (error) {
        let errorMessage;

        if (error.response) {
            // If the error is from the Cloud Save server
            errorMessage = JSON.stringify({
                response: error.response.data,
                status: error.response.status,
                headers: error.response.headers,
            });
        } else {
            // If the error is from the script
            errorMessage = JSON.stringify(error.message);
        }

        logger.error(
            `Could not get the key ${params.buildingId} in Cloud Save. Got error ${errorMessage}`
        );
        // Return an error to the game client
        throw Error(`An error occurred when getting the cooldown of the ${params.buildingId}`);
    }

    // Return a response to the client which indicates if the building can be built and if not, how long is left on the cooldown
    const timeRemaining = (cooldown - currentTimestamp) / 1000;
    const hasExpired = timeRemaining < 0 ? true : false;

    return {
        serverTimestamp: currentTimestamp,
        cooldown: {
            hasExpired: hasExpired,
            timeRemaining: hasExpired ? 0 : timeRemaining,
        },
    };
};

Redeemable coupons

The goal-oriented nature of games means players usually expect to earn rewards for completing tasks. For example, players might expect to:

  • Earn items and experience after defeating a boss
  • Receive currency for completing a side quest
  • Receive an upgrade to their weapons when crafting

Additionally, games give players in-game rewards. You might want to offer your players in-game rewards as part of a promotional or seasonal event, as a reimbursement for an in-game issue, or as a form of recognition for reporting bugs and beta testing.

Cloud Code enables you to build a feature-rich gifting system by writing scripts to validate coupon codes and offer reward items in other services. You can alter coupon logic even when the game is live, without releasing a game client update. Simply publishing a new version of a Cloud Code script with the changes is enough to enforce the new redeeming rules.

The examples here do not prevent all malicious behavior. Cloud Code cannot prevent players from altering the code of their game clients which can enable them to interact with other Unity services directly. The writing of fully-fledged server-authoritative code is a planned feature for Cloud Code.

Basic example with Cloud Save

A coupon system has two main requirements: validating a coupon code and keeping track of which player has redeemed it. You can meet these requirements with a script that stores the valid coupons locally, then using Cloud Save to check if a coupon has been redeemed. You can also include an expiration date in the coupon metadata and check if a coupon has expired using the Date library. The output of the script can describe what the reward is:

Copy

JavaScript

/*
 * Verify that a coupon passed as a script parameter "couponId" is valid, has not expired and has not been redeemed by using Cloud Save.
 * Return the reward and make the coupon invalid for that user.
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');
const _ = require('lodash-4.17');

const CLOUD_SAVE_COUPONS_KEY = 'REDEEMED_COUPONS';
const VALID_COUPONS = [
    // Gift the player 10 coins
    {
        id: 'FREECOINS10',
        reward: {
            id: 'coins',
            amount: 10,
        },
        expiresAt: new Date(2021, 09, 29),
    },
    // Gift the player a rare armor
    {
        id: 'RAREARMOR1',
        reward: {
            id: 'rare-armor',
            amount: 1,
        },
        expiresAt: new Date(2021, 09, 29),
    },
];

module.exports = async ({ params, context }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    // Validate that the script parameter "couponId" is one of the valid coupons and select the corresponding coupon configuration
    const inputCouponId = _.toUpper(params.couponId);
    const coupon = _.find(VALID_COUPONS, function (c) {
        return c.id === inputCouponId;
    });

    if (!coupon) {
        throw Error(`Invalid coupon "${params.couponId}"`);
    }

    //Check that the coupon has not expired
    if (Date.now() > coupon.expiresAt.getTime()) {
        throw Error(`The coupon "${coupon.id}" has expired`);
    }

    // Get the redeemed coupons from Cloud Save
    const cloudSaveGetResponse = await cloudSaveApi.getItems(projectId, playerId, [
        CLOUD_SAVE_COUPONS_KEY,
    ]);

    const redeemedCouponsData = _.find(cloudSaveGetResponse?.data?.results, function (r) {
        return r.key === CLOUD_SAVE_COUPONS_KEY;
    });

    const redeemedCoupons = redeemedCouponsData?.value ?? [];

    // Check if the coupon has been redeemed
    if (redeemedCoupons && _.indexOf(redeemedCoupons, coupon.id) >= 0) {
        throw Error(`The coupon "${coupon.id}" has already been redeemed`);
    }

    // Add the coupon id to the array of redeemed coupons and save it back into Cloud Save
    redeemedCoupons.push(coupon.id);
    await cloudSaveApi.setItem(projectId, playerId, {
        key: CLOUD_SAVE_COUPONS_KEY,
        value: redeemedCoupons,
    });

    // Return the reward for the coupon
    return coupon.reward;
};

Using Economy

You can take this example further by using the Economy service. With the Economy service, you can configure Inventory Items and Currencies to use throughout your game. When your Economy configuration is published, you can start gifting items and currencies directly from the script after validating the coupon:

Copy

JavaScript

/*
 * Verify that a coupon passed as a script parameter "couponId" is valid, has not expired and has not been redeemed by using Cloud Save.
 * Gift the appropriate reward in Economy.
 * Note: the Economy configuration needs to be published before it is available from Cloud Code scripts
 *
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');
const { CurrenciesApi, InventoryApi } = require('@unity-services/economy-2.3');
const _ = require('lodash-4.17');

const CLOUD_SAVE_COUPONS_KEY = 'REDEEMED_COUPONS';
const VALID_COUPONS = [
    // Gift the player 10 coins
    {
        id: 'FREECOINS10',
        reward: {
            id: 'COINS',
            type: 'currency',
            amount: 10,
        },
        expiresAt: new Date(2021, 09, 29),
    },
    // Gift the player a rare armor
    {
        id: 'RAREARMOR1',
        reward: {
            id: 'RARE_ARMOR',
            type: 'inventoryItem',
            amount: 1,
        },
        expiresAt: new Date(2021, 10, 21),
    },
];

module.exports = async ({ params, context }) => {
    // Initialize a Cloud Save API client using the player credentials
    const { projectId, playerId } = context;
    const cloudSaveApi = new DataApi(context);

    // Validate that the script parameter "couponId" is one of the valid coupons and select the corresponding coupon configuration
    const inputCouponId = _.toUpper(params.couponId);
    const coupon = _.find(VALID_COUPONS, function (c) {
        return c.id === inputCouponId;
    });

    if (!coupon) {
        throw Error(`Invalid coupon "${params.couponId}"`);
    }

    //Check that the coupon has not expired
    if (Date.now() > coupon.expiresAt.getTime()) {
        throw Error(`The coupon "${coupon.id}" has expired`);
    }

    // Get the redeemed coupons from Cloud Save
    const cloudSaveGetResponse = await cloudSaveApi.getItems(projectId, playerId, [
        CLOUD_SAVE_COUPONS_KEY,
    ]);

    const redeemedCouponsData = _.find(cloudSaveGetResponse?.data?.results, function (r) {
        return r.key === CLOUD_SAVE_COUPONS_KEY;
    });

    const redeemedCoupons = redeemedCouponsData?.value ?? [];

    // Check if the coupon has been redeemed
    if (redeemedCoupons && _.indexOf(redeemedCoupons, coupon.id) >= 0) {
        throw Error(`The coupon "${coupon.id}" has already been redeemed`);
    }

    // Gift the coupon reward to the player
    await redeemCouponReward(coupon.reward, projectId, playerId, context);

    // Add the coupon id to the array of redeemed coupons and save it back into Cloud Save
    redeemedCoupons.push(coupon.id);
    await cloudSaveApi.setItem(projectId, playerId, {
        key: CLOUD_SAVE_COUPONS_KEY,
        value: redeemedCoupons,
    });

    // Return the reward for the coupon
    return {
        id: coupon.reward.id,
        amount: coupon.reward.amount,
    };
};

// Gift the reward to a player based on the reward.type property
async function redeemCouponReward(reward, projectId, playerId, context) {
    // Initialize a Economy API clients using the player credentials
    const currenciesApi = new CurrenciesApi(context);
    const inventoryApi = new InventoryApi(context);

    // Gift the reward
    switch (reward.type) {
        case 'currency':
            await currenciesApi.incrementPlayerCurrencyBalance(projectId, playerId, reward.id, {
                currencyId: reward.id,
                amount: reward.amount,
            });
            break;
        case 'inventoryItem':
            await inventoryApi.addInventoryItem(projectId, playerId, addInventoryRequest: {
                inventoryItemId: reward.id,
            });
            break;
        default:
            throw Error('Invalid reward type');
    }
}

Integrating Remote Config

One shortcoming of this example is the configurability of coupons. Making a new coupon or invalidating existing ones requires code changes, which does not provide enough flexibility. You can define coupons in Remote Config as custom JSON values:

You must save Remote Config values before using them in Cloud Code scripts. This configuration removes the barrier for people outside of engineering to managing coupons, as they would no longer need to alter live code (which also reduces risk). Integrating with Remote Config enables you to manage coupon codes and rewards, and define special coupons based on Remote Config Campaigns.

Copy

JavaScript

/*
 * Retrieve coupon details from Remote Config.
 * Verify that a coupon passed as a script parameter "couponId" is valid, has not expired and has not been redeemed by using Cloud Save.
 * Gift the appropriate reward in Economy.
 * Note: the Economy configuration needs to be published and Remote Config values need to be saved before they are available in Cloud Code scripts.
 *
 */

const { DataApi } = require('@unity-services/cloud-save-1.2');
const { CurrenciesApi, InventoryApi } = require('@unity-services/economy-2.3');
const { SettingsApi } = require('@unity-services/remote-config-1.1');
const _ = require('lodash-4.17');

const CLOUD_SAVE_COUPONS_KEY = 'REDEEMED_COUPONS';
const REMOTE_CONFIG_COUPONS_KEY = 'COUPONS';

module.exports = async ({ params, context }) => {
    // Initialize the Cloud Save and Remote Config API clients using the player credentials
    const { projectId, environmentId, playerId } = context;
    const cloudSaveApi = new DataApi(context);
    const remoteConfig = new SettingsApi(context);

    // Fetch the available coupons from Remote Config
    const remoteConfigResponse = await remoteConfig.assignSettingsGet(
        projectId,
        environmentId,
        'settings',
        [REMOTE_CONFIG_COUPONS_KEY]
    );
    const availableCoupons =
        remoteConfigResponse?.data?.configs?.settings?.[REMOTE_CONFIG_COUPONS_KEY];

    // Validate that the script parameter "couponId" is one of the valid coupons and select the corresponding coupon configuration
    const inputCouponId = _.toUpper(params.couponId);
    const coupon = _.find(availableCoupons, function (c) {
        return c.id === inputCouponId;
    });

    if (!coupon) {
        throw Error(`Invalid coupon "${params.couponId}"`);
    }

    //Check that the coupon has not expired
    if (Date.now() > coupon.expiresAt) {
        throw Error(`The coupon "${coupon.id}" has expired`);
    }

    // Get the redeemed coupons from Cloud Save
    const cloudSaveGetResponse = await cloudSaveApi.getItems(projectId, playerId, [
        CLOUD_SAVE_COUPONS_KEY,
    ]);

    const redeemedCouponsData = _.find(cloudSaveGetResponse?.data?.results, function (r) {
        return r.key === CLOUD_SAVE_COUPONS_KEY;
    });

    const redeemedCoupons = redeemedCouponsData?.value ?? [];

    // Check if the coupon has been redeemed
    if (redeemedCoupons && _.indexOf(redeemedCoupons, coupon.id) >= 0) {
        throw Error(`The coupon "${coupon.id}" has already been redeemed`);
    }

    // Gift the coupon reward to the player
    await redeemCouponReward(coupon.reward, projectId, playerId, context);

    // Add the coupon id to the array of redeemed coupons and save it back into Cloud Save
    redeemedCoupons.push(coupon.id);
    await cloudSaveApi.setItem(projectId, playerId, {
        key: CLOUD_SAVE_COUPONS_KEY,
        value: redeemedCoupons,
    });

    // Return the reward for the coupon
    return {
        id: coupon.reward.id,
        amount: coupon.reward.amount,
    };
};

// Gift the reward to a player based on the reward.type property
async function redeemCouponReward(reward, projectId, playerId, context) {
    // Initialize a Economy API clients using the player credentials
    const currenciesApi = new CurrenciesApi(context);
    const inventoryApi = new InventoryApi(context);

    // Gift the reward
    switch (reward.type) {
        case 'currency':
            await currenciesApi.incrementPlayerCurrencyBalance(projectId, playerId, reward.id, {
                currencyId: reward.id,
                amount: reward.amount,
            });
            break;
        case 'inventoryItem':
            await inventoryApi.addInventoryItem(projectId, playerId, addInventoryRequest: {
                inventoryItemId: reward.id,
            });
            break;
        default:
            throw Error('Invalid reward type');
    }
}

Daily rewards

Daily rewards are becoming an increasingly popular game mechanic used across all genres of games, and are especially popular with free-to-play titles. They offer an engaging way of bringing players back every day while letting them earn a variety of items, ranging from purely cosmetic skins and avatars to inventory items and in-game currency.

Cloud Code's ability to easily integrate with other Unity services makes it an excellent choice for building the logic around daily rewards. You can define items and currencies in Economy, probabilities in Remote Config, and write the underlying algorithm in Cloud Code. This allows you to alter the logic of daily rewards on the fly without the need to update the game client.

For example, you can:

  • Reduce the availability of some items
  • Increase the number of items that get rewarded
  • Include additional validation that can prevent players from receiving duplicates of rare items
  • Alter the items and probabilities through Remote Config Campaigns

Setup

The following example shows how to define multiple pet toys as inventory items in Economy. You can define the daily reward parameters in Remote Config before tying it all together with a Cloud Code script.

Economy

The first step in creating this game mechanic is to define the rewards with the Inventory Item type in Economy:

You must publish the Economy configuration before using it in Cloud Code scripts.

Remote Config

You can use Remote Config to define the daily rewards parameters as a custom JSON value under the key DAILY_REWARDS_VARIABLES:

Copy

JSON

{
  "rewards": [
    {"id": "TOY_CHOCO", "probability": 10},
    {"id": "TOY_JOJO", "probability": 65},
    {"id": "TOY_KIKI", "probability": 80}
  ]
}
Save the key-value pair so that it is available in Cloud Code scripts.

Daily rewards script

Once the configuration is in place, you can write a simple script that interacts with Economy and Remote Config to gift a random toy to the player using a simple weighted random algorithm:

Copy

JavaScript

/*
 * Retrieve the daily rewards configuration from Remote Config
 * Select a random item from the configuration and gift the item to the player in Economy
 * Note: the Economy configuration needs to be published and Remote Config values need to be saved before they are available in Cloud Code scripts.
 *
 */

const { InventoryApi } = require('@unity-services/economy-2.3');
const { SettingsApi } = require('@unity-services/remote-config-1.1');
const _ = require('lodash-4.17');

const REMOTE_CONFIG_VARIABLES_KEY = 'DAILY_REWARDS_VARIABLES';

module.exports = async ({ context }) => {
    // Initialize the Economy and Remote Config API clients using the player credentials
    const { projectId, environmentId, playerId } = context;
    const inventoryApi = new InventoryApi(context);
    const remoteConfigApi = new SettingsApi(context);

    // Fetch the daily rewards configuration from Remote Config
    const remoteConfigResponse = await remoteConfigApi.assignSettingsGet(
        projectId,
        environmentId,
        'settings',
        [REMOTE_CONFIG_VARIABLES_KEY]
    );

    // Extract the daily rewards from the response
    const dailyRewardsConfig = remoteConfigResponse?.data?.configs?.settings?.[REMOTE_CONFIG_VARIABLES_KEY];
    const dailyRewards = dailyRewardsConfig?.rewards;

    // Pick a random reward from the options
    const reward = selectRandomReward(dailyRewards);

    // Gift the reward to the player
    await inventoryApi.addInventoryItem({ projectId, playerId, addInventoryRequest: { inventoryItemId: reward.id } });
    return { reward: reward.id };
};

// Select a random item based on the probability of each option
function selectRandomReward(options) {
    if (!_.isArray(options)) {
        throw Error('Invalid daily reward options');
    }

    let sumOfProbabilities = 0;
    options.forEach((option) => {
        sumOfProbabilities += option.probability;
    });

    let random = _.random(0, sumOfProbabilities);

    for (let i = 0; i < options.length; i++) {
        if (random < options[i].probability) {
            return options[i];
        }
        random -= options[i].probability;
    }

    throw Error('Unable to select a daily reward');
}

Using Lobby

You can use Lobby and Cloud Code together to define common triggers in games. Consider the situation where a game lobby is ready to start a match. You could trigger the following script in response to a game lobby becoming ready. This example also features integration with Remote Config.

Before you follow this example, enable both Lobby and Cloud Code through the Unity Dashboard, and follow the steps to install the required SDKs. Create a lobby in your project using service authentication, and note its ID. You can do this through Cloud Code (see examples for Unity Lobby).

The following example gives the script two parameters: the service ID used to create the lobby, and the lobby ID. We get the lobby and then update it with several changes to its metadata. These changes are:

  • Locking the lobby so new players can no longer join.
  • Constructing a random “player order” for the game by using the lobby’s player IDs.
  • Randomly choosing a map for the game by using the LOBBY_CURRENT_MAPS config defined in Remote Config. The script returns a basic Boolean that indicates success or failure.
Copy
JavaScript
const _ = require("lodash-4.17");
const { LobbyApi, DataObjectVisibilityEnum } = require("@unity-services/lobby-1.2");
const { SettingsApi } = require("@unity-services/remote-config-1.1");
 
module.exports = async ({params,context,logger}) => {
    const { serviceId, lobbyId } = params;
    const { projectId, environmentId, playerId } = context;
    const lobbyApi = new LobbyApi(context);
    const remoteConfig = new SettingsApi(context);
   
    try {
        const {data: lobby} = await lobbyApi.getLobby(lobbyId, serviceId);
        return setUpGame(remoteConfig, projectId, environmentId, serviceId, lobbyApi, logger, lobby);
    } catch (err) {
       logger.error(`Failed to set up lobby: ${err.response.data.detail}`);
       return false;
    }
   
};
 
// Updates a lobby to set up for the start of a game, including reading a value from Remote Config to randomly assign to the lobby.
async function setUpGame(remoteConfig, projectId, environmentId, serviceId, lobbyApi, logger, lobby) {
    let playerIds = [];
    for (let i = 0; i < lobby.players.length; i++) {
        playerIds.push(lobby.players[i].id);
    }
     
    // Generate a turn order for the game by shuffling the player IDs.
    _.shuffle(playerIds);
   
    // Load all of the maps that are currently configured as available in Remote Config and pick a random one for this game.
    let maps = getCurrentMapsFromRemoteConfig(remoteConfig, projectId, environmentId)
    if (maps == null) {
      throw "Maps loaded from Remote Config are null.";
    }
    let gameMap = _.sample(maps);
   
    try {
      const updateResponse = await lobbyApi.updateLobby(
          lobby.id,
          serviceId,
          null,
          {
              isLocked: true, // Lock the lobby so that new players cannot join.
              hostId: playerIds[0], // Transfer host ownership from the service to one of the players.
              data: {
                  "playerOrder": {
                      value: playerIds.toString(),
                      visibility: DataObjectVisibilityEnum.Member // Set the visibility of the player order so that only lobby members can access it.
                  },
                  "map": {
                      value: gameMap,
                      visibility: DataObjectVisibilityEnum.Public // Set the visibility of the game map so that anyone who views the lobby can access it.
                  }
              }
          }
      );
     
      return true;
    } catch (err) {
      throw err;
    }
     
    return lobbyUpdateSuccess;
}
 
// Loads the Remote Config JSON value for LOBBY_CURRENT_MAPS and returns the array of maps inside of it.
// If the JSON is invalid, the return value is null.
async function getCurrentMapsFromRemoteConfig(remoteConfig, projectId, environmentId) {
  const currentMapsId = 'LOBBY_CURRENT_MAPS';
  const remoteConfigResponse = await remoteConfig.assignSettingsGet(
    projectId,
    environmentId,
    'settings',
    [currentMapsId]
  );
 
  let mapsJSON = remoteConfigResponse?.data?.configs?.settings?.[currentMapsId];
  if (!mapsJSON || !mapsJSON.maps || mapsJSON.maps.length == 0) {
    return null;
  }
       
  return mapsJSON.maps;
}

The Remote Config JSON here has a simple structure, but you can extend this example configuration to achieve more fine-grained control over your game. Likewise, there are a number of different triggers for Cloud Code that you might want to consider in addition to game setup, for example:

  • Triggering on lobby creation to adjust metadata values and sanitize strings.
  • Using Cloud Save player data to filter a player’s query results, approve/deny a join, and set metadata.
  • Using Economy data to set player or lobby metadata.