Trusted Arbiter

在 Unity 游戏中比较 CloudX 出价与受支持的第三方出价

Trusted Arbiter 会比较已加载的 CloudX 出价与受支持的第三方出价,并返回选中的平台。从 Unity SDK 4.1.0 起可用(底层依赖 Android SDK 4.1.1 和 iOS SDK 3.4.1),支持 CloudX、Unity LevelPlay、PubMatic、AdMob 和 Google Ad Manager 出价输入。发布商传入的自定义出价输入需要 Unity SDK 4.4.1 或更高版本;AdMob 和 Google Ad Manager 出价输入需要 Unity SDK 4.5.0 或更高版本。

支持的广告格式

Trusted Arbiter 与广告格式无关——只要是已加载的 CloudX 广告,无论格式如何都可以参与仲裁。横幅广告、MREC、插屏广告、激励视频广告和 App Open 广告均受支持。

插屏广告、激励视频广告和 App Open 广告属于全屏格式,遵循本页展示的分步流程和控制器模式。横幅广告和 MREC 属于视图格式,需要下方横幅广告与 MREC 仲裁一节中额外的处理方式,因为未获胜的出价对应的视图绝不能被展示。

基础 API

从已加载的广告创建出价候选项,然后传给 CloudXSdk.Arbiter()。下面的示例展示 CloudX、LevelPlay 和 PubMatic;自定义、AdMob 和 Google Ad Manager 出价输入请参见后续专门章节。

using System.Collections.Generic;
using CloudX;
using UnityEngine;

// cloudXAd 是 CloudX OnAdLoadSuccess 回调中的 CloudXAd 对象。
// 它的 AdValues 映射携带了服务器用于校验出价的受信任载荷键。
// levelPlayNetwork / levelPlayRevenue / levelPlayPrecision 来自 Unity LevelPlay 广告信息。
// pubMaticPrice 和 pubMaticPartner 来自 PubMatic/OpenWrap 出价对象。
var bids = new List<CloudXArbiterBid>
{
    new CloudXArbiterBid.CloudX(cloudXAd),
    new CloudXArbiterBid.LevelPlay(
        NetworkName: levelPlayNetwork,
        Revenue: levelPlayRevenue,
        Precision: levelPlayPrecision),
    new CloudXArbiterBid.PubMatic(
        Price: pubMaticPrice,
        PartnerName: pubMaticPartner),
};

CloudXSdk.Arbiter(bids, result =>
{
    Debug.Log($"选中的平台: {result.Platform}");
});

CloudXArbiterBid.CloudX 接收 CloudX 加载回调中的 CloudXAd 对象。CloudXArbiterBid.LevelPlay 接收 Unity LevelPlay 广告信息值。CloudXArbiterBid.PubMatic 接收 PubMatic OpenWrap 出价价格和可选的合作伙伴名称。CloudXArbiterBid.AdMobCloudXArbiterBid.Gam 接收已加载 Google 广告的广告单元 ID,并可根据已上报收益自动定价。完成回调在主线程上执行,因此你可以直接在其中展示广告或更新 UI。

result.Platform 在选中平台时为 CloudXArbiterPlatform.CloudXLevelPlayPubMaticCustomAdMobGam;当无法选出获胜平台时(例如未传入任何出价),则为 CloudXArbiterPlatform.None。结果还提供 result.Id(拍卖标识符)、result.BidId(获胜出价标识符,当平台为 None 时为 null)以及 result.Extras(获胜广告网络返回的附加元数据)。

AdMob 与 Google Ad Manager

CloudX 会将已加载的 CloudX 广告与已加载的 AdMob 或 Google Ad Manager 广告进行比较。AdMob 和 Ad Manager 是两个独立的需求来源,因此二者可以同时参与同一次仲裁。此方式适用于发布商自行管理的聚合配置,包括商业上称为 AdMob Pro 的账户;SDK 不提供单独的 AdMob Pro API。

Google 需求方通常不会在展示前透露已加载广告的价格,因此仲裁器无法获得可与 CloudX 出价比较的 pre-bid 价格。CloudX 会根据同类广告单元过往的表现估算出价,因此你无需自行提供价格。不需要任何 pre-bid 定价 API。

如果你的 AdMob 账户能够在 pre-bid 阶段提供展示级收益数据,也可以自行提供该精确价格来代替使用估算值——参见下方使用 pre-bid ILRD 手动输入价格

将 Google 付费事件回传给 CloudX(必需)

回传 Google 的展示级收入数据是 Trusted Arbiter 的 AdMob 与 Ad Manager 集成中必需的一环。当仲裁中胜出的 AdMob 或 Ad Manager 广告展示之后,你必须把 Google Mobile Ads Unity 插件的 OnAdPaid 事件转发给 CloudX SDK。如果不转发,CloudX 就无法获知 Google 需求的真实成交价格,仲裁器在后续轮次中的估算精度也会随之下降。

每个广告对象都会通过 OnAdPaid 回调提供一个 AdValueAdValue.Value 以微单位(micro-units)上报,因此需要先除以 1_000_000.0,再映射精度,然后传给 CloudXSdk.ReportRevenueData()。AdMob 广告请使用 CloudXRevenuePlatform.AdMob,Ad Manager 广告请使用 CloudXRevenuePlatform.Gam

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)
{
    // adValue.Value is in micro-units of adValue.CurrencyCode.
    return CloudXSdk.ReportRevenueData(new CloudXRevenueData(
        Platform: platform,
        Revenue: adValue.Value / 1_000_000.0,
        AdFormat: adFormat,
        CurrencyCode: adValue.CurrencyCode,
        Precision: ToCloudXRevenuePrecision(adValue.Precision),
        AdUnitId: adUnitId));
}

// AdMob ad that competed in the arbitration.
adMobInterstitial.OnAdPaid += adValue =>
{
    ReportGooglePaidEvent(CloudXRevenuePlatform.AdMob, adValue, "interstitial", adMobAdUnitId);
};

// Ad Manager ad that competed in the arbitration.
adManagerInterstitial.OnAdPaid += adValue =>
{
    ReportGooglePaidEvent(CloudXRevenuePlatform.Gam, adValue, "interstitial", adManagerAdUnitId);
};

完整的上报设置(包括其他受支持的平台以及全部可选字段)参见发布者上报的收入数据

用你已加载广告的广告单元 ID 创建出价:

// NetworkName 为可选参数;如果你知道获胜的广告来源,请传入它。
var adMobBid = new CloudXArbiterBid.AdMob(
    AdUnitId: adMobAdUnitId,
    NetworkName: adMobNetworkName ?? "admob");

// Ad Manager 广告单元 ID 的格式为 /NNNNNNN/placement/name。
var adManagerBid = new CloudXArbiterBid.Gam(AdUnitId: "/21775744923/example/interstitial");

var bids = new List<CloudXArbiterBid>
{
    new CloudXArbiterBid.CloudX(cloudXAd),
    adMobBid,
    adManagerBid,
};

CloudXSdk.Arbiter(bids, result =>
{
    switch (result.Platform)
    {
        case CloudXArbiterPlatform.CloudX:
            CloudXSdk.ShowInterstitial("YOUR_CLOUDX_AD_UNIT_ID", "level_complete");
            break;
        case CloudXArbiterPlatform.AdMob:
            adMobInterstitial.Show();
            break;
        case CloudXArbiterPlatform.Gam:
            adManagerInterstitial.Show();
            break;
    }
});

获胜的 Google 出价会报告自己的平台——CloudXArbiterPlatform.AdMobCloudXArbiterPlatform.Gam。你不再需要像处理自定义出价那样,通过 result.PlatformName 来区分这两个来源。

传入空白的广告单元 ID,出价对象仍会正常构建,不会导致应用崩溃,但它不具备可用的身份信息:既不会被定价,也会被服务器拒绝。

使用 pre-bid ILRD 手动输入价格

部分 AdMob 账户可以在 pre-bid 阶段提供展示级收益数据(ILRD):已加载广告的 AdValue 在加载时即可获取,早于广告展示。这是一项受账户控制的历史能力,请与你的 Google 客户团队确认你的账户是否已启用。在展示之前就已知的精确单次展示价格,是自行提供价格优于 CloudX 估算值的唯一场景。

将 pre-bid 广告价值作为 ManualRevenuePerImpressionUSD 传入,它会覆盖估算值:

var adMobBid = new CloudXArbiterBid.AdMob(
    AdUnitId: adMobAdUnitId,
    NetworkName: adMobNetworkName ?? "admob",
    // AdValue.Value 以微单位(micros)表示:1,000,000 微单位等于 1 个货币单位。
    // 请勿再额外除以 1,000——该数值本身已是单次展示的收益,而不是 CPM。
    ManualRevenuePerImpressionUSD: preBidAdValue.Value / 1_000_000.0);

该数值的处理方式:

  • 0.0 是一个真实的价格。它表示该出价价值为零——而不是价格缺失。
  • 负值和非有限值不是有效价格,因此会被视为缺失并记录日志。
  • 空白的广告单元 ID 会导致手动价格被完全丢弃,因为没有身份信息的出价无法通过校验。

ManualRevenuePerImpressionUSD 是单次展示的美元收益,而不是 CPM。请换算你的价格来源报告的数值:

  • 以微单位(micros)表示的数值 必须除以 1,000,000。Unity 版 Google Mobile Ads SDK 以微单位报告广告价值,因此 AdValue.Value5000 时对应单次展示 0.005。请勿把微单位数值误当作 eCPM 而再额外除以 1,000。
  • 非美元金额 必须先换算为美元。

分步示例:在 CloudX 与 LevelPlay 之间仲裁

本演练展示了应读取哪个 Unity LevelPlay 回调,以及应将哪些值传入 CloudXSdk.Arbiter()。示例使用插屏广告,但相同的字段映射适用于任何广告格式——各格式所需的具体处理方式请参见支持的广告格式

加载两个候选项

创建 LevelPlay 插屏广告,并为每个平台启动加载。请在加载之前订阅加载回调。

var levelPlayAd = new LevelPlayInterstitialAd("YOUR_LEVELPLAY_AD_UNIT_ID");
levelPlayAd.OnAdLoaded += OnLevelPlayLoaded;
levelPlayAd.OnAdLoadFailed += OnLevelPlayLoadFailed;
levelPlayAd.LoadAd();

CloudXAdsCallbacks.Interstitial.OnAdLoadSuccess += OnCloudXLoaded;
CloudXSdk.LoadInterstitial("YOUR_CLOUDX_AD_UNIT_ID");

保存每个平台已加载的广告

LevelPlay 在其 OnAdLoaded 回调中提供 LevelPlayAdInfo;CloudX 在 OnAdLoadSuccess 中提供 CloudXAd。请保存两者——下一步会从中读取仲裁输入。

private CloudXAd _cloudXAd;
private LevelPlayAdInfo _levelPlayInfo;

private void OnCloudXLoaded(CloudXAd ad) => _cloudXAd = ad;
private void OnLevelPlayLoaded(LevelPlayAdInfo info) => _levelPlayInfo = info;

将值映射为出价

LevelPlayAdInfo 读取 LevelPlay 字段,并传给 CloudXArbiterBid.LevelPlay。CloudX 出价直接接收 CloudXAd

LevelPlayAdInfo 字段类型CloudXArbiterBid.LevelPlay 参数
adNetworkstringNetworkName
revenuedouble?Revenue(用 ?? 0 合并空值)
precisionstringPrecision
var bids = new List<CloudXArbiterBid>
{
    new CloudXArbiterBid.CloudX(_cloudXAd),
    new CloudXArbiterBid.LevelPlay(
        NetworkName: _levelPlayInfo.adNetwork,   // LevelPlayAdInfo.adNetwork
        Revenue: _levelPlayInfo.revenue ?? 0,    // LevelPlayAdInfo.revenue 为 double?
        Precision: _levelPlayInfo.precision),    // LevelPlayAdInfo.precision
};

运行仲裁器

将出价连同完成回调传给 CloudXSdk.Arbiter()。回调在 Unity 主线程上执行。

CloudXSdk.Arbiter(bids, OnArbiterCompleted);

展示获胜平台

根据 result.Platform 进行分支,展示获胜平台的广告。CloudXArbiterPlatform.None 表示未选出获胜平台——此时不展示广告,继续应用流程。

private void OnArbiterCompleted(CloudXArbiterResult result)
{
    switch (result.Platform)
    {
        case CloudXArbiterPlatform.CloudX:
            CloudXSdk.ShowInterstitial("YOUR_CLOUDX_AD_UNIT_ID", "level_complete");
            break;
        case CloudXArbiterPlatform.LevelPlay:
            levelPlayAd.ShowAd("level_complete");
            break;
        default:
            break; // CloudXArbiterPlatform.None——无获胜平台;不展示广告
    }
}

下方的 ArbiterInterstitialController 将上述步骤封装为一个可复用的组件,会在到达广告位之前提前准备好获胜平台。

插屏广告示例

这个插屏广告示例会在两个平台之间仲裁:CloudX 和 Unity LevelPlay。在到达广告位之前先准备好获胜平台:

  1. 并行加载 CloudX 和 LevelPlay。
  2. 等待两个平台都加载完成或加载失败。
  3. 只将已加载的候选项提交给 Trusted Arbiter。
  4. 缓存选中的平台。
  5. 到达广告位时,立即展示缓存的获胜广告。

如果两个平台都加载失败,开始新的加载周期。如果到达广告位时还没有准备好获胜平台,则继续应用流程,不展示广告。

ArbiterInterstitialController.cs
using System.Collections.Generic;
using CloudX;
using UnityEngine;

/// <summary>
/// 提前准备好 Trusted Arbiter 的获胜平台,以便在到达广告位时立即展示插屏广告。
///
/// 并行加载 CloudX 和 LevelPlay 插屏广告,等待两者都加载完成或加载失败,
/// 将已加载的候选项提交给 CloudXSdk.Arbiter,并将选中的 CloudXArbiterPlatform
/// 缓存到 _nextWinner 中。
/// </summary>
public class ArbiterInterstitialController : MonoBehaviour
{
    private const string CloudXAdUnitId = "interstitial_main";

    // 当 LevelPlay Unity SDK 报告插屏广告已加载时,由宿主设置。
    public string LevelPlayNetwork;
    public double LevelPlayRevenue;
    public string LevelPlayPrecision;

    // 当仲裁器为下一次展示选出平台后调用。
    public System.Action<CloudXArbiterPlatform> OnWinnerPrepared;

    private CloudXAd _cloudXAd;
    private bool _cloudXLoadDone;
    private bool _levelPlayLoaded;
    private bool _levelPlayLoadDone;
    private CloudXArbiterPlatform? _nextWinner;

    private void OnEnable()
    {
        CloudXAdsCallbacks.Interstitial.OnAdLoadSuccess += OnCloudXLoaded;
        CloudXAdsCallbacks.Interstitial.OnAdLoadFailed += OnCloudXLoadFailed;
        CloudXAdsCallbacks.Interstitial.OnAdHidden += OnCloudXHidden;
        CloudXAdsCallbacks.Interstitial.OnAdShowFailed += OnCloudXShowFailed;
    }

    private void OnDisable()
    {
        CloudXAdsCallbacks.Interstitial.OnAdLoadSuccess -= OnCloudXLoaded;
        CloudXAdsCallbacks.Interstitial.OnAdLoadFailed -= OnCloudXLoadFailed;
        CloudXAdsCallbacks.Interstitial.OnAdHidden -= OnCloudXHidden;
        CloudXAdsCallbacks.Interstitial.OnAdShowFailed -= OnCloudXShowFailed;
    }

    /// <summary>为每个当前没有缓存广告的平台启动加载。</summary>
    public void LoadMissingAds()
    {
        if (_cloudXAd == null) CloudXSdk.LoadInterstitial(CloudXAdUnitId);
        if (!_levelPlayLoaded) LoadLevelPlayInterstitial();
    }

    /// <summary>
    /// 展示已准备好的获胜广告,仅当确实发起了展示调用时返回 true。
    ///
    /// 当没有准备好获胜平台或缓存的广告已不可用时返回 false,
    /// 此时会启动一次新的加载周期。
    /// </summary>
    public bool ShowAtPlacement(string placement)
    {
        switch (_nextWinner)
        {
            case CloudXArbiterPlatform.CloudX:
                return ShowCloudX(placement);
            case CloudXArbiterPlatform.LevelPlay:
                return ShowLevelPlay(placement);
            default:
                return false;
        }
    }

    /// <summary>
    /// 在两个平台都完成后运行仲裁器,然后缓存获胜平台。
    ///
    /// 在两个加载都完成之前提前返回。如果两个平台都没有加载成功,则重启加载周期;
    /// 否则将已加载的候选项提交给 CloudXSdk.Arbiter。
    /// </summary>
    private void MaybePrepareWinner()
    {
        if (!_cloudXLoadDone || !_levelPlayLoadDone) return;

        if (_cloudXAd == null && !_levelPlayLoaded)
        {
            _cloudXLoadDone = false;
            _levelPlayLoadDone = false;
            LoadMissingAds();
            return;
        }

        var bids = new List<CloudXArbiterBid>();
        if (_cloudXAd != null)
        {
            bids.Add(new CloudXArbiterBid.CloudX(_cloudXAd));
        }

        if (_levelPlayLoaded)
        {
            bids.Add(new CloudXArbiterBid.LevelPlay(
                NetworkName: LevelPlayNetwork,
                Revenue: LevelPlayRevenue,
                Precision: LevelPlayPrecision));
        }

        CloudXSdk.Arbiter(bids, result =>
        {
            _nextWinner = result.Platform;
            OnWinnerPrepared?.Invoke(result.Platform);
        });
    }

    private bool ShowCloudX(string placement)
    {
        if (CloudXSdk.IsInterstitialReady(CloudXAdUnitId))
        {
            CloudXSdk.ShowInterstitial(CloudXAdUnitId, placement);
            return true;
        }

        ClearCloudXAndLoadMissingAds();
        return false;
    }

    private bool ShowLevelPlay(string placement)
    {
        if (IsLevelPlayInterstitialReady())
        {
            ShowLevelPlayInterstitial(placement);
            return true;
        }

        ClearLevelPlayAndLoadMissingAds();
        return false;
    }

    private void OnCloudXLoaded(CloudXAd ad)
    {
        _cloudXAd = ad;
        _cloudXLoadDone = true;
        MaybePrepareWinner();
    }

    private void OnCloudXLoadFailed(string adUnitId, CloudXError error)
    {
        _cloudXAd = null;
        _cloudXLoadDone = true;
        MaybePrepareWinner();
    }

    private void OnCloudXHidden(CloudXAd ad) => ClearCloudXAndLoadMissingAds();

    private void OnCloudXShowFailed(CloudXAd ad, CloudXError error) => ClearCloudXAndLoadMissingAds();

    private void ClearCloudXAndLoadMissingAds()
    {
        _cloudXAd = null;
        _cloudXLoadDone = false;
        _nextWinner = null;
        LoadMissingAds();
    }

    private void ClearLevelPlayAndLoadMissingAds()
    {
        _levelPlayLoaded = false;
        _levelPlayLoadDone = false;
        _nextWinner = null;
        LoadMissingAds();
    }

    /*
     * 下面的成员封装了 LevelPlay Unity SDK。请用你集成的 LevelPlay SDK 调用替换其方法体,
     * 并将其加载回调接入上面的字段:加载成功时,设置 _levelPlayLoaded = true 及各个
     * LevelPlay* 值,然后设置 _levelPlayLoadDone = true 并调用 MaybePrepareWinner();
     * 加载失败时,设置 _levelPlayLoaded = false、_levelPlayLoadDone = true,
     * 并调用 MaybePrepareWinner()。广告关闭或展示失败时,调用
     * ClearLevelPlayAndLoadMissingAds() 以丢弃已消耗的广告并开始新的加载周期。
     */
    private void LoadLevelPlayInterstitial() { /* LevelPlay.LoadInterstitial(...) */ }
    private bool IsLevelPlayInterstitialReady() => _levelPlayLoaded;
    private void ShowLevelPlayInterstitial(string placement) { /* LevelPlay.ShowInterstitial(...) */ }
}

ShowAtPlacement() 仅当确实发起了展示调用时返回 true。在 LevelPlay 广告保持加载状态期间,请持续更新缓存的 LevelPlay 候选项值。

对于 PubMatic OpenWrap,使用 new CloudXArbiterBid.PubMatic(price, partnerName) 创建第三方出价。如果仲裁服务不可用,SDK 会在传入的受支持出价输入中回退选择可比较美元出价最高的平台。

横幅广告与 MREC 仲裁

横幅广告和 MREC 属于视图格式:与插屏广告和激励视频广告不同,可以同时加载多个候选项并将其附加到视图上,因此除了上面的基础 API 之外,仲裁还需要额外的处理。MREC 与横幅广告使用相同的流程——本节将两者放在一起介绍。

关闭自动刷新

两个平台默认都会各自独立进行自动刷新,这会与仲裁过程产生竞争,并在仲裁器不知情的情况下重新加载广告。在仲裁之前,请关闭每个参与仲裁的广告网络的自动刷新:

  • 在 CloudX 控制台中关闭该广告单元的自动刷新,并在创建广告后调用 CloudXSdk.StopBannerAutoRefresh(adUnitId)(MREC 使用 CloudXSdk.StopMrecAutoRefresh(adUnitId))。
  • 同时关闭其他参与仲裁的广告网络的横幅广告自动刷新。

视图附加

只有获胜出价对应的横幅广告可以被展示。展示未获胜的广告会使其渲染并触发一次未被仲裁器选中的出价的展示事件,因此未获胜的候选项必须保持已加载但隐藏的状态。

刷新周期

由于横幅广告和 MREC 是持续展示的,而不是在单一广告位上展示一次,因此仲裁需要按周期重复运行,而不是只运行一次。推荐的流程如下:

  1. 并行加载 → 运行仲裁器 → 展示获胜广告。
  2. 获胜广告的展示事件触发后,立即从获胜的广告网络开始加载新的填充广告。
  3. 保留未获胜的广告网络已填充的广告,用于下一轮仲裁;仅对上一轮未填充的广告网络重新发起加载请求。
  4. 等待未完成的加载响应全部返回后,再次运行仲裁器。
  5. 按 20–30 秒的间隔刷新已展示的广告,替换为新的获胜广告。间隔短于 20 秒会降低 CPM 表现。

横幅广告示例

这个横幅广告示例会在 CloudX 和 Unity LevelPlay 的横幅广告之间按周期性刷新循环进行仲裁:

  1. 关闭两个平台的自动刷新,并并行加载两个横幅广告。
  2. 等两者都加载完成或加载失败后运行仲裁器,并展示获胜广告的视图。
  3. 获胜广告展示事件触发后,从获胜的广告网络加载新的填充广告;未获胜广告网络当前的填充广告保留至下一轮。
  4. 仅对未填充的广告网络重新发起加载请求。
  5. 等待未完成的加载全部返回后,再次运行仲裁器,并将展示的视图切换为新的获胜广告。
  6. 按 20–30 秒的定时器重复上述流程。
ArbiterBannerController.cs
using System.Collections.Generic;
using CloudX;
using UnityEngine;

/// <summary>
/// 按周期性间隔运行 Trusted Arbiter,决定 "banner_main" 应展示 CloudX 还是
/// LevelPlay 的横幅广告。
///
/// 两个广告网络的自动刷新均已关闭,因此加载和展示完全由仲裁结果驱动:
/// 只展示仲裁器选出的获胜广告,未获胜的广告保持已加载但隐藏状态,
/// 获胜广告自身的展示事件会触发它的下一次填充加载。
/// </summary>
public class ArbiterBannerController : MonoBehaviour
{
    private const string CloudXAdUnitId = "banner_main";
    private const float RefreshIntervalSeconds = 25f;

    // 当 LevelPlay Unity SDK 报告横幅广告已加载时,由宿主设置。
    public string LevelPlayNetwork;
    public double LevelPlayRevenue;
    public string LevelPlayPrecision;

    private CloudXAd _cloudXAd;
    private bool _cloudXLoaded;
    private bool _cloudXLoadDone;
    private bool _levelPlayLoaded;
    private bool _levelPlayLoadDone;
    private CloudXArbiterPlatform? _shownPlatform;
    private float _refreshTimer;

    private void OnEnable()
    {
        var config = new CloudXAdViewConfiguration(CloudXAdViewConfiguration.AdViewPosition.BottomCenter);
        CloudXSdk.CreateBanner(CloudXAdUnitId, config);
        CloudXSdk.StopBannerAutoRefresh(CloudXAdUnitId);

        CloudXAdsCallbacks.Banner.OnAdLoadSuccess += OnCloudXLoaded;
        CloudXAdsCallbacks.Banner.OnAdLoadFailed += OnCloudXLoadFailed;
        CloudXAdsCallbacks.Banner.OnAdRevenuePaid += OnCloudXImpression;

        LoadMissingAds();
    }

    private void OnDisable()
    {
        CloudXAdsCallbacks.Banner.OnAdLoadSuccess -= OnCloudXLoaded;
        CloudXAdsCallbacks.Banner.OnAdLoadFailed -= OnCloudXLoadFailed;
        CloudXAdsCallbacks.Banner.OnAdRevenuePaid -= OnCloudXImpression;
    }

    private void Update()
    {
        if (_shownPlatform == null) return;

        _refreshTimer += Time.deltaTime;
        if (_refreshTimer >= RefreshIntervalSeconds)
        {
            _refreshTimer = 0f;
            RunArbiter();
        }
    }

    /// <summary>为每个当前没有已填充广告的网络启动加载。</summary>
    private void LoadMissingAds()
    {
        if (!_cloudXLoaded) CloudXSdk.LoadBanner(CloudXAdUnitId);
        if (!_levelPlayLoaded) LoadLevelPlayBanner();
    }

    /// <summary>
    /// 将当前已填充的候选项提交给 CloudXSdk.Arbiter,只展示获胜广告的视图,
    /// 并隐藏未获胜的广告,使其永远不会渲染或触发展示事件。
    /// </summary>
    private void RunArbiter()
    {
        if (!_cloudXLoadDone || !_levelPlayLoadDone) return;

        var bids = new List<CloudXArbiterBid>();
        if (_cloudXLoaded) bids.Add(new CloudXArbiterBid.CloudX(_cloudXAd));
        if (_levelPlayLoaded)
        {
            bids.Add(new CloudXArbiterBid.LevelPlay(
                NetworkName: LevelPlayNetwork,
                Revenue: LevelPlayRevenue,
                Precision: LevelPlayPrecision));
        }

        if (bids.Count == 0) return;

        CloudXSdk.Arbiter(bids, result =>
        {
            _shownPlatform = result.Platform;
            ShowWinnerHideLoser(result.Platform);
        });
    }

    private void ShowWinnerHideLoser(CloudXArbiterPlatform winner)
    {
        if (winner == CloudXArbiterPlatform.CloudX)
        {
            CloudXSdk.ShowBanner(CloudXAdUnitId);
            HideLevelPlayBanner();
        }
        else if (winner == CloudXArbiterPlatform.LevelPlay)
        {
            CloudXSdk.HideBanner(CloudXAdUnitId);
            ShowLevelPlayBanner();
        }
    }

    private void OnCloudXLoaded(CloudXAd ad)
    {
        _cloudXAd = ad;
        _cloudXLoaded = true;
        _cloudXLoadDone = true;
        RunArbiter();
    }

    private void OnCloudXLoadFailed(string adUnitId, CloudXError error)
    {
        _cloudXLoaded = false;
        _cloudXLoadDone = true;
        RunArbiter();
    }

    /// <summary>
    /// 在展示中的 CloudX 横幅广告展示事件被确认后触发。立即开始为获胜的广告网络
    /// 加载下一次填充;未获胜广告网络当前的填充广告会保留至下一轮仲裁。
    /// </summary>
    private void OnCloudXImpression(CloudXAd ad)
    {
        if (_shownPlatform != CloudXArbiterPlatform.CloudX) return;

        _cloudXLoaded = false;
        _cloudXLoadDone = false;
        CloudXSdk.LoadBanner(CloudXAdUnitId);
    }

    /*
     * 下面的成员封装了 LevelPlay Unity SDK。请用你集成的 LevelPlay SDK 调用替换其方法体,
     * 并将其回调接入上面的字段:加载成功时,设置 _levelPlayLoaded = true 及各个
     * LevelPlay* 值,然后设置 _levelPlayLoadDone = true 并调用 RunArbiter();
     * 加载失败时,设置 _levelPlayLoaded = false、_levelPlayLoadDone = true,
     * 并调用 RunArbiter()。在 LevelPlay 横幅广告的展示事件回调中,当它是当前展示的
     * 平台时,设置 _levelPlayLoaded = false、_levelPlayLoadDone = false,并开始其
     * 下一次加载——与上面的 OnCloudXImpression 保持一致。请在 OnEnable 中同时关闭
     * LevelPlay 横幅广告的自动刷新。
     */
    private void LoadLevelPlayBanner() { /* LevelPlay.LoadBanner(...); 关闭其自动刷新 */ }
    private void ShowLevelPlayBanner() { /* LevelPlay 横幅视图.Show() */ }
    private void HideLevelPlayBanner() { /* LevelPlay 横幅视图.Hide() */ }
}

RunArbiter() 只会展示获胜广告的视图——未获胜广告网络的广告会保持已加载但隐藏状态,直到它在之后的某一轮中获胜。对于 MREC,请使用 CloudXSdk.CreateMrecCloudXSdk.LoadMrecCloudXSdk.ShowMrecCloudXSdk.HideMrecCloudXSdk.StopMrecAutoRefresh 替代对应的横幅广告方法——仲裁流程本身完全相同。

自定义出价输入

当需要让 Trusted Arbiter 比较 CloudX 与没有专用出价类型的第三方平台时,可以使用 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" });

var bids = new List<CloudXArbiterBid>
{
    new CloudXArbiterBid.CloudX(cloudXAd),
    customBid,
};

CloudXSdk.Arbiter(bids, result =>
{
    Debug.Log($"选中的平台: {result.Platform}");
});

当自定义出价胜出时,result.PlatformCloudXArbiterPlatform.Customresult.PlatformName 为出价中传入的 PlatformNameRevenuePerImpressionUSD 应传入单次展示的美元收益,而不是 CPM。请使用 "EXACT""ESTIMATED""PUBLISHER_DEFINED""UNDEFINED" 作为 Precision 描述该收益值的精度。如果自定义出价缺少非空的 PlatformName、收益或精度,该出价会被丢弃并记录警告,不会参与竞争。