Trusted Arbiter
在 Unity 游戏中比较 CloudX 出价和受支持的第三方出价
Trusted Arbiter 会把已加载的 CloudX 广告与您自行接入的其他平台出价进行比较,并返回应当展示的平台。Unity SDK 4.1.0 开始支持 CloudX、Unity LevelPlay 和 PubMatic 出价。自定义出价需要 4.4.1 或更高版本,AdMob 和 Google Ad Manager 出价需要 4.5.0 或更高版本。
为什么使用 Trusted Arbiter?
当应用从多个平台加载广告时,需要决定展示哪个广告。Trusted Arbiter 为受支持的出价提供统一的比较 API。
减少需要维护的出价比较逻辑
将受支持的出价提交给 Trusted Arbiter,根据返回的平台选择要展示的广告。应用仍需负责合作伙伴 SDK 的集成、广告加载和展示。
使用可用的出价值
在可以获取实际出价值时,Trusted Arbiter 可以使用这些值进行比较。部分广告需求使用估算价格。请参阅下文中各类受支持输入的定价说明。
调用 CloudXSdk.Arbiter 前,CloudXSdk.Initialize 必须已经完成。为每条已加载的广告构建一个出价,然后交给它:
var bids = new List<CloudXArbiterBid>
{
new CloudXArbiterBid.CloudX(cloudXAd),
new CloudXArbiterBid.AdMob(AdUnitId: adMobAdUnitId),
};
CloudXArbiterResult nextWinner = null;
CloudXSdk.Arbiter(bids, result =>
{
/* 此处保存全屏广告结果,稍后在广告位到达时展示。 */
nextWinner = result.Platform == CloudXArbiterPlatform.None
? null
: result;
});对于全屏广告,请保存结果,稍后在广告位回调中展示。横幅广告和 MREC 可以在仲裁回调返回时渲染选中的视图。可用常量为 CloudX、AdMob、Gam、LevelPlay、PubMatic、Custom 和 None。
Trusted Arbiter 需要在 CloudX 控制台中为您的应用启用。在启用之前,每次调用仍由本地兜底应答,而兜底只比较带有本地可比价格的出价。未设置手动价格的 Google 出价没有可在本地比较的价格,因此在与已定价的出价比较时会落败,但当它是唯一有效出价时仍会胜出。通过 ManualRevenuePerImpressionUSD 自行定价的 Google 出价可以正常参与比较。
示例应用
CloudX Unity 示例应用以插屏广告运行本页描述的流程。CloudX 和 AdMob 并行加载,两个填充都作为出价参与,由 CloudXSdk.Arbiter 选出胜出方,并从保存的结果中展示,因此展示路径不会发起网络请求。
ArbiterInterstitialController.cs
完整的加载、仲裁与展示流程,两个 SDK 的调用都集中在一个文件里。激励视频广告可在此文件基础上替换为对应的激励视频调用。
ArbiterScreen.cs
启动两个 SDK,并在 CloudX 应答后创建控制器。它只负责示例布局,请参考其中规则,不要复制该文件。
DemoConfig.cs
分平台的应用密钥和广告单元 ID。自己运行示例时首先要修改这个文件。
示例应用使用 Google 的公开 AdMob 测试广告单元,它们上报的收入为 0.0。CloudX 不会把零价格存入定价历史,因此测试期间 AdMob 出价没有可比的历史价格。改为指向真实投放的广告单元,即可看到价格之间的比较。
何时运行仲裁
请在候选广告加载完成后运行仲裁,绝不要放在展示路径上。在功能已启用且有多个候选时,CloudXSdk.Arbiter 会请求服务端,而到达广告位的用户不应为这次网络请求等待。
并行加载所有候选。当它们全部有结果(加载成功或失败)之后运行仲裁并保存结果。到了广告位处,立即展示已保存的胜出方。广告关闭、展示失败或候选过期之后,再跑一轮。
private CloudXArbiterResult _nextWinner;
private int _arbiterRound;
/*
* 两个候选都有结果后运行,早于广告位。
* 只有真正加载成功的广告才会成为出价。
*/
private void PrepareWinner(
CloudXAd cloudXAd,
string adMobAdUnitId)
{
var bids = new List<CloudXArbiterBid>();
if (cloudXAd != null && CloudXSdk.IsInterstitialReady(cloudXAd.AdUnitId))
{
bids.Add(new CloudXArbiterBid.CloudX(cloudXAd));
}
if (adMobInterstitial?.CanShowAd() == true)
{
bids.Add(new CloudXArbiterBid.AdMob(AdUnitId: adMobAdUnitId));
}
PrepareWinner(bids);
}
private void PrepareWinner(IReadOnlyList<CloudXArbiterBid> bids)
{
/*
* 广告位可能在较早一轮仲裁返回前消费一个候选,因此多轮可能重叠。只有最新
* 一轮可以保存结果,否则旧结果可能指向已经展示过的广告。
*/
var round = ++_arbiterRound;
_nextWinner = null;
if (bids.Count == 0)
{
BeginLoadCycle();
return;
}
CloudXSdk.Arbiter(bids, result =>
{
if (round != _arbiterRound)
{
return;
}
/* None 不是胜出方,不能按胜出方保存。 */
_nextWinner = result.Platform == CloudXArbiterPlatform.None
? null
: result;
});
}
/* 在广告位处运行,这里没有网络请求。 */
private void ShowAd(string cloudXAdUnitId)
{
var winner = _nextWinner;
_nextWinner = null;
/* 让仍然描述本次广告位候选的仲裁轮次失效。 */
_arbiterRound++;
if (winner == null)
{
BeginLoadCycle();
return;
}
switch (winner.Platform)
{
case CloudXArbiterPlatform.CloudX
when CloudXSdk.IsInterstitialReady(cloudXAdUnitId):
CloudXSdk.ShowInterstitial(cloudXAdUnitId);
break;
case CloudXArbiterPlatform.AdMob
when adMobInterstitial?.CanShowAd() == true:
adMobInterstitial.Show();
break;
default:
Debug.Log($"胜出方 {winner.Platform} 不受支持或已不再处于加载状态");
BeginLoadCycle();
break;
}
}BeginLoadCycle 是应用现有的周期入口:它先应用重试退避,再并行加载缺少填充的候选,复用仍持有填充的候选,并在它们全部有结果后调用 PrepareWinner。如果广告位比胜出方来得更早,可以在下一轮运行期间跳过广告,或者展示唯一加载成功的候选,并让其 hidden 回调开始下一轮。这是可接受的降级路径,但不应成为常态。
返回 None 的那一轮会让已加载的候选继续持有各自的填充。仅仅持有填充不会触发任何后续,因此不会有 hidden 回调或展示失败来启动下一轮。上面的空值分支会明确开始下一轮,不再等待不会到来的回调。
CloudXSdk.Arbiter 周围没有需要捕获的异步异常:它只调用一次回调,且回调抛出的异常会由 SDK 捕获并记录。Google Mobile Ads 回调不同。凡是会修改控制器状态、调用 CloudX 或使用 Unity API 的 Google 回调,都要用 MobileAdsEventExecutor.ExecuteInUpdate 包住整个回调体,使其在 Unity 主线程上运行。
支持的广告格式
仲裁与格式无关。它接受任何已加载的 CloudX 广告,各格式的字段映射相同。
- 全屏广告:插屏、激励视频和 App Open。 请按上文在广告位之前准备好胜出方。
- 视图类广告:横幅广告和 MREC。 在仲裁回调返回时渲染胜出方。参见横幅广告和 MREC。
AdMob 与 Google Ad Manager
CloudX 会把已加载的 CloudX 广告与已加载的 AdMob 或 Google Ad Manager 广告进行比较。AdMob 和 Ad Manager 是彼此独立的需求方,可以在同一次仲裁中同时出价。
Google 需求方通常不会在展示之前公开已加载广告的价格。CloudX 会根据您的 Google 历史表现自动为出价定价,因此出价无需提供价格。下面的收入上报会为这份历史提供数据,这也是它为必需项的原因。参见 AdMob/GAM estimated 定价的工作方式。
var adManagerAdUnitId = "/21775744923/example/interstitial";
var adMobNetworkName = "admob";
var bids = new List<CloudXArbiterBid>();
if (cloudXAd != null && CloudXSdk.IsInterstitialReady(cloudXAdUnitId))
{
bids.Add(new CloudXArbiterBid.CloudX(cloudXAd));
}
if (adMobInterstitial?.CanShowAd() == true)
{
var loadedAdapter = adMobInterstitial
.GetResponseInfo()
?.GetLoadedAdapterResponseInfo();
adMobNetworkName = string.IsNullOrWhiteSpace(loadedAdapter?.AdSourceName)
? "admob"
: loadedAdapter.AdSourceName;
bids.Add(new CloudXArbiterBid.AdMob(
AdUnitId: adMobAdUnitId,
NetworkName: adMobNetworkName));
}
if (adManagerInterstitial?.CanShowAd() == true)
{
bids.Add(new CloudXArbiterBid.Gam(
AdUnitId: adManagerAdUnitId));
}
PrepareWinner(bids);
private void ShowGoogleArbitrationWinner(string cloudXAdUnitId)
{
var winner = _nextWinner;
_nextWinner = null;
_arbiterRound++;
switch (winner?.Platform)
{
case CloudXArbiterPlatform.CloudX
when CloudXSdk.IsInterstitialReady(cloudXAdUnitId):
CloudXSdk.ShowInterstitial(cloudXAdUnitId);
break;
case CloudXArbiterPlatform.AdMob
when adMobInterstitial?.CanShowAd() == true:
adMobInterstitial.Show();
break;
case CloudXArbiterPlatform.Gam
when adManagerInterstitial?.CanShowAd() == true:
adManagerInterstitial.Show();
break;
default:
Debug.Log($"胜出方 {winner?.Platform} 不受支持或已不再处于加载状态");
BeginLoadCycle();
break;
}
}胜出的 Google 出价会上报 CloudXArbiterPlatform.AdMob 或 CloudXArbiterPlatform.Gam,因此无需查看 PlatformName 来区分两者。广告单元 ID 为空时仍会构建出价,但该出价没有可用身份,不会被定价,服务端也会拒绝它。
把 Google 付费事件回传给 CloudX
上报 Google 的展示级收入是 AdMob 与 Ad Manager 仲裁的必需环节,而不是可选的数据统计。缺少这些事件,CloudX 就无法得知 Google 需求方的实际价格,后续仲裁中的估价也会变差。
本示例使用 Google Mobile Ads Unity 插件 com.google.ads.mobile。AdValue.Value 在两个平台上均以微单位上报,因此需要除以 1,000,000。该插件还要求在原生层声明 AdMob 应用 ID,请完成 AdMob 应用 ID(必需)中的设置。只接入 Ad Manager 需求的 Android 应用可以改为声明 AD_MANAGER_APP。
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 ReportGooglePaidEvent(
CloudXRevenuePlatform platform,
AdValue adValue,
string adFormat,
string adUnitId,
string networkName) =>
CloudXSdk.ReportRevenueData(new CloudXRevenueData(
Platform: platform,
Revenue: adValue.Value / 1_000_000.0,
AdFormat: adFormat,
CurrencyCode: adValue.CurrencyCode,
Precision: ToCloudXRevenuePrecision(adValue.Precision),
NetworkName: networkName,
AdUnitId: adUnitId));
adMobInterstitial.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() =>
{
var accepted = ReportGooglePaidEvent(
CloudXRevenuePlatform.AdMob,
adValue,
"interstitial",
adMobAdUnitId,
adMobNetworkName);
Debug.Log($"CloudX 是否接受收入:{accepted}");
});
adManagerInterstitial.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() =>
{
var accepted = ReportGooglePaidEvent(
CloudXRevenuePlatform.Gam,
adValue,
"interstitial",
adManagerAdUnitId,
"gam");
Debug.Log($"CloudX 是否接受 GAM 收入:{accepted}");
});请上报您传给出价的同一个广告单元 ID,这样 CloudX 才能正确归属实际价格。数据被丢弃时,ReportRevenueData 返回 false。完整字段说明参见发布者上报的收入数据。
返回 true 并不表示该价格已进入 CloudX 的定价历史。该历史只保留带广告单元 ID 的正数美元金额。收入为零的上报会被接受,但不用于定价,因此 Google 的测试广告单元不会让 AdMob 出价获得可比价格。
使用预出价 ILRD 手动传值
部分 AdMob 账户可以在广告展示之前取得展示级收入。这是按账户开通的历史能力,请与您的 Google 客户团队确认。只有在展示前就已知的确切单次展示价格,才比 CloudX 的估价更合适。
var adMobBid = new CloudXArbiterBid.AdMob(
AdUnitId: adMobAdUnitId,
NetworkName: adMobNetworkName,
ManualRevenuePerImpressionUSD: preBidAdValue.Value / 1_000_000.0);ManualRevenuePerImpressionUSD 是单次展示的美元收入,不是 CPM。AdValue.Value 为 5000 时,单次展示收入为 0.005,请不要再除以 1,000。非美元金额需先换算。
0.0是一个真实价格,表示该出价没有价值。- 负值和非有限值会被视为未提供并记录日志。
- 广告单元 ID 为空时手动价格会被丢弃,因为该出价无法校验。
横幅广告和 MREC
自动刷新与仲裁相冲突,因为它可能在仲裁选出胜出方后替换广告。请在 CloudX 控制台中关闭自动刷新,并在创建视图前调用停止方法。不要调用 StartBannerAutoRefresh。Google Mobile Ads Unity 插件没有刷新 API,因此还要在 AdMob 控制台中把广告单元的 Automatic refresh 设置为 Disabled。
CloudXSdk.StopBannerAutoRefresh(adUnitId);
CloudXSdk.CreateBanner(adUnitId, new CloudXAdViewConfiguration(CloudXAdViewConfiguration.AdViewPosition.BottomCenter));
CloudXSdk.LoadBanner(adUnitId);只用 ShowBanner 展示胜出出价对应的广告,其余广告用 HideBanner 保持隐藏。创建 Google BannerView 后,请在第一次 LoadAd 前调用 Hide(),并且只在 AdMob 胜出后展示。
胜出方产生展示之后,只向该网络请求新的填充。保留仍有填充的落败广告,只请求没有填充的网络,并在所有响应返回后再次运行仲裁。展示中的广告建议每 20 到 30 秒刷新一次,间隔更短会降低 CPM 表现。
MREC 使用相同流程,对应方法为 CreateMrec、LoadMrec、ShowMrec、HideMrec 和 StopMrecAutoRefresh,回调组为 CloudXAdsCallbacks.Mrec。
其他中介平台
CloudXArbiterBid.LevelPlay 接受 Unity LevelPlay 自己上报的值。在 LevelPlay Unity SDK 8.x 中,这些字段使用 camelCase,且 revenue 是可空的 double:
var levelPlayBid = new CloudXArbiterBid.LevelPlay(
NetworkName: levelPlayAdInfo.adNetwork,
Revenue: levelPlayAdInfo.revenue ?? 0,
Precision: levelPlayAdInfo.precision);CloudXArbiterBid.PubMatic 接受来自 OpenWrap 出价对象的价格:
var pubMaticBid = new CloudXArbiterBid.PubMatic(
Price: pubMaticPrice,
PartnerName: pubMaticPartnerName);自定义出价
当某个平台没有专门的出价类型时,请使用 CloudXArbiterBid.Custom。
var customBid = new CloudXArbiterBid.Custom(
PlatformName: "my_mediation_platform",
NetworkName: "winning_demand_source",
RevenuePerImpressionUSD: 0.00125,
Precision: "EXACT",
Extras: new Dictionary<string, string>
{
["ad_unit"] = "third-party-ad-unit-id",
});自定义出价胜出时,result.Platform 为 CloudXArbiterPlatform.Custom,result.PlatformName 为您传入的 PlatformName。RevenuePerImpressionUSD 是单次展示的美元收入,不是 CPM。Precision 是字符串标记,例如 "EXACT"、"ESTIMATED"、"PUBLISHER_DEFINED" 或 "UNDEFINED"。缺少非空平台名称、收入或精度的出价会被丢弃并记录日志。
结果
| 属性 | 类型 | 说明 |
|---|---|---|
Platform | CloudXArbiterPlatform | 胜出的平台,或 None。 |
PlatformName | string | 具体平台名称;自定义出价胜出时为您传入的平台名称。 |
BidId | string? | 选中出价的标识;没有胜出方时为 null。 |
Id | string | 本次仲裁请求的标识。 |
Extras | IReadOnlyDictionary<string, string> | 仲裁为选中出价返回的元数据。 |