引き換え可能なクーポン

ゲームの目標指向の性質は、プレイヤーが通常、タスクの完了にゲーム内報酬の獲得を期待することを意味します。例えば、プレイヤーは以下を期待する可能性があります。

  • ボスを倒した後のアイテムと経験値の獲得
  • サイドクエストの完了に対する通貨の受け取り
  • 制作時の武器へのアップグレードの受け取り

また、ゲームはプレイヤーにゲーム内報酬を付与します。プロモーションまたはシーズンイベントの一環として、ゲーム内の問題の補償として、またはバグのレポートやベータテストのお礼の形式として、プレイヤーにゲーム内報酬を提供できます。

Cloud Code は、クーポンコードを検証し、他のサービスのゲーム内報酬アイテムを提供するスクリプトを作成することで、機能豊富なギフトシステムをビルドできます。ゲームがライブのときでも、ゲームクライアントの更新をリリースすることなくクーポンロジックを変更できます。変更を含む Cloud Code スクリプトの新バージョンの単純な公開は、新しい引き換えルールを施行するのに十分です。

ここでの例は、悪意のある動作をすべて防ぐものではありません。Cloud Code は、プレイヤーがゲームクライアントのコードを変更して他の Unity サービスを直接操作できるようになることを防止できません。本格的なサーバー主導型コードの記述は、Cloud Code で予定されている機能です。

Cloud Save での基本的な例

クーポンシステムには、クーポンコードの検証と、それを引き換えるプレイヤーの追跡という 2 つの主要な要件があります。これらの要件は、有効なクーポンをローカルに格納し、その後 Cloud Save を使用して、クーポンが引き換えられたかどうかを確認するスクリプトで満たすことができます。クーポンメタデータに失効日を追加し、Date ライブラリを使用してクーポンが失効したかどうかを確認することもできます。スクリプトの出力では、ゲーム内報酬が何であるかを説明できます。

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.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',
            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;
};

Economy の使用

Economy サービスを使用して、この例をさらに進めることができます。Economy サービスを使用して、ゲーム全体で インベントリアイテム通貨 を設定できます。Economy 設定が公開されたら、クーポンの検証後にスクリプトから直接アイテムと通貨の贈与を開始できます。

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.3');
const { CurrenciesApi, InventoryApi } = require('@unity-services/economy-2.4');
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, {
                amount: reward.amount,
            });
            break;
        case 'inventoryItem':
            await inventoryApi.addInventoryItem(projectId, playerId, addInventoryRequest: {
                inventoryItemId: reward.id,
            });
            break;
        default:
            throw Error('Invalid reward type');
    }
}

Remote Config のインテグレーション

この例の 1 つの欠点は、クーポンの設定しやすさです。新しいクーポンの作成または既存のクーポンの無効化にはコードの変更が必要であり、十分な柔軟性が提供されません。Remote Config でクーポンをカスタム JSON 値として定義できます。

Remote Config 値は、Cloud Code スクリプトで使用する前に保存する必要があります。この設定により、設計外部の人員がライブコードを変更する必要がなくなるため、クーポンを管理する場合の障壁が除去されます (リスクも軽減されます)。Remote Config とのインテグレーションにより、クーポンコードとゲーム内報酬を管理し、Remote Config キャンペーンに基づいて特別なクーポンを定義できます。

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.3');
const { CurrenciesApi, InventoryApi } = require('@unity-services/economy-2.4');
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, {
                amount: reward.amount,
            });
            break;
        case 'inventoryItem':
            await inventoryApi.addInventoryItem(projectId, playerId, addInventoryRequest: {
                inventoryItemId: reward.id,
            });
            break;
        default:
            throw Error('Invalid reward type');
    }
}