可兑换的优惠券

游戏以目标为导向的性质意味着玩家通常希望通过完成任务获得奖励。例如,玩家可能希望:

  • 击败 boss 后获得物品和经验值
  • 获得货币以完成支线任务
  • 在制作时获得武器升级

此外,游戏还为玩家提供游戏内奖励。例如,您可能希望为玩家提供游戏内奖励作为促销或季节性活动中的奖励,作为对游戏内问题的补偿,或作为对玩家报告错误和参与 Beta 测试的奖赏。

借助 Cloud Code,您可以编写脚本来验证优惠券代码以及在其他服务中提供奖励物品,从而构建功能丰富的礼品系统。即使游戏已上线也可以更改优惠券逻辑,无需发布游戏客户端更新。只需发布包含更改的新版 Cloud Code 脚本,即可实施新的兑换规则。

此处的示例并不能阻止所有恶意行为。Cloud Code 无法阻止玩家更改其游戏客户端的代码,因此他们能够直接与其他 Unity 服务进行交互。Cloud Code 已计划在未来支持编写完全合格的服务器授权代码。

使用 Cloud Save 的基本示例

优惠券系统有两个主要要求:验证优惠券代码和跟踪兑换了优惠券的玩家。为了满足这些要求,您可以使用脚本在本地存储有效优惠券,然后使用 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.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',
            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;
};

// Uncomment the code below to enable the inline parameter definition
// - Requires Cloud Code JS dev environment setup with NodeJS (https://docs.unity3d.com/Packages/com.unity.services.cloudcode@latest/index.html?subfolder=/manual/Authoring/javascript_project.html)
//
// module.exports.params = {
//   couponId: { type: "String", required: true },
// };

**注意:**如果您使用 Unity 编辑器来管理您的脚本,您可以取消注释脚本尾部的代码,以便声明所需的 couponId 脚本内参数。要了解有关如何管理脚本内参数的更多信息,请参阅在 Unity 编辑器中修改脚本参数。发布该脚本。

使用 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.4');
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');
    }
}

// Uncomment the code below to enable the inline parameter definition
// - Requires Cloud Code JS dev environment setup with NodeJS (https://docs.unity3d.com/Packages/com.unity.services.cloudcode@latest/index.html?subfolder=/manual/Authoring/javascript_project.html)
//
// module.exports.params = {
//   couponId: { type: "String", required: true },
// };

**注意:**如果您使用 Unity 编辑器来管理您的脚本,您可以取消注释脚本尾部的代码,以便声明所需的 couponId 脚本内参数。要了解有关如何管理脚本内参数的更多信息,请参阅在 Unity 编辑器中修改脚本参数。发布该脚本。

集成 Remote Config

此示例的一个缺点是优惠券的可配置性。创建新优惠券或使现有优惠券失效需要更改代码,这样就不能提供足够的灵活性。您可以在 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.4');
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');
    }
}

// Uncomment the code below to enable the inline parameter definition
// - Requires Cloud Code JS dev environment setup with NodeJS (https://docs.unity3d.com/Packages/com.unity.services.cloudcode@latest/index.html?subfolder=/manual/Authoring/javascript_project.html)
//
// module.exports.params = {
//   couponId: { type: "String", required: true },
// };

**注意:**如果您使用 Unity 编辑器来管理您的脚本,您可以取消注释脚本尾部的代码,以便声明所需的 couponId 脚本内参数。要了解有关如何管理脚本内参数的更多信息,请参阅在 Unity 编辑器中修改脚本参数。发布该脚本。