概览

CloudX Unity SDK 设置和核心功能概览

CloudX Unity SDK 使您能够在 iOS 和 Android 上通过横幅、MREC、插屏、激励和 App Open 广告实现 Unity 游戏变现。

安装

CloudX Unity SDK 以 .unitypackage 文件形式分发。

  1. CloudX Unity SDK 4.7.0 发布页 下载 CloudXSdk-4.7.0.unitypackage
  2. 在 Unity 中,转到 Assets > Import Package > Custom Package
  3. 选择下载的 .unitypackage 文件
  4. 提示时导入所有资源

示例应用

cloudx-io/cloudx-unity 仓库本身也是一个可运行的 Unity 演示项目,展示了 Banner、MREC、插屏和激励广告的完整接入。它内置 CloudX 演示后台 ID,无需账号即可运行。

如需接入您自己的 CloudX 应用,请替换 Assets/Scripts/DemoConfig.cs 中的 app key 和广告位 ID,然后在 Project Settings > Player > Identification 中设置该应用注册的 bundle identifier。竞价请求按 app key 和 bundle identifier 授权,两者都必须与您后台的应用一致,否则 SDK 将无法获得填充。

广告网络适配器

CloudX SDK 需要广告网络适配器来投放广告。在 Assets/CloudXSdk/Editor/CloudXDependencies.xml 中取消注释相应行即可启用。各适配器的详细设置(包括 iOS Info.plist 要求),请参阅适配器章节。

初始化

在加载任何广告之前初始化 SDK。您可以在初始化之前选择性地配置用户和应用属性。

using CloudX;
using UnityEngine;

public class MyGameManager : MonoBehaviour
{
    void Start()
    {
        InitializeCloudX();
    }

    void InitializeCloudX()
    {
        // 初始化前配置(可选)
        CloudXSdk.SetHashedUserId("hashed-user-id");
        CloudXSdk.SetUserKeyValue("user_level", "premium");
        CloudXSdk.SetAppKeyValue("app_version", "1.0.0");

        // 在初始化之前订阅回调
        CloudXInitializationCallbacks.OnSdkInitializedEvent += OnSdkInitialized;
        CloudXInitializationCallbacks.OnSdkInitializationFailedEvent += OnSdkInitializationFailed;

        // 初始化 SDK
        var config = CloudXInitializationConfiguration.Create("YOUR_APP_KEY").Build();
        CloudXSdk.Initialize(config);
    }

    private void OnSdkInitialized(CloudXSdkConfiguration config)
    {
        Debug.Log("CloudX SDK 初始化成功");
        // 现在可以加载广告
    }

    private void OnSdkInitializationFailed(CloudXError error)
    {
        Debug.LogError($"SDK 初始化失败: {error}");
    }
}

广告格式

CloudX Unity SDK 支持横幅、MREC、插屏、激励和 App Open 广告集成。请查阅各格式专属指南了解实现详情:

高级功能

回调线程

CloudX 的所有回调 — 初始化、广告生命周期、Trusted Arbiter 以及 OnAdRevenuePaid — 都是从原生线程发出的。CloudXSdk.InvokeEventsOnUnityMainThread 决定 CloudX 是先把回调交给 Unity 主线程,还是直接在发出回调的线程上调用它。

// 可选;在 Initialize() 之前设置可同时覆盖初始化回调
CloudXSdk.InvokeEventsOnUnityMainThread = true;
行为适用场景
未设置(默认)所有回调都在 Unity 主线程上运行,只有全屏广告(插屏、App Open、激励)的 OnAdRevenuePaid 例外,它在后台线程上送达。横幅和 MREC 的收入回调在主线程上。希望回调里可以调用 Unity API,同时收入上报在展示发生时就触发。
true所有回调都在 Unity 主线程上运行,包括全屏广告的 OnAdRevenuePaid收入回调里还要更新游戏内容(UI、GameObject、协程)。
false所有回调都在原生回调线程上就地调用。回调里不得使用 Unity API。需要在事件发生的当下就收到,并且自行调度回主线程。
  • 该属性在每次回调时读取,因此可以随时修改。要覆盖初始化回调,请在 Initialize() 之前设置。
  • 回调抛出异常时会被捕获并以 ERROR 级别记录;该事件的其他订阅者以及后续广告生命周期都不受影响。

为什么全屏收入回调默认走后台线程

主线程回调是在 Unity 的 Update() 里派发的。全屏广告在前台的时候 Unity 播放器可能处于暂停状态,Update() 不会执行,排入主线程队列的内容可能要等广告关闭后才送达。全屏 OnAdRevenuePaid 改为在原生线程送达,才能让您的收入统计(分析、MMP 转发)在展示发生时就收到信号,而不是等广告关闭之后。

这是 Unity 本身的限制,不是 CloudX 特有的:任何您要求在 Unity 主线程上运行的回调,只要是在全屏广告展示期间产生的,都可能要等播放器恢复。OnAdShowSuccess 有时候等广告关闭之后才回调,就是同一个原因,属于预期行为。

如何在事件发生的当下收到回调

设置 false,然后自行把工作排队。只有排队的 lambda 在主线程上运行,回调本身会立刻返回给 CloudX。

using System;
using System.Collections.Concurrent;
using CloudX;
using UnityEngine;
using UnityEngine.UI;

public class AdEventPump : MonoBehaviour
{
    [SerializeField] private Text revenueLabel;

    private readonly ConcurrentQueue<Action> _pending = new ConcurrentQueue<Action>();

    private void Awake()
    {
        DontDestroyOnLoad(gameObject);
        CloudXSdk.InvokeEventsOnUnityMainThread = false;

        CloudXAdsCallbacks.Interstitial.OnAdRevenuePaid += ad =>
        {
            // 在原生线程上运行,展示发生时就到。这里不要调用 Unity API。
            MyAnalytics.TrackRevenue(ad.AdUnitId, ad.NetworkName, ad.Revenue);
            _pending.Enqueue(() => revenueLabel.text = $"Revenue {ad.Revenue:F4}");
        };
    }

    private void Update()
    {
        while (_pending.TryDequeue(out var action))
        {
            action();
        }
    }
}

排查

现象原因和处理
全屏回调(OnAdShowSuccessOnAdRevenuePaid)等广告关闭之后才到预期行为:Unity 播放器可能在广告后面被暂停,主线程送达因此要等待。如果需要更早收到,设置 false 并自行调度。
日志里出现 Caught exception in publisher event: <event>您的回调抛了异常。这行日志会给出事件名、订阅的方法,以及它是不是在 Unity 主线程上送达的。回调在非主线程上调用 Unity API 就会抛在这里 — 设置 true,或者把这部分工作挪到 Update() 队列里。
日志里出现 UnityMainThreadDispatcher does not existCloudX 的派发 GameObject 被 App 销毁了。之后回调会在原生线程上送达,因此任何调用 Unity API 的回调都会抛异常。请不要销毁 CloudX 的 DontDestroyOnLoad 对象。

事件不会被悄悄丢弃:要么在您要求的线程上送达,要么在原生线程上送达并附一条说明原因的 ERROR 日志。唯一的例外是派发 GameObject 被销毁,此时它已经排入主线程队列、尚未执行的内容会被丢弃。

隐私控制

如果您的应用没有使用 CMP,可以在初始化之前手动覆盖隐私状态。

// 在 Initialize() 之前可选地设置手动隐私覆盖
CloudXSdk.SetHasUserConsent(true);
CloudXSdk.SetDoNotSell(false);

// 传入 null 可清除手动覆盖,重新交由 CMP 或 IAB 信号决定
CloudXSdk.SetHasUserConsent(null);
CloudXSdk.SetDoNotSell(null);
  • SetHasUserConsent(bool?) 用于设置 GDPR 同意覆盖。
  • SetDoNotSell(bool?) 用于设置 CCPA do-not-sell 覆盖。
  • 如果存在 IAB 同意或隐私信号,它们会优先于这些手动覆盖值。

iOS ATT 使用说明文案

从 Unity SDK 2.2.4 开始,如果您的应用尚未在 Info.plist 中定义 NSUserTrackingUsageDescription,CloudX 的 iOS 后处理步骤会自动添加该字段。

  • 默认值:This uses device info for more personalized ads and content
  • 如果您已经提供了 NSUserTrackingUsageDescription,CloudX 会保留您现有的值不变。
  • 如果您希望展示自定义的 ATT 提示文案,请在 Info.plist 中显式设置自己的值。

用户定向

配置用户和应用属性以获得更好的广告定向。在 Initialize 之前调用这些方法。

// 设置哈希用户 ID
CloudXSdk.SetHashedUserId("hashed-user-id-12345");

// 设置用户级键值对
CloudXSdk.SetUserKeyValue("user_level", "premium");
CloudXSdk.SetUserKeyValue("age_group", "25-34");

// 设置应用级键值对
CloudXSdk.SetAppKeyValue("app_version", "1.0.0");
CloudXSdk.SetAppKeyValue("build_number", "123");

// 清除所有自定义键值对
CloudXSdk.ClearAllKeyValues();

哈希用户 ID 是由发布商提供的假名标识符。当适用的隐私信号允许时,CloudX 会在请求展示活动导出中以 hashed_user_id 返回竞价时记录的值。使用 auction_id 关联这些导出。您可以使用该 ID 将 CloudX 活动与自己的用户数据分群关联。未设置 ID、隐私信号阻止持久化或值超过 128 个字符时,导出值为空。请勿传递未经哈希的个人数据。

身份透传

通过 SetUserKeyValue 传入 UID 2.0、EUID、LiveRamp、ID5 和 Intent IQ。有值时设置一次,刷新后再设一次。

传入内容
uidapi.comUID2 广告 token,不要传 refresh token。不要解密。UID2 文档
euid.euEUID 广告 token,不要传 refresh token。不要解密。EUID 文档
liveramp.comLiveRamp ATS 信封,不要传 RampID。LiveRamp 文档
id5-sync.comID5 通用 UID。不要传 0ID5 文档
intentiq.comIntent IQ ID(IIQ ID),来自你的 Intent IQ 集成。Intent IQ 文档

CloudX 不会生成这些 ID。请先用 UID2、EUID、LiveRamp ATS、ID5 或 Intent IQ 生成,再把字符串传进来。这与哈希用户 ID 不是同一回事。

CloudXSdk.SetUserKeyValue("uidapi.com", uid2Token);
CloudXSdk.SetUserKeyValue("euid.eu", euidToken);
CloudXSdk.SetUserKeyValue("liveramp.com", liveRampEnvelope);
CloudXSdk.SetUserKeyValue("id5-sync.com", id5Id);
CloudXSdk.SetUserKeyValue("intentiq.com", intentIQId);

收入追踪

所有广告格式都通过 OnAdRevenuePaid 事件提供收入回调。CloudXAd 对象包含收入信息:

CloudXAdsCallbacks.Banner.OnAdRevenuePaid += (ad) =>
{
    Debug.Log($"收入: ${ad.Revenue:F4}");
    Debug.Log($"广告网络: {ad.NetworkName}");
    Debug.Log($"广告单元: {ad.AdUnitId}");
    Debug.Log($"广告格式: {ad.AdFormat}");
    Debug.Log($"广告位: {ad.Placement}");
    Debug.Log($"网络广告位: {ad.NetworkPlacement}");

    // 在您的分析中追踪收入
    TrackRevenue(ad.Revenue, ad.NetworkName);
};

MMP 广告收入连接器

如果您的应用使用 Adjust、AppsFlyer 或 Singular,可以无需编写 Unity C# 胶水代码,直接将 CloudX 竞得展示收入转发给移动归因合作伙伴。请在 Assets/CloudXSdk/Editor/CloudXDependencies.xml 中取消注释连接器依赖,然后运行 EDM4U 依赖解析。

请只选择连接器或您自己的 OnAdRevenuePaid 转发代码其中一种方式,不要同时使用。

发布者上报的收入数据

如果您的应用在 CloudX 广告流程之外接收来自 AdMob、Google Ad Manager、InMobi、TopOn 或其他聚合平台的展示级收入回调或 bid 元数据,请在 CloudX 初始化完成后将这些事件转发给 CloudX:

字段必填描述
PlatformCloudXRevenuePlatform.AdMobCloudXRevenuePlatform.GamCloudXRevenuePlatform.InMobiCloudXRevenuePlatform.TopOnCloudXRevenuePlatform.Custom("MyProvider")
Revenue单次展示收入,使用 CurrencyCode 对应货币;不是 CPM/eCPM
AdFormat广告格式字符串,例如 bannermrecinterstitialrewarded
CurrencyCodeISO 4217 货币代码(如已知)
PrecisionCloudXRevenuePrecision.ExactEstimatedPublisherDefinedUndefined
NetworkName获胜广告网络名称(如已知)
AdUnitId聚合平台广告单元 ID
ThirdPartyAdPlacementId广告网络侧广告单元或 placement ID
CreativeId广告网络返回的创意 ID
NetworkPlacement广告网络 placement 标识
CountryCode用户国家代码(如已知)
UserSegment用户分群(如已知)

当事件被 CloudX 收益链路接受时,CloudXSdk.ReportRevenueData() 返回 true。当 payload 无效、SDK 尚未初始化或收入追踪不可用时,它返回 false

AdMob paid event

使用 Google Mobile Ads Unity 插件时,每个广告对象都会暴露 OnAdPaid。回调中的 AdValue.Value 以微单位上报,因此传给 CloudX 前请先除以 1_000_000.0

private static CloudXRevenuePrecision ToCloudXRevenuePrecision(AdValue.PrecisionType precision) => precision switch
{
    AdValue.PrecisionType.Precise => CloudXRevenuePrecision.Exact,
    AdValue.PrecisionType.Estimated => CloudXRevenuePrecision.Estimated,
    AdValue.PrecisionType.PublisherProvided => CloudXRevenuePrecision.PublisherDefined,
    _ => CloudXRevenuePrecision.Undefined,
};

private static bool ReportAdMobPaidEvent(AdValue adValue, string adFormat, string adUnitId)
{
    return CloudXSdk.ReportRevenueData(new CloudXRevenueData(
        Platform: CloudXRevenuePlatform.AdMob,
        Revenue: adValue.Value / 1_000_000.0,
        AdFormat: adFormat,
        CurrencyCode: adValue.CurrencyCode,
        Precision: ToCloudXRevenuePrecision(adValue.Precision),
        AdUnitId: adUnitId,
    ));
}

bannerView.OnAdPaid += adValue =>
{
    ReportAdMobPaidEvent(adValue, "banner", ADMOB_BANNER_UNIT_ID);
};

Google Ad Manager paid event 的上报方式与 AdMob paid event 相同,但需要使用 CloudXRevenuePlatform.Gam 和 Ad Manager 广告单元 ID。

private static bool ReportGamPaidEvent(AdValue adValue, string adFormat, string adUnitId)
{
    return CloudXSdk.ReportRevenueData(new CloudXRevenueData(
        Platform: CloudXRevenuePlatform.Gam,
        Revenue: adValue.Value / 1_000_000.0,
        AdFormat: adFormat,
        CurrencyCode: adValue.CurrencyCode,
        Precision: ToCloudXRevenuePrecision(adValue.Precision),
        AdUnitId: adUnitId,
    ));
}

adManagerInterstitial.OnAdPaid += adValue =>
{
    ReportGamPaidEvent(adValue, "interstitial", GAM_INTERSTITIAL_UNIT_ID);
};

InMobi impression event

对于 InMobi,请在 OnAdFetchSuccessful 中保存 args.AdMetaInfo。当 OnAdImpression 触发时,把保存的 metaInfo.Bid 上报给 CloudX,然后清空保存的值。

private AdMetaInfo bannerMetaInfo;

private static bool ReportInMobiImpression(AdMetaInfo metaInfo, string adFormat, string placementId)
{
    return CloudXSdk.ReportRevenueData(new CloudXRevenueData(
        Platform: CloudXRevenuePlatform.InMobi,
        Revenue: metaInfo.Bid,
        AdFormat: adFormat,
        Precision: CloudXRevenuePrecision.Estimated,
        ThirdPartyAdPlacementId: placementId,
        CreativeId: metaInfo.CreativeID,
    ));
}

bannerAd.OnAdFetchSuccessful += (_, args) =>
{
    bannerMetaInfo = args.AdMetaInfo;
};

bannerAd.OnAdImpression += (_, _) =>
{
    ReportInMobiImpression(bannerMetaInfo, "banner", INMOBI_BANNER_PLACEMENT_ID);
    bannerMetaInfo = null;
};

TopOn revenue event

使用 TopOn Unity plugin v2.1.8 或更高版本时,请在广告对象上设置 IATAdRevenueListener。使用 adInfo.publisher_revenue 作为单次展示收入;不要使用 adInfo.adsource_price,因为 TopOn 将该值作为 CPM/eCPM 上报。

using AnyThinkAds.Api;
using CloudX;

private static CloudXRevenuePrecision ToTopOnRevenuePrecision(string precision)
{
    return precision switch
    {
        "exact" => CloudXRevenuePrecision.Exact,
        "estimated" => CloudXRevenuePrecision.Estimated,
        "publisher_defined" => CloudXRevenuePrecision.PublisherDefined,
        _ => CloudXRevenuePrecision.Undefined,
    };
}

private sealed class TopOnRevenueListener : IATAdRevenueListener
{
    public void onAdRevenuePaid(string placementId, ATCallbackInfo adInfo)
    {
        ReportTopOnRevenue(adInfo, "banner", placementId);
    }
}

private static bool ReportTopOnRevenue(ATCallbackInfo adInfo, string adFormat, string placementId)
{
    return CloudXSdk.ReportRevenueData(new CloudXRevenueData(
        Platform: CloudXRevenuePlatform.TopOn,
        Revenue: adInfo.publisher_revenue,
        AdFormat: adFormat,
        CurrencyCode: adInfo.currency,
        Precision: ToTopOnRevenuePrecision(adInfo.precision),
        NetworkName: adInfo.network_name,
        AdUnitId: placementId,
        ThirdPartyAdPlacementId: adInfo.network_placement_id,
        NetworkPlacement: adInfo.adsource_id,
        CountryCode: adInfo.country,
    ));
}

ATBannerAd.Instance.setAdRevenueListener(TOPON_BANNER_PLACEMENT_ID, new TopOnRevenueListener());

自定义平台事件

对于没有 CloudX SDK 内置常量的 provider,请使用 Custom;AdMob、Google Ad Manager、InMobi 和 TopOn 已有内置常量。这里的值只表示 provider 名称,例如 CloudXRevenuePlatform.Custom("TradPlus")CloudXRevenuePlatform.Custom("Nimbus")。请保持名称稳定,便于 CloudX 一致归类该 provider 的收益。

不要把金额或币种放进 provider 名称里。如果一条 TradPlus 展示带来 USD 0.01 收入,请传 Platform: CloudXRevenuePlatform.Custom("TradPlus")Revenue: 0.01CurrencyCode: "USD"。如果来源返回 CPM/eCPM,请先除以 1_000.0

private static bool ReportCustomRevenueEvent(
    string providerName,
    double revenue,
    string adFormat,
    string currencyCode,
    string adUnitId,
    string placementId)
{
    return CloudXSdk.ReportRevenueData(new CloudXRevenueData(
        Platform: CloudXRevenuePlatform.Custom(providerName),
        Revenue: revenue,
        AdFormat: adFormat,
        CurrencyCode: currencyCode,
        Precision: CloudXRevenuePrecision.PublisherDefined,
        AdUnitId: adUnitId,
        ThirdPartyAdPlacementId: placementId,
    ));
}