概览
CloudX Unity SDK 设置和核心功能概览
CloudX Unity SDK 使您能够在 iOS 和 Android 上通过横幅、MREC、插屏、激励和 App Open 广告实现 Unity 游戏变现。
安装
CloudX Unity SDK 以 .unitypackage 文件形式分发。
- 从 CloudX Unity SDK 4.7.0 发布页 下载
CloudXSdk-4.7.0.unitypackage - 在 Unity 中,转到 Assets > Import Package > Custom Package
- 选择下载的
.unitypackage文件 - 提示时导入所有资源
示例应用
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 广告集成。请查阅各格式专属指南了解实现详情:
横幅广告和 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();
}
}
}排查
| 现象 | 原因和处理 |
|---|---|
全屏回调(OnAdShowSuccess、OnAdRevenuePaid)等广告关闭之后才到 | 预期行为:Unity 播放器可能在广告后面被暂停,主线程送达因此要等待。如果需要更早收到,设置 false 并自行调度。 |
日志里出现 Caught exception in publisher event: <event> | 您的回调抛了异常。这行日志会给出事件名、订阅的方法,以及它是不是在 Unity 主线程上送达的。回调在非主线程上调用 Unity API 就会抛在这里 — 设置 true,或者把这部分工作挪到 Update() 队列里。 |
日志里出现 UnityMainThreadDispatcher does not exist | CloudX 的派发 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.com | UID2 广告 token,不要传 refresh token。不要解密。UID2 文档 |
euid.eu | EUID 广告 token,不要传 refresh token。不要解密。EUID 文档 |
liveramp.com | LiveRamp ATS 信封,不要传 RampID。LiveRamp 文档 |
id5-sync.com | ID5 通用 UID。不要传 0。ID5 文档 |
intentiq.com | Intent 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:
| 字段 | 必填 | 描述 |
|---|---|---|
Platform | 是 | CloudXRevenuePlatform.AdMob、CloudXRevenuePlatform.Gam、CloudXRevenuePlatform.InMobi、CloudXRevenuePlatform.TopOn 或 CloudXRevenuePlatform.Custom("MyProvider") |
Revenue | 是 | 单次展示收入,使用 CurrencyCode 对应货币;不是 CPM/eCPM |
AdFormat | 是 | 广告格式字符串,例如 banner、mrec、interstitial 或 rewarded |
CurrencyCode | 否 | ISO 4217 货币代码(如已知) |
Precision | 否 | CloudXRevenuePrecision.Exact、Estimated、PublisherDefined 或 Undefined |
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
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.01 和 CurrencyCode: "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,
));
}