From 8f4fa817a447042d067229143c84e905a6a31559 Mon Sep 17 00:00:00 2001 From: Anton Urankar <124867229+antonurankar-moloco@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:12:46 +0200 Subject: [PATCH 1/3] Add an Arbiter/TPA demo: CloudX vs AdMob through Trusted Arbiter CloudX and AdMob load in parallel for all four formats and CloudXSdk.Arbiter picks the platform to show. Interstitial and rewarded prepare the winner ahead of the placement; banner and MREC arbitrate and then render on a 25 s cycle with auto-refresh off on both sides and only the winner's view ever shown. AdMob paid events are forwarded through ReportRevenueData, as the docs require. The Options button routes to the new ArbiterScene. Google's AdMob test ad unit ids move from FirstLookConfig to DemoConfig so both demo folders share them. --- Assets/Scenes/ArbiterScene.unity | 1547 +++++++++++++++++ Assets/Scenes/ArbiterScene.unity.meta | 7 + Assets/Scripts/Arbiter.meta | 8 + Assets/Scripts/Arbiter/ArbiterAdController.cs | 204 +++ .../Arbiter/ArbiterAdController.cs.meta | 2 + .../Arbiter/ArbiterBannerController.cs | 113 ++ .../Arbiter/ArbiterBannerController.cs.meta | 2 + Assets/Scripts/Arbiter/ArbiterConfig.cs | 29 + Assets/Scripts/Arbiter/ArbiterConfig.cs.meta | 2 + .../Arbiter/ArbiterFullscreenController.cs | 262 +++ .../ArbiterFullscreenController.cs.meta | 2 + .../Arbiter/ArbiterInlineController.cs | 470 +++++ .../Arbiter/ArbiterInlineController.cs.meta | 2 + .../Arbiter/ArbiterInterstitialController.cs | 115 ++ .../ArbiterInterstitialController.cs.meta | 2 + .../Scripts/Arbiter/ArbiterMrecController.cs | 115 ++ .../Arbiter/ArbiterMrecController.cs.meta | 2 + .../Arbiter/ArbiterRewardedController.cs | 132 ++ .../Arbiter/ArbiterRewardedController.cs.meta | 2 + Assets/Scripts/Arbiter/ArbiterScreen.cs | 487 ++++++ Assets/Scripts/Arbiter/ArbiterScreen.cs.meta | 2 + Assets/Scripts/DemoConfig.cs | 20 + Assets/Scripts/FirstLook/FirstLookConfig.cs | 25 +- Assets/Scripts/FirstLook/FirstLookScreen.cs | 8 +- Assets/Scripts/OptionsScreen.cs | 16 +- ProjectSettings/EditorBuildSettings.asset | 3 + README.md | 107 +- docs/images/arbiter-inline.png | Bin 0 -> 63208 bytes docs/images/arbiter-screen.png | Bin 0 -> 31157 bytes docs/images/options-screen.png | Bin 10884 -> 12873 bytes 30 files changed, 3639 insertions(+), 47 deletions(-) create mode 100644 Assets/Scenes/ArbiterScene.unity create mode 100644 Assets/Scenes/ArbiterScene.unity.meta create mode 100644 Assets/Scripts/Arbiter.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterAdController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterAdController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterBannerController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterBannerController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterConfig.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterConfig.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterFullscreenController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterFullscreenController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterInlineController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterInlineController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterInterstitialController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterInterstitialController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterMrecController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterMrecController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterRewardedController.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterRewardedController.cs.meta create mode 100644 Assets/Scripts/Arbiter/ArbiterScreen.cs create mode 100644 Assets/Scripts/Arbiter/ArbiterScreen.cs.meta create mode 100644 docs/images/arbiter-inline.png create mode 100644 docs/images/arbiter-screen.png diff --git a/Assets/Scenes/ArbiterScene.unity b/Assets/Scenes/ArbiterScene.unity new file mode 100644 index 0000000..c3153d5 --- /dev/null +++ b/Assets/Scenes/ArbiterScene.unity @@ -0,0 +1,1547 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 0 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 4890085278179872738, guid: b9ae659ad95074ed28609bd7545ffb90, + type: 2} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &277484667 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 277484670} + - component: {fileID: 277484669} + - component: {fileID: 277484668} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &277484668 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 277484667} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &277484669 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 277484667} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &277484670 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 277484667} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &519420028 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 519420032} + - component: {fileID: 519420031} + - component: {fileID: 519420029} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &519420029 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 +--- !u!20 &519420031 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.15686275, g: 0.26666668, b: 0.6509804, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 0 + m_HDR: 1 + m_AllowMSAA: 0 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 0 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &519420032 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &524696371 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 524696372} + - component: {fileID: 524696374} + - component: {fileID: 524696373} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &524696372 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 524696371} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 846627341} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 2.5} + m_SizeDelta: {x: -40, y: -15} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &524696373 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 524696371} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 40 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Show Banner +--- !u!222 &524696374 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 524696371} + m_CullTransparentMesh: 0 +--- !u!1 &636086475 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 636086478} + - component: {fileID: 636086477} + - component: {fileID: 636086476} + m_Layer: 5 + m_Name: InterstitialStatusText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &636086476 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 636086475} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Loading... +--- !u!222 &636086477 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 636086475} + m_CullTransparentMesh: 0 +--- !u!224 &636086478 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 636086475} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.71} + m_AnchorMax: {x: 1, y: 0.77} + m_AnchoredPosition: {x: 105, y: 0} + m_SizeDelta: {x: -230, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &694260087 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 694260088} + - component: {fileID: 694260090} + - component: {fileID: 694260089} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &694260088 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 694260087} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1665391391} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 2.5} + m_SizeDelta: {x: -40, y: -15} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &694260089 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 694260087} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 40 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Show Rewarded +--- !u!222 &694260090 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 694260087} + m_CullTransparentMesh: 0 +--- !u!1 &846627337 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 846627341} + - component: {fileID: 846627340} + - component: {fileID: 846627339} + - component: {fileID: 846627338} + m_Layer: 5 + m_Name: ShowBannerButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &846627338 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 846627337} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 846627339} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &846627339 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 846627337} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 482bb6becec9f49fda9230607c1e1097, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &846627340 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 846627337} + m_CullTransparentMesh: 0 +--- !u!224 &846627341 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 846627337} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 524696372} + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.58} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 400, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1024874062 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1024874066} + - component: {fileID: 1024874065} + - component: {fileID: 1024874064} + - component: {fileID: 1024874063} + - component: {fileID: 1024874068} + - component: {fileID: 1024874069} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1024874063 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1024874062} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &1024874064 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1024874062} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &1024874065 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1024874062} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &1024874066 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1024874062} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1734153390} + - {fileID: 1034179796} + - {fileID: 636086478} + - {fileID: 1665391391} + - {fileID: 1382241166} + - {fileID: 846627341} + - {fileID: 1170807040} + - {fileID: 1972437132} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!114 &1024874068 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1024874062} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c4d7e2857b734401a7835410dd65776b, type: 3} + m_Name: + m_EditorClassIdentifier: + showBannerButton: {fileID: 846627338} + showMrecButton: {fileID: 1170807041} + showInterstitialButton: {fileID: 1034179793} + showRewardedButton: {fileID: 1665391392} + initializationStatusText: {fileID: 1972437130} + interstitialStatusText: {fileID: 636086476} + rewardedStatusText: {fileID: 1382241164} +--- !u!114 &1024874069 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1024874062} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: acafde15ea31470e81e2be191a938627, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1034179792 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1034179796} + - component: {fileID: 1034179795} + - component: {fileID: 1034179794} + - component: {fileID: 1034179793} + m_Layer: 5 + m_Name: ShowInterstitialButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1034179793 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1034179792} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1034179794} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1034179794 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1034179792} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 482bb6becec9f49fda9230607c1e1097, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1034179795 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1034179792} + m_CullTransparentMesh: 0 +--- !u!224 &1034179796 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1034179792} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1590164947} + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.7} + m_AnchorMax: {x: 0.5, y: 0.78} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 400, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1170807039 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1170807040} + - component: {fileID: 1170807043} + - component: {fileID: 1170807042} + - component: {fileID: 1170807041} + m_Layer: 5 + m_Name: ShowMRecButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1170807040 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1170807039} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1341077592} + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.4} + m_AnchorMax: {x: 0.5, y: 0.48} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 400, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1170807041 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1170807039} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1170807042} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1170807042 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1170807039} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 482bb6becec9f49fda9230607c1e1097, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1170807043 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1170807039} + m_CullTransparentMesh: 0 +--- !u!1 &1341077591 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1341077592} + - component: {fileID: 1341077594} + - component: {fileID: 1341077593} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1341077592 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1341077591} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1170807040} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 2.5} + m_SizeDelta: {x: -40, y: -15} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1341077593 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1341077591} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 40 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Show MREC +--- !u!222 &1341077594 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1341077591} + m_CullTransparentMesh: 0 +--- !u!1 &1382241163 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1382241166} + - component: {fileID: 1382241165} + - component: {fileID: 1382241164} + m_Layer: 5 + m_Name: RewardedStatusText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1382241164 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1382241163} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Loading... +--- !u!222 &1382241165 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1382241163} + m_CullTransparentMesh: 0 +--- !u!224 &1382241166 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1382241163} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.61} + m_AnchorMax: {x: 1, y: 0.67} + m_AnchoredPosition: {x: 105, y: 0} + m_SizeDelta: {x: -230, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1590164946 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1590164947} + - component: {fileID: 1590164949} + - component: {fileID: 1590164948} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1590164947 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1590164946} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1034179796} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 2.5} + m_SizeDelta: {x: -40, y: -15} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1590164948 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1590164946} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 40 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Show Interstitial +--- !u!222 &1590164949 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1590164946} + m_CullTransparentMesh: 0 +--- !u!1 &1665391390 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1665391391} + - component: {fileID: 1665391394} + - component: {fileID: 1665391393} + - component: {fileID: 1665391392} + m_Layer: 5 + m_Name: ShowRewardedButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1665391391 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1665391390} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 694260088} + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.6} + m_AnchorMax: {x: 0.5, y: 0.68} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 400, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1665391392 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1665391390} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1665391393} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1665391393 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1665391390} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 482bb6becec9f49fda9230607c1e1097, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1665391394 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1665391390} + m_CullTransparentMesh: 0 +--- !u!1 &1734153389 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1734153390} + - component: {fileID: 1734153392} + - component: {fileID: 1734153391} + m_Layer: 5 + m_Name: AppTitle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1734153390 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1734153389} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.8} + m_AnchorMax: {x: 0.5, y: 0.88} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 600, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1734153391 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1734153389} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 50 + m_FontStyle: 1 + m_BestFit: 1 + m_MinSize: 10 + m_MaxSize: 60 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: CloudX Arbiter/TPA Demo +--- !u!222 &1734153392 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1734153389} + m_CullTransparentMesh: 1 +--- !u!1 &1972437129 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1972437132} + - component: {fileID: 1972437131} + - component: {fileID: 1972437130} + m_Layer: 5 + m_Name: InitializationStatusText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1972437130 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1972437129} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: ff72041106c504e268ea0ba778b962ee, type: 3} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 0 + m_MaxSize: 300 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'Status: Not initialized' +--- !u!222 &1972437131 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1972437129} + m_CullTransparentMesh: 1 +--- !u!224 &1972437132 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1972437129} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1024874066} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.22} + m_AnchorMax: {x: 0.5, y: 0.28} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 500, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 519420032} + - {fileID: 1024874066} + - {fileID: 277484670} diff --git a/Assets/Scenes/ArbiterScene.unity.meta b/Assets/Scenes/ArbiterScene.unity.meta new file mode 100644 index 0000000..eb7064e --- /dev/null +++ b/Assets/Scenes/ArbiterScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0365ea9803b44c7da3c9fc6ebe403f63 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Arbiter.meta b/Assets/Scripts/Arbiter.meta new file mode 100644 index 0000000..55a5faa --- /dev/null +++ b/Assets/Scripts/Arbiter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5ff48743f31740239414917f7126a831 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Arbiter/ArbiterAdController.cs b/Assets/Scripts/Arbiter/ArbiterAdController.cs new file mode 100644 index 0000000..c81d207 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterAdController.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using CloudX; +using GoogleMobileAds.Api; +using UnityEngine; + +/* + * Shared base for the Arbiter/TPA controllers. Every format follows the same + * Trusted Arbiter rule - CloudX and AdMob load in parallel, the loaded ones + * become bids, CloudXSdk.Arbiter picks the platform - so the bookkeeping the + * formats share lives here once: ids, the loaded CloudXAd (the CloudX bid is + * built from it), the arbiter call with its in-flight guard, the AdMob bid, and + * the Google paid-event forwarding that prices AdMob bids. The two families + * split below: fullscreen (interstitial, rewarded) prepares a winner ahead of + * the placement in ArbiterFullscreenController; inline (banner, MREC) + * arbitrates and then renders on a refresh cycle in ArbiterInlineController. + * + * Platform identity uses the SDK's CloudXArbiterPlatform everywhere (CloudX, + * AdMob) so the same value names a loaded side, a bid, and a winner. + * + * A publisher copying this into an app takes this base plus the one family base + * and the one concrete they need. + */ +public abstract class ArbiterAdController : IDisposable +{ + private const string TAG = "CloudXUnityDemo"; + + public event Action AdLoaded; + public event Action AdLoadFailed; + public event Action AdClicked; + /* Every arbiter result, with the number of bids submitted. */ + public event Action ArbiterCompleted; + + protected string CloudXAdUnitId { get; } + protected string AdMobAdUnitId { get; } + + /* + * When CloudX initialization failed, its callbacks may never fire and its + * bridge is not initialized, so the controller skips the CloudX leg and + * decides locally: AdMob is the only candidate, so AdMob wins. + */ + protected bool CloudXAvailable { get; } + + protected bool IsDisposed { get; private set; } + protected bool IsLoadingCloudX { get; set; } + protected bool IsLoadingAdMob { get; set; } + protected bool ArbiterInFlight { get; private set; } + + /* The CloudXAd from OnAdLoadSuccess; its AdValues carry the arbiter payload. */ + protected CloudXAd LoadedCloudXAd { get; set; } + + /* + * Bumped by Hide/Dispose so an arbiter result that was in flight at that + * moment is reported but never acted on. + */ + private int _generation; + + protected ArbiterAdController(string cloudXAdUnitId, string adMobAdUnitId, bool cloudXAvailable) + { + CloudXAdUnitId = cloudXAdUnitId; + AdMobAdUnitId = adMobAdUnitId; + CloudXAvailable = cloudXAvailable; + } + + protected static void Log(string message) => Debug.Log($"[{TAG}][Arbiter] {message}"); + + public abstract void Load(); + + protected void RaiseAdLoaded(CloudXArbiterPlatform platform) => AdLoaded?.Invoke(platform); + protected void RaiseAdLoadFailed(CloudXArbiterPlatform platform, string message) => AdLoadFailed?.Invoke(platform, message); + protected void RaiseAdClicked(CloudXArbiterPlatform platform) => AdClicked?.Invoke(platform); + + protected void InvalidateInFlightArbiter() => _generation++; + + /* + * Runs one arbiter round over the loaded candidates. The SDK owns the + * timeout and the fallback and always completes: one bid wins without a + * service call, several bids go to the arbiter service or, when it is + * unavailable, to the local highest-price fallback. Nothing here wraps the + * call in a timer or compares prices - the docs forbid both. The callback + * arrives on the Unity main thread. + */ + protected void RunArbiter(List bids, Action onResult) + { + ArbiterInFlight = true; + var generation = _generation; + Log($"Arbiter: {bids.Count} bid(s) for {CloudXAdUnitId}"); + + Decide(bids, result => + { + ArbiterInFlight = false; + + if (IsDisposed) + { + return; + } + + Log($"Arbiter result: platform={result.Platform} platformName={result.PlatformName} " + + $"id={result.Id} bidId={result.BidId ?? "-"} bids={bids.Count}"); + ArbiterCompleted?.Invoke(result, bids.Count); + + if (generation == _generation) + { + onResult(result); + } + }); + } + + private void Decide(List bids, Action onResult) + { + if (CloudXAvailable) + { + CloudXSdk.Arbiter(bids, onResult); + return; + } + + /* + * CloudX never initialized, so the bids can only be the AdMob one. The + * SDK would select a lone bid without a service call anyway; deciding + * locally keeps the flow uniform without touching an uninitialized SDK. + */ + onResult(new CloudXArbiterResult( + Id: "local", + Platform: CloudXArbiterPlatform.AdMob, + PlatformName: "AdMob", + BidId: null, + Extras: new Dictionary())); + } + + /* + * An AdMob bid carries no price: CloudX prices it from the realized revenue + * reported through ReportAdMobPaidEvent. NetworkName is the ad source that + * filled, when Google exposes it. + */ + protected CloudXArbiterBid AdMobBid(string adSourceName) => + new CloudXArbiterBid.AdMob(AdMobAdUnitId, NetworkName: adSourceName ?? "admob"); + + protected static string AdSourceName(ResponseInfo responseInfo) => + responseInfo?.GetLoadedAdapterResponseInfo()?.AdSourceName; + + /* + * Required part of the AdMob integration: forward Google's impression-level + * revenue so CloudX learns what AdMob demand actually pays. AdValue.Value is + * in micro-units of CurrencyCode. + */ + protected void ReportAdMobPaidEvent(AdValue adValue, string adFormat, string adSourceName) + { + if (!CloudXAvailable) + { + Log("AdMob paid event not forwarded: CloudX is not initialized"); + return; + } + + var accepted = CloudXSdk.ReportRevenueData(new CloudXRevenueData( + Platform: CloudXRevenuePlatform.AdMob, + Revenue: adValue.Value / 1_000_000.0, + AdFormat: adFormat, + CurrencyCode: adValue.CurrencyCode, + Precision: ToCloudXRevenuePrecision(adValue.Precision), + NetworkName: adSourceName, + AdUnitId: AdMobAdUnitId)); + + Log($"ReportRevenueData({adFormat}, {adValue.Value} micros {adValue.CurrencyCode}, " + + $"{adValue.Precision}) accepted={accepted}"); + } + + 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, + }; + + /* + * Subscribe/unsubscribe the CloudX callbacks. Called from the concrete + * constructor (not here) so the subclass is fully constructed first. + */ + protected abstract void SubscribeCloudXCallbacks(); + protected abstract void UnsubscribeCloudXCallbacks(); + protected abstract void DestroyCloudXAd(); + protected abstract void DestroyAdMobAd(); + + public void Dispose() + { + if (IsDisposed) + { + return; + } + + IsDisposed = true; + InvalidateInFlightArbiter(); + + UnsubscribeCloudXCallbacks(); + + if (CloudXAvailable) + { + DestroyCloudXAd(); + } + + DestroyAdMobAd(); + } +} diff --git a/Assets/Scripts/Arbiter/ArbiterAdController.cs.meta b/Assets/Scripts/Arbiter/ArbiterAdController.cs.meta new file mode 100644 index 0000000..5c86d22 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterAdController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7a8011a978b54f3597830b71f333656c \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterBannerController.cs b/Assets/Scripts/Arbiter/ArbiterBannerController.cs new file mode 100644 index 0000000..fbf722f --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterBannerController.cs @@ -0,0 +1,113 @@ +using CloudX; +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; + +/* + * Arbiter/TPA banner. Shared flow lives in ArbiterInlineController; this class + * only supplies the banner SDK calls. Top banner on both SDKs. Auto-refresh is + * kept off on both sides - see the ArbiterInlineController class note; the + * crucial CloudX call is StopBannerAutoRefresh before create. + */ +public sealed class ArbiterBannerController : ArbiterInlineController +{ + private const CloudXAdViewConfiguration.AdViewPosition CloudXPosition = + CloudXAdViewConfiguration.AdViewPosition.TopCenter; + + private BannerView _adMobBanner; + + public ArbiterBannerController( + string cloudXAdUnitId, + string adMobAdUnitId, + bool cloudXAvailable, + float refreshIntervalSeconds) + : base(cloudXAdUnitId, adMobAdUnitId, cloudXAvailable, refreshIntervalSeconds) + { + SubscribeCloudXCallbacks(); + } + + protected override string AdFormatName => "banner"; + + protected override void SubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Banner.OnAdLoadSuccess += CloudXOnLoadSuccess; + CloudXAdsCallbacks.Banner.OnAdLoadFailed += CloudXOnLoadFailed; + CloudXAdsCallbacks.Banner.OnAdClicked += CloudXOnClicked; + CloudXAdsCallbacks.Banner.OnAdRevenuePaid += CloudXOnRevenuePaid; + } + + protected override void UnsubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Banner.OnAdLoadSuccess -= CloudXOnLoadSuccess; + CloudXAdsCallbacks.Banner.OnAdLoadFailed -= CloudXOnLoadFailed; + CloudXAdsCallbacks.Banner.OnAdClicked -= CloudXOnClicked; + CloudXAdsCallbacks.Banner.OnAdRevenuePaid -= CloudXOnRevenuePaid; + } + + protected override void CloudXCreateAndLoad() + { + CloudXSdk.DestroyBanner(CloudXAdUnitId); + + /* + * Required, not optional: CloudX banner auto-refresh is opt-out, so + * without this the first ShowBanner would start a background reload that + * races the arbiter. It goes before CreateBanner: the native layer + * registers the ad unit as refresh-disabled even with no view yet, then + * creates the view with refresh already off, so no timer ever runs. + * (Destroy clears that registration, hence this order.) It also unlocks + * the explicit LoadBanner used for later rounds. + */ + CloudXSdk.StopBannerAutoRefresh(CloudXAdUnitId); + + /* + * Placement and custom data must be set before CreateBanner so they are + * on the first request. CreateBanner also issues the first load. + */ + CloudXSdk.SetBannerPlacement(CloudXAdUnitId, "arbiter_screen"); + CloudXSdk.SetBannerCustomData(CloudXAdUnitId, "arbiter_banner_data"); + CloudXSdk.CreateBanner(CloudXAdUnitId, new CloudXAdViewConfiguration(CloudXPosition)); + } + + protected override void CloudXLoad() => CloudXSdk.LoadBanner(CloudXAdUnitId); + protected override void CloudXShow() => CloudXSdk.ShowBanner(CloudXAdUnitId); + protected override void CloudXHide() => CloudXSdk.HideBanner(CloudXAdUnitId); + protected override void DestroyCloudXAd() => CloudXSdk.DestroyBanner(CloudXAdUnitId); + + protected override void AdMobCreateHidden() + { + DestroyAdMobAd(); + + /* + * A BannerView loads once per LoadAd; there is no refresh API to turn off + * here. Its refresh is the ad unit's Automatic refresh setting in the + * AdMob console, which MUST be Disabled for this unit. Google Mobile Ads + * raises its callbacks off the Unity main thread; ExecuteInUpdate moves + * them back on. + */ + _adMobBanner = new BannerView(AdMobAdUnitId, AdSize.Banner, AdPosition.Top); + + _adMobBanner.OnBannerAdLoaded += () => MobileAdsEventExecutor.ExecuteInUpdate(OnAdMobLoaded); + + _adMobBanner.OnBannerAdLoadFailed += error => MobileAdsEventExecutor.ExecuteInUpdate(() => + OnAdMobLoadFailed(error.GetMessage())); + + _adMobBanner.OnAdClicked += () => MobileAdsEventExecutor.ExecuteInUpdate(OnAdMobClicked); + _adMobBanner.OnAdImpressionRecorded += () => MobileAdsEventExecutor.ExecuteInUpdate(OnAdMobImpression); + + /* Required: this is how CloudX learns what the AdMob bid was worth. */ + _adMobBanner.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() => OnAdMobPaid(adValue)); + + /* Created hidden: a load must never render a view the arbiter did not pick. */ + _adMobBanner.Hide(); + } + + protected override void AdMobLoad() => _adMobBanner.LoadAd(new AdRequest()); + protected override void AdMobShow() => _adMobBanner.Show(); + protected override void AdMobHide() => _adMobBanner?.Hide(); + protected override ResponseInfo AdMobResponseInfo() => _adMobBanner?.GetResponseInfo(); + + protected override void DestroyAdMobAd() + { + _adMobBanner?.Destroy(); + _adMobBanner = null; + } +} diff --git a/Assets/Scripts/Arbiter/ArbiterBannerController.cs.meta b/Assets/Scripts/Arbiter/ArbiterBannerController.cs.meta new file mode 100644 index 0000000..48c9815 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterBannerController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c7d242bad30d46bb930db895a0857b26 \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterConfig.cs b/Assets/Scripts/Arbiter/ArbiterConfig.cs new file mode 100644 index 0000000..7e78b0f --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterConfig.cs @@ -0,0 +1,29 @@ +/* + * Arbiter/TPA demo switches. The CloudX and AdMob ad unit ids both come from + * DemoConfig (the AdMob ones are Google's official test units). + * + * The AdMob banner and MREC units MUST have Automatic refresh set to Disabled + * in the AdMob console. The arbiter cycle owns refresh here: it decides when a + * new fill is requested and which network's view is on screen. An AdMob unit + * that refreshes on its own would swap the creative behind the arbiter's back. + */ +public static class ArbiterConfig +{ + /* + * Flip to true to watch the single-bid path: CloudX is asked to fill an + * unknown ad unit and fails, so AdMob is the only bid in every round and the + * SDK selects it without a service call. + */ + public const bool ForceCloudXNoFill = false; + + /* + * Banner and MREC re-arbitrate on this interval (docs recommend 20-30 s; + * shorter intervals decrease CPM performance). + */ + public const float InlineRefreshIntervalSeconds = 25f; + + private const string InvalidCloudXAdUnitId = "arbiter-invalid-unit"; + + public static string CloudXAdUnitOrInvalid(string realAdUnitId) => + ForceCloudXNoFill ? InvalidCloudXAdUnitId : realAdUnitId; +} diff --git a/Assets/Scripts/Arbiter/ArbiterConfig.cs.meta b/Assets/Scripts/Arbiter/ArbiterConfig.cs.meta new file mode 100644 index 0000000..c451b37 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e92cf3cfd62c471f93527a0f664c5eb5 \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs new file mode 100644 index 0000000..468df33 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using CloudX; + +/* + * Fullscreen Arbiter/TPA controllers (interstitial, rewarded): the + * prepare-ahead rule from docs.cloudx.io -> Trusted Arbiter. Both networks load + * in parallel; once both have settled (loaded or failed) the loaded ones are + * submitted to CloudXSdk.Arbiter and the result is stored. Show() then shows the + * stored winner immediately - no arbiter call and no network call on the show + * path - and returns false when no winner is prepared, in which case the caller + * carries on with the game. The cycle restarts (reload what is missing, + * re-arbitrate) after the ad closes or fails to show. + * + * The CloudX callback handlers below filter by ad unit id and drive the shared + * state; the concrete subclass only routes the right CloudXAdsCallbacks group + * into them (SubscribeCloudXCallbacks) and supplies the format's SDK calls. + */ +public abstract class ArbiterFullscreenController : ArbiterAdController +{ + public event Action AdShown; + public event Action AdShowFailed; + public event Action AdClosed; + + private bool _cloudXSettled; + private bool _adMobSettled; + private CloudXArbiterResult _nextWinner; + + protected ArbiterFullscreenController( + string cloudXAdUnitId, + string adMobAdUnitId, + bool cloudXAvailable) + : base(cloudXAdUnitId, adMobAdUnitId, cloudXAvailable) + { + /* No CloudX leg to wait for. */ + _cloudXSettled = !cloudXAvailable; + } + + /* The platform Show() would use right now; null when no winner is stored. */ + public CloudXArbiterPlatform? PreparedWinner => + _nextWinner == null || _nextWinner.Platform == CloudXArbiterPlatform.None + ? null + : _nextWinner.Platform; + + /* + * True when nothing is loaded, loading or decided, so only a fresh Load() + * can move things forward. The screen's backoff retry keys off this: while a + * candidate, a load or a round is pending, the controller gets there itself. + */ + public bool NeedsRetry => + !IsDisposed && !ArbiterInFlight && PreparedWinner == null + && !IsLoadingCloudX && !IsLoadingAdMob && !CloudXHasBid && !AdMobCanShow(); + + private bool CloudXHasBid => LoadedCloudXAd != null && CloudXIsReady(); + + /* + * Loads whichever network does not hold a fill. After one side failed, a + * retry reloads only that side and re-arbitrates when it settles. + */ + public override void Load() + { + if (IsDisposed || ArbiterInFlight) + { + return; + } + + if (CloudXAvailable && !CloudXHasBid && !IsLoadingCloudX) + { + _cloudXSettled = false; + LoadedCloudXAd = null; + IsLoadingCloudX = true; + CloudXLoad(); + } + + if (!AdMobCanShow() && !IsLoadingAdMob) + { + _adMobSettled = false; + IsLoadingAdMob = true; + DestroyAdMobAd(); + AdMobLoad(); + } + + /* + * Both sides may already hold a fill with no winner stored - after a + * None result, or after a stale winner was dropped - in which case there + * is nothing to load and the round has to be run again from here. + */ + if (_nextWinner == null) + { + MaybePrepareWinner(); + } + } + + /* + * Shows the stored winner. A stale winner (its ad expired or was consumed) + * is dropped and false is returned; the caller reloads. + */ + public bool Show() + { + if (IsDisposed || _nextWinner == null) + { + return false; + } + + var winner = _nextWinner.Platform; + _nextWinner = null; + + switch (winner) + { + case CloudXArbiterPlatform.CloudX when CloudXHasBid: + CloudXShow(); + return true; + case CloudXArbiterPlatform.CloudX: + LoadedCloudXAd = null; + return false; + case CloudXArbiterPlatform.AdMob when AdMobCanShow(): + AdMobShow(); + return true; + case CloudXArbiterPlatform.AdMob: + DestroyAdMobAd(); + return false; + default: + return false; + } + } + + /* + * Runs the arbiter once both networks have settled and stores the result. + * With no loaded candidate there is nothing to arbitrate; the screen's + * backoff retry calls Load() again. + */ + private void MaybePrepareWinner() + { + if (IsDisposed || ArbiterInFlight || !_cloudXSettled || !_adMobSettled) + { + return; + } + + var bids = new List(); + + if (CloudXAvailable && CloudXHasBid) + { + bids.Add(new CloudXArbiterBid.CloudX(LoadedCloudXAd)); + } + + if (AdMobCanShow()) + { + bids.Add(AdMobBid(AdMobAdSourceName())); + } + + if (bids.Count == 0) + { + return; + } + + RunArbiter(bids, result => _nextWinner = result); + } + + protected void RaiseAdShown(CloudXArbiterPlatform platform) => AdShown?.Invoke(platform); + protected void RaiseAdShowFailed(CloudXArbiterPlatform platform, string message) => AdShowFailed?.Invoke(platform, message); + protected void RaiseAdClosed(CloudXArbiterPlatform platform) => AdClosed?.Invoke(platform); + + /* + * CloudX callback handlers, shared by both fullscreen formats. The concrete + * subscribes the matching CloudXAdsCallbacks group to these. + */ + protected void CloudXOnLoadSuccess(CloudXAd ad) + { + if (ad.AdUnitId != CloudXAdUnitId) + { + return; + } + + IsLoadingCloudX = false; + LoadedCloudXAd = ad; + _cloudXSettled = true; + RaiseAdLoaded(CloudXArbiterPlatform.CloudX); + MaybePrepareWinner(); + } + + protected void CloudXOnLoadFailed(string adUnitId, CloudXError error) + { + if (adUnitId != CloudXAdUnitId) + { + return; + } + + IsLoadingCloudX = false; + LoadedCloudXAd = null; + _cloudXSettled = true; + RaiseAdLoadFailed(CloudXArbiterPlatform.CloudX, error.Message); + MaybePrepareWinner(); + } + + protected void CloudXOnShowSuccess(CloudXAd ad) + { + if (ad.AdUnitId == CloudXAdUnitId) + { + RaiseAdShown(CloudXArbiterPlatform.CloudX); + } + } + + protected void CloudXOnShowFailed(CloudXAd ad, CloudXError error) + { + if (ad.AdUnitId != CloudXAdUnitId) + { + return; + } + + LoadedCloudXAd = null; + RaiseAdShowFailed(CloudXArbiterPlatform.CloudX, error.Message); + } + + protected void CloudXOnHidden(CloudXAd ad) + { + if (ad.AdUnitId != CloudXAdUnitId) + { + return; + } + + /* The fill is consumed; the next Load() requests a new one. */ + LoadedCloudXAd = null; + RaiseAdClosed(CloudXArbiterPlatform.CloudX); + } + + protected void CloudXOnClicked(CloudXAd ad) + { + if (ad.AdUnitId == CloudXAdUnitId) + { + RaiseAdClicked(CloudXArbiterPlatform.CloudX); + } + } + + /* + * AdMob load results, reported by the concrete on the Unity main thread + * after it stored (or destroyed) the ad object. + */ + protected void OnAdMobLoaded() + { + IsLoadingAdMob = false; + _adMobSettled = true; + RaiseAdLoaded(CloudXArbiterPlatform.AdMob); + MaybePrepareWinner(); + } + + protected void OnAdMobLoadFailed(string message) + { + IsLoadingAdMob = false; + _adMobSettled = true; + RaiseAdLoadFailed(CloudXArbiterPlatform.AdMob, message); + MaybePrepareWinner(); + } + + /* Format-specific SDK calls. */ + protected abstract bool CloudXIsReady(); + protected abstract void CloudXLoad(); + protected abstract void CloudXShow(); + protected abstract bool AdMobCanShow(); + protected abstract void AdMobLoad(); + protected abstract void AdMobShow(); + protected abstract string AdMobAdSourceName(); +} diff --git a/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs.meta b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs.meta new file mode 100644 index 0000000..43186ae --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 089aef4926504e6eb090be737648afb4 \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterInlineController.cs b/Assets/Scripts/Arbiter/ArbiterInlineController.cs new file mode 100644 index 0000000..3ba730d --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterInlineController.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using CloudX; +using GoogleMobileAds.Api; + +/* + * Inline Arbiter/TPA controllers (banner, MREC): the arbitrate-then-render rule + * from docs.cloudx.io -> Trusted Arbiter -> Banner and MREC arbitration. Both + * networks load in parallel and stay loaded; when both have settled the loaded + * ones are submitted to CloudXSdk.Arbiter and the winner's view is shown from + * the completion callback. The loser's view is never shown - it would render + * and fire an impression for a bid the arbiter did not select - it just keeps + * its fill for the next round. + * + * Auto-refresh is OFF on both sides and this class drives the cycle itself: + * 1. The shown winner's impression marks its fill as consumed; the loser keeps + * its fill. + * 2. Rounds repeat on RefreshIntervalSeconds (docs: 20-30 s). When the + * interval elapses the consumed winner (and any network without a fill) is + * reloaded, and the round runs once those loads settle. Settled loads never + * start a round on their own once something is on screen, so there is + * exactly one arbiter call per interval and never one over a half-loaded + * candidate set. + * 3. Only the new winner's view is shown; the other is hidden. + * + * One deliberate deviation from the docs' cycle, which reloads the winner the + * moment its impression fires: both Unity plugins reload into the view that is + * on screen, so an immediate reload replaced the visible creative within a + * second of showing it (verified on device, with a second AdMob paid event). + * Reloading at the interval keeps each shown creative up for the whole + * interval; the cost is the load latency before the swap. + * + * CloudX inline auto-refresh is opt-out: the concrete's CloudXCreateAndLoad calls + * Stop*AutoRefresh before create and nothing here calls Start*AutoRefresh. AdMob + * has no refresh API at all: the ad unit's Automatic refresh MUST be Disabled in + * the AdMob console, or AdMob swaps the creative behind the arbiter's back. + * + * The refresh clock is fed by the owning MonoBehaviour through Tick(), so this + * stays a plain class like the fullscreen controllers. + */ +public abstract class ArbiterInlineController : ArbiterAdController +{ + /* + * The platform whose view is on screen after a round; None when the round + * produced no winner and both views were hidden. + */ + public event Action WinnerShown; + + private readonly float _refreshIntervalSeconds; + + private bool _cloudXCreated; + private bool _cloudXLoaded; + private bool _adMobCreated; + private bool _adMobLoaded; + private string _adMobAdSourceName; + + /* User intent from the toggle button; the clock runs while this is set. */ + private bool _wantShown; + private CloudXArbiterPlatform? _shownPlatform; + private float _refreshTimer; + private int _roundId; + private int _impressionHandledRound = -1; + /* The shown winner's impression fired; its fill is reloaded when the interval elapses. */ + private bool _winnerConsumed; + + protected ArbiterInlineController( + string cloudXAdUnitId, + string adMobAdUnitId, + bool cloudXAvailable, + float refreshIntervalSeconds) + : base(cloudXAdUnitId, adMobAdUnitId, cloudXAvailable) + { + _refreshIntervalSeconds = refreshIntervalSeconds; + } + + public bool IsShown => _wantShown; + + /* What is actually on screen; null while nothing is. */ + public CloudXArbiterPlatform? ShownPlatform => _shownPlatform; + + /* + * True when nothing is loaded, loading or on screen, so only a fresh Load() + * can move things forward. The screen's backoff retry keys off this; in + * every other state the cycle re-requests fills itself. + */ + public bool NeedsRetry => + !IsDisposed && !ArbiterInFlight && _shownPlatform == null && !AnyLoaded && !AnyLoading; + + private bool AnyLoaded => _cloudXLoaded || _adMobLoaded; + private bool AnyLoading => IsLoadingCloudX || IsLoadingAdMob; + + /* + * Loads whichever network does not hold a fill. The first CloudX load comes + * from create (Destroy -> Stop*AutoRefresh -> Create); later ones reuse the + * view, which is allowed once refresh is stopped. AdMob's view is created + * hidden once and reloaded in place. + */ + public override void Load() + { + if (IsDisposed) + { + return; + } + + if (CloudXAvailable && !_cloudXLoaded && !IsLoadingCloudX) + { + IsLoadingCloudX = true; + + if (_cloudXCreated) + { + CloudXLoad(); + } + else + { + _cloudXCreated = true; + CloudXCreateAndLoad(); + } + } + + if (!_adMobLoaded && !IsLoadingAdMob) + { + IsLoadingAdMob = true; + + if (!_adMobCreated) + { + _adMobCreated = true; + AdMobCreateHidden(); + } + + AdMobLoad(); + } + } + + /* + * Asks for the ad to be on screen. A round runs as soon as the candidates + * have settled (immediately, if they already have) and its winner is shown + * from the arbiter callback; WinnerShown reports which. + */ + public void Show() + { + if (IsDisposed || _wantShown) + { + return; + } + + _wantShown = true; + _refreshTimer = 0f; + + if (!AnyLoaded && !AnyLoading) + { + Load(); + } + + TryRunRound(); + } + + /* + * Hides both views and stops the clock. Fills are kept, so the next Show() + * arbitrates over them without a reload. A round in flight is reported but + * shows nothing. + */ + public void Hide() + { + if (IsDisposed || !_wantShown) + { + return; + } + + _wantShown = false; + _refreshTimer = 0f; + InvalidateInFlightArbiter(); + HideBothViews(); + /* Before the shown platform is forgotten: a consumed fill must not be re-shown later. */ + ReloadConsumedWinner(); + _shownPlatform = null; + } + + /* Advances the refresh clock; called every frame by the owner. */ + public void Tick(float deltaSeconds) + { + if (IsDisposed || !_wantShown) + { + return; + } + + _refreshTimer += deltaSeconds; + + if (_refreshTimer >= _refreshIntervalSeconds) + { + ReloadConsumedWinner(); + TryRunRound(); + } + } + + /* + * Requests the next fill for the winner whose impression fired. Runs when + * the interval elapses (or the ad is hidden), never right after the + * impression - see the class note. Load() also re-requests any network that + * did not fill last round. + */ + private void ReloadConsumedWinner() + { + if (!_winnerConsumed) + { + return; + } + + _winnerConsumed = false; + + if (_shownPlatform == CloudXArbiterPlatform.CloudX) + { + _cloudXLoaded = false; + } + else if (_shownPlatform == CloudXArbiterPlatform.AdMob) + { + _adMobLoaded = false; + } + + Load(); + } + + /* + * The one gate for arbitration: the ad is wanted, no round is in flight, no + * load is outstanding, and a round is due - nothing is on screen yet, or the + * interval has elapsed. Load settlement, the clock and Show() all end up + * here, so whichever condition becomes true last starts the round, once. + */ + private void TryRunRound() + { + if (IsDisposed || !_wantShown || ArbiterInFlight || AnyLoading) + { + return; + } + + var due = _shownPlatform == null || _refreshTimer >= _refreshIntervalSeconds; + if (!due) + { + return; + } + + var bids = new List(); + + if (CloudXAvailable && _cloudXLoaded) + { + bids.Add(new CloudXArbiterBid.CloudX(LoadedCloudXAd)); + } + + if (_adMobLoaded) + { + bids.Add(AdMobBid(_adMobAdSourceName)); + } + + if (bids.Count == 0) + { + /* + * Nothing to arbitrate. With a stale creative still on screen, keep it + * up and re-request both fills on the interval; with nothing shown, + * the screen's backoff retry owns the reload. + */ + if (_shownPlatform != null) + { + _refreshTimer = 0f; + Load(); + } + + return; + } + + _refreshTimer = 0f; + RunArbiter(bids, OnRoundDecided); + } + + private void OnRoundDecided(CloudXArbiterResult result) + { + switch (result.Platform) + { + case CloudXArbiterPlatform.CloudX when _cloudXLoaded: + AdMobHide(); + CloudXShow(); + ShowingWinner(CloudXArbiterPlatform.CloudX); + break; + case CloudXArbiterPlatform.AdMob when _adMobLoaded: + if (CloudXAvailable) + { + CloudXHide(); + } + + AdMobShow(); + ShowingWinner(CloudXArbiterPlatform.AdMob); + break; + default: + /* + * None (nothing locally priceable during fallback) or a winner + * whose fill is gone: show nobody. The clock keeps running, so + * the next interval retries over the retained fills. + */ + _shownPlatform = null; + HideBothViews(); + WinnerShown?.Invoke(CloudXArbiterPlatform.None); + break; + } + } + + private void ShowingWinner(CloudXArbiterPlatform platform) + { + _shownPlatform = platform; + _roundId++; + _winnerConsumed = false; + WinnerShown?.Invoke(platform); + } + + private void HideBothViews() + { + if (CloudXAvailable) + { + CloudXHide(); + } + + AdMobHide(); + } + + /* + * The shown winner's impression: its fill is consumed and gets reloaded when + * the interval elapses (the loser keeps its fill). Counted once per round. + */ + private void OnImpression(CloudXArbiterPlatform platform) + { + if (IsDisposed || !_wantShown || platform != _shownPlatform || _impressionHandledRound == _roundId) + { + return; + } + + _impressionHandledRound = _roundId; + _winnerConsumed = true; + } + + /* + * CloudX callback handlers, shared by both inline formats. The concrete + * subscribes the matching CloudXAdsCallbacks group to these. + */ + protected void CloudXOnLoadSuccess(CloudXAd ad) + { + if (ad.AdUnitId != CloudXAdUnitId) + { + return; + } + + IsLoadingCloudX = false; + _cloudXLoaded = true; + LoadedCloudXAd = ad; + RaiseAdLoaded(CloudXArbiterPlatform.CloudX); + TryRunRound(); + } + + protected void CloudXOnLoadFailed(string adUnitId, CloudXError error) + { + if (adUnitId != CloudXAdUnitId) + { + return; + } + + IsLoadingCloudX = false; + _cloudXLoaded = false; + RaiseAdLoadFailed(CloudXArbiterPlatform.CloudX, error.Message); + /* The other network may still win a round on its own. */ + TryRunRound(); + } + + protected void CloudXOnClicked(CloudXAd ad) + { + if (ad.AdUnitId == CloudXAdUnitId) + { + RaiseAdClicked(CloudXArbiterPlatform.CloudX); + } + } + + protected void CloudXOnRevenuePaid(CloudXAd ad) + { + if (ad.AdUnitId == CloudXAdUnitId) + { + OnImpression(CloudXArbiterPlatform.CloudX); + } + } + + /* + * AdMob results, reported by the concrete on the Unity main thread. They can + * arrive after Dispose (the callback was already queued), so they check + * IsDisposed first and never raise into a screen that is gone. + */ + protected void OnAdMobLoaded() + { + IsLoadingAdMob = false; + + if (IsDisposed) + { + return; + } + + _adMobAdSourceName = AdSourceName(AdMobResponseInfo()); + + /* A load must never reveal the loser's view. */ + if (_shownPlatform != CloudXArbiterPlatform.AdMob) + { + AdMobHide(); + } + + _adMobLoaded = true; + RaiseAdLoaded(CloudXArbiterPlatform.AdMob); + TryRunRound(); + } + + protected void OnAdMobLoadFailed(string message) + { + IsLoadingAdMob = false; + + if (IsDisposed) + { + return; + } + + _adMobLoaded = false; + RaiseAdLoadFailed(CloudXArbiterPlatform.AdMob, message); + TryRunRound(); + } + + protected void OnAdMobClicked() + { + if (!IsDisposed) + { + RaiseAdClicked(CloudXArbiterPlatform.AdMob); + } + } + + protected void OnAdMobImpression() => OnImpression(CloudXArbiterPlatform.AdMob); + + /* + * Required AdMob paid-event forwarding, then the impression handling (which + * is idempotent with OnAdMobImpression, in case only one of the two fires). + */ + protected void OnAdMobPaid(AdValue adValue) + { + if (IsDisposed) + { + return; + } + + ReportAdMobPaidEvent(adValue, AdFormatName, _adMobAdSourceName); + OnImpression(CloudXArbiterPlatform.AdMob); + } + + /* "banner" or "mrec", the AdFormat reported with AdMob revenue. */ + protected abstract string AdFormatName { get; } + + /* + * Format-specific SDK calls. CloudXCreateAndLoad must Stop*AutoRefresh, set + * placement/custom data, then create the view (which issues the first load) + * - it must not Start*AutoRefresh. CloudXLoad reloads the existing view. + * AdMobCreateHidden creates the BannerView, wires its events through + * ExecuteInUpdate and hides it before any load. + */ + protected abstract void CloudXCreateAndLoad(); + protected abstract void CloudXLoad(); + protected abstract void CloudXShow(); + protected abstract void CloudXHide(); + protected abstract void AdMobCreateHidden(); + protected abstract void AdMobLoad(); + protected abstract void AdMobShow(); + protected abstract void AdMobHide(); + protected abstract ResponseInfo AdMobResponseInfo(); +} diff --git a/Assets/Scripts/Arbiter/ArbiterInlineController.cs.meta b/Assets/Scripts/Arbiter/ArbiterInlineController.cs.meta new file mode 100644 index 0000000..c42be48 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterInlineController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fdd2ea6de1d34ce39fb66baca7d23480 \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterInterstitialController.cs b/Assets/Scripts/Arbiter/ArbiterInterstitialController.cs new file mode 100644 index 0000000..b5efb60 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterInterstitialController.cs @@ -0,0 +1,115 @@ +using CloudX; +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; + +/* + * Arbiter/TPA interstitial. Shared flow lives in ArbiterFullscreenController; + * this class only supplies the interstitial SDK calls for each side. + */ +public sealed class ArbiterInterstitialController : ArbiterFullscreenController +{ + private const string AdFormatName = "interstitial"; + + private InterstitialAd _adMobInterstitial; + + public ArbiterInterstitialController( + string cloudXAdUnitId, + string adMobAdUnitId, + bool cloudXAvailable) + : base(cloudXAdUnitId, adMobAdUnitId, cloudXAvailable) + { + SubscribeCloudXCallbacks(); + } + + protected override void SubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Interstitial.OnAdLoadSuccess += CloudXOnLoadSuccess; + CloudXAdsCallbacks.Interstitial.OnAdLoadFailed += CloudXOnLoadFailed; + CloudXAdsCallbacks.Interstitial.OnAdShowSuccess += CloudXOnShowSuccess; + CloudXAdsCallbacks.Interstitial.OnAdShowFailed += CloudXOnShowFailed; + CloudXAdsCallbacks.Interstitial.OnAdHidden += CloudXOnHidden; + CloudXAdsCallbacks.Interstitial.OnAdClicked += CloudXOnClicked; + } + + protected override void UnsubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Interstitial.OnAdLoadSuccess -= CloudXOnLoadSuccess; + CloudXAdsCallbacks.Interstitial.OnAdLoadFailed -= CloudXOnLoadFailed; + CloudXAdsCallbacks.Interstitial.OnAdShowSuccess -= CloudXOnShowSuccess; + CloudXAdsCallbacks.Interstitial.OnAdShowFailed -= CloudXOnShowFailed; + CloudXAdsCallbacks.Interstitial.OnAdHidden -= CloudXOnHidden; + CloudXAdsCallbacks.Interstitial.OnAdClicked -= CloudXOnClicked; + } + + protected override bool CloudXIsReady() => CloudXSdk.IsInterstitialReady(CloudXAdUnitId); + protected override void CloudXLoad() => CloudXSdk.LoadInterstitial(CloudXAdUnitId); + protected override void CloudXShow() => CloudXSdk.ShowInterstitial(CloudXAdUnitId); + protected override void DestroyCloudXAd() => CloudXSdk.DestroyInterstitial(CloudXAdUnitId); + + protected override bool AdMobCanShow() => _adMobInterstitial != null && _adMobInterstitial.CanShowAd(); + protected override string AdMobAdSourceName() => AdSourceName(_adMobInterstitial?.GetResponseInfo()); + + protected override void AdMobLoad() + { + /* + * Google Mobile Ads raises its callbacks off the Unity main thread. + * ExecuteInUpdate moves the whole body onto it, so controller state and + * the events subscribers use for UI both stay on one thread, like the + * CloudX callbacks. + */ + InterstitialAd.Load( + AdMobAdUnitId, + new AdRequest(), + (ad, error) => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + if (IsDisposed) + { + ad?.Destroy(); + return; + } + + if (error != null || ad == null) + { + OnAdMobLoadFailed(error?.GetMessage() ?? "AdMob returned no ad"); + return; + } + + _adMobInterstitial = ad; + RegisterAdMobEvents(ad); + OnAdMobLoaded(); + })); + } + + protected override void AdMobShow() => _adMobInterstitial.Show(); + + protected override void DestroyAdMobAd() + { + _adMobInterstitial?.Destroy(); + _adMobInterstitial = null; + } + + private void RegisterAdMobEvents(InterstitialAd ad) + { + ad.OnAdFullScreenContentOpened += () => MobileAdsEventExecutor.ExecuteInUpdate(() => + RaiseAdShown(CloudXArbiterPlatform.AdMob)); + + ad.OnAdFullScreenContentClosed += () => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + DestroyAdMobAd(); + RaiseAdClosed(CloudXArbiterPlatform.AdMob); + }); + + ad.OnAdFullScreenContentFailed += error => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + DestroyAdMobAd(); + RaiseAdShowFailed(CloudXArbiterPlatform.AdMob, error.GetMessage()); + }); + + ad.OnAdClicked += () => MobileAdsEventExecutor.ExecuteInUpdate(() => + RaiseAdClicked(CloudXArbiterPlatform.AdMob)); + + /* Required: this is how CloudX learns what the AdMob bid was worth. */ + ad.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() => + ReportAdMobPaidEvent(adValue, AdFormatName, AdSourceName(ad.GetResponseInfo()))); + } +} diff --git a/Assets/Scripts/Arbiter/ArbiterInterstitialController.cs.meta b/Assets/Scripts/Arbiter/ArbiterInterstitialController.cs.meta new file mode 100644 index 0000000..a61414e --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterInterstitialController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7dbdaf7c30934f1ba8a0ad7eb95e8d94 \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterMrecController.cs b/Assets/Scripts/Arbiter/ArbiterMrecController.cs new file mode 100644 index 0000000..7588432 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterMrecController.cs @@ -0,0 +1,115 @@ +using CloudX; +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; + +/* + * Arbiter/TPA MREC (300x250). Shared flow lives in ArbiterInlineController; + * this class only supplies the MREC SDK calls: bottom-center on both SDKs, MREC + * size on AdMob. MREC takes an AdViewPosition only (a vertical config throws). + * Auto-refresh is kept off on both sides - see the ArbiterInlineController class + * note; the crucial CloudX call is StopMrecAutoRefresh before create. Note the + * capital-R setter names against the lowercase-r lifecycle methods. + */ +public sealed class ArbiterMrecController : ArbiterInlineController +{ + private const CloudXAdViewConfiguration.AdViewPosition CloudXPosition = + CloudXAdViewConfiguration.AdViewPosition.BottomCenter; + + private BannerView _adMobMrec; + + public ArbiterMrecController( + string cloudXAdUnitId, + string adMobAdUnitId, + bool cloudXAvailable, + float refreshIntervalSeconds) + : base(cloudXAdUnitId, adMobAdUnitId, cloudXAvailable, refreshIntervalSeconds) + { + SubscribeCloudXCallbacks(); + } + + protected override string AdFormatName => "mrec"; + + protected override void SubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Mrec.OnAdLoadSuccess += CloudXOnLoadSuccess; + CloudXAdsCallbacks.Mrec.OnAdLoadFailed += CloudXOnLoadFailed; + CloudXAdsCallbacks.Mrec.OnAdClicked += CloudXOnClicked; + CloudXAdsCallbacks.Mrec.OnAdRevenuePaid += CloudXOnRevenuePaid; + } + + protected override void UnsubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Mrec.OnAdLoadSuccess -= CloudXOnLoadSuccess; + CloudXAdsCallbacks.Mrec.OnAdLoadFailed -= CloudXOnLoadFailed; + CloudXAdsCallbacks.Mrec.OnAdClicked -= CloudXOnClicked; + CloudXAdsCallbacks.Mrec.OnAdRevenuePaid -= CloudXOnRevenuePaid; + } + + protected override void CloudXCreateAndLoad() + { + CloudXSdk.DestroyMrec(CloudXAdUnitId); + + /* + * Required, not optional: CloudX MREC auto-refresh is opt-out, so + * without this the first ShowMrec would start a background reload that + * races the arbiter. It goes before CreateMrec: the native layer + * registers the ad unit as refresh-disabled even with no view yet, then + * creates the view with refresh already off, so no timer ever runs. + * (Destroy clears that registration, hence this order.) It also unlocks + * the explicit LoadMrec used for later rounds. + */ + CloudXSdk.StopMrecAutoRefresh(CloudXAdUnitId); + + /* + * Placement and custom data must be set before CreateMrec so they are + * on the first request. CreateMrec also issues the first load. + */ + CloudXSdk.SetMRecPlacement(CloudXAdUnitId, "arbiter_screen"); + CloudXSdk.SetMRecCustomData(CloudXAdUnitId, "arbiter_mrec_data"); + CloudXSdk.CreateMrec(CloudXAdUnitId, new CloudXAdViewConfiguration(CloudXPosition)); + } + + protected override void CloudXLoad() => CloudXSdk.LoadMrec(CloudXAdUnitId); + protected override void CloudXShow() => CloudXSdk.ShowMrec(CloudXAdUnitId); + protected override void CloudXHide() => CloudXSdk.HideMrec(CloudXAdUnitId); + protected override void DestroyCloudXAd() => CloudXSdk.DestroyMrec(CloudXAdUnitId); + + protected override void AdMobCreateHidden() + { + DestroyAdMobAd(); + + /* + * A BannerView loads once per LoadAd; there is no refresh API to turn off + * here. Its refresh is the ad unit's Automatic refresh setting in the + * AdMob console, which MUST be Disabled for this unit. Google Mobile Ads + * raises its callbacks off the Unity main thread; ExecuteInUpdate moves + * them back on. + */ + _adMobMrec = new BannerView(AdMobAdUnitId, AdSize.MediumRectangle, AdPosition.Bottom); + + _adMobMrec.OnBannerAdLoaded += () => MobileAdsEventExecutor.ExecuteInUpdate(OnAdMobLoaded); + + _adMobMrec.OnBannerAdLoadFailed += error => MobileAdsEventExecutor.ExecuteInUpdate(() => + OnAdMobLoadFailed(error.GetMessage())); + + _adMobMrec.OnAdClicked += () => MobileAdsEventExecutor.ExecuteInUpdate(OnAdMobClicked); + _adMobMrec.OnAdImpressionRecorded += () => MobileAdsEventExecutor.ExecuteInUpdate(OnAdMobImpression); + + /* Required: this is how CloudX learns what the AdMob bid was worth. */ + _adMobMrec.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() => OnAdMobPaid(adValue)); + + /* Created hidden: a load must never render a view the arbiter did not pick. */ + _adMobMrec.Hide(); + } + + protected override void AdMobLoad() => _adMobMrec.LoadAd(new AdRequest()); + protected override void AdMobShow() => _adMobMrec.Show(); + protected override void AdMobHide() => _adMobMrec?.Hide(); + protected override ResponseInfo AdMobResponseInfo() => _adMobMrec?.GetResponseInfo(); + + protected override void DestroyAdMobAd() + { + _adMobMrec?.Destroy(); + _adMobMrec = null; + } +} diff --git a/Assets/Scripts/Arbiter/ArbiterMrecController.cs.meta b/Assets/Scripts/Arbiter/ArbiterMrecController.cs.meta new file mode 100644 index 0000000..d93d1e1 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterMrecController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 59d4160cecd449cba714ca1b7f9efdda \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterRewardedController.cs b/Assets/Scripts/Arbiter/ArbiterRewardedController.cs new file mode 100644 index 0000000..363c760 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterRewardedController.cs @@ -0,0 +1,132 @@ +using System; +using CloudX; +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; + +/* + * Arbiter/TPA rewarded. Same as ArbiterInterstitialController plus the reward + * callback of both SDKs surfaced through RewardEarned. + */ +public sealed class ArbiterRewardedController : ArbiterFullscreenController +{ + private const string AdFormatName = "rewarded"; + + public event Action RewardEarned; + + private RewardedAd _adMobRewarded; + + public ArbiterRewardedController( + string cloudXAdUnitId, + string adMobAdUnitId, + bool cloudXAvailable) + : base(cloudXAdUnitId, adMobAdUnitId, cloudXAvailable) + { + SubscribeCloudXCallbacks(); + } + + protected override void SubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Rewarded.OnAdLoadSuccess += CloudXOnLoadSuccess; + CloudXAdsCallbacks.Rewarded.OnAdLoadFailed += CloudXOnLoadFailed; + CloudXAdsCallbacks.Rewarded.OnAdShowSuccess += CloudXOnShowSuccess; + CloudXAdsCallbacks.Rewarded.OnAdShowFailed += CloudXOnShowFailed; + CloudXAdsCallbacks.Rewarded.OnAdHidden += CloudXOnHidden; + CloudXAdsCallbacks.Rewarded.OnAdClicked += CloudXOnClicked; + CloudXAdsCallbacks.Rewarded.OnAdRewarded += CloudXOnRewarded; + } + + protected override void UnsubscribeCloudXCallbacks() + { + CloudXAdsCallbacks.Rewarded.OnAdLoadSuccess -= CloudXOnLoadSuccess; + CloudXAdsCallbacks.Rewarded.OnAdLoadFailed -= CloudXOnLoadFailed; + CloudXAdsCallbacks.Rewarded.OnAdShowSuccess -= CloudXOnShowSuccess; + CloudXAdsCallbacks.Rewarded.OnAdShowFailed -= CloudXOnShowFailed; + CloudXAdsCallbacks.Rewarded.OnAdHidden -= CloudXOnHidden; + CloudXAdsCallbacks.Rewarded.OnAdClicked -= CloudXOnClicked; + CloudXAdsCallbacks.Rewarded.OnAdRewarded -= CloudXOnRewarded; + } + + protected override bool CloudXIsReady() => CloudXSdk.IsRewardedReady(CloudXAdUnitId); + protected override void CloudXLoad() => CloudXSdk.LoadRewarded(CloudXAdUnitId); + protected override void CloudXShow() => CloudXSdk.ShowRewarded(CloudXAdUnitId); + protected override void DestroyCloudXAd() => CloudXSdk.DestroyRewarded(CloudXAdUnitId); + + protected override bool AdMobCanShow() => _adMobRewarded != null && _adMobRewarded.CanShowAd(); + protected override string AdMobAdSourceName() => AdSourceName(_adMobRewarded?.GetResponseInfo()); + + protected override void AdMobLoad() + { + /* + * Google Mobile Ads raises its callbacks off the Unity main thread. + * ExecuteInUpdate moves the whole body onto it, so controller state and + * the events subscribers use for UI both stay on one thread, like the + * CloudX callbacks. + */ + RewardedAd.Load( + AdMobAdUnitId, + new AdRequest(), + (ad, error) => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + if (IsDisposed) + { + ad?.Destroy(); + return; + } + + if (error != null || ad == null) + { + OnAdMobLoadFailed(error?.GetMessage() ?? "AdMob returned no ad"); + return; + } + + _adMobRewarded = ad; + RegisterAdMobEvents(ad); + OnAdMobLoaded(); + })); + } + + protected override void AdMobShow() + { + _adMobRewarded.Show(reward => MobileAdsEventExecutor.ExecuteInUpdate(() => + RewardEarned?.Invoke(CloudXArbiterPlatform.AdMob, $"{reward.Amount} {reward.Type}"))); + } + + protected override void DestroyAdMobAd() + { + _adMobRewarded?.Destroy(); + _adMobRewarded = null; + } + + private void CloudXOnRewarded(CloudXAd ad, CloudXReward reward) + { + if (ad.AdUnitId == CloudXAdUnitId) + { + RewardEarned?.Invoke(CloudXArbiterPlatform.CloudX, $"{reward.Amount} {reward.Label}"); + } + } + + private void RegisterAdMobEvents(RewardedAd ad) + { + ad.OnAdFullScreenContentOpened += () => MobileAdsEventExecutor.ExecuteInUpdate(() => + RaiseAdShown(CloudXArbiterPlatform.AdMob)); + + ad.OnAdFullScreenContentClosed += () => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + DestroyAdMobAd(); + RaiseAdClosed(CloudXArbiterPlatform.AdMob); + }); + + ad.OnAdFullScreenContentFailed += error => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + DestroyAdMobAd(); + RaiseAdShowFailed(CloudXArbiterPlatform.AdMob, error.GetMessage()); + }); + + ad.OnAdClicked += () => MobileAdsEventExecutor.ExecuteInUpdate(() => + RaiseAdClicked(CloudXArbiterPlatform.AdMob)); + + /* Required: this is how CloudX learns what the AdMob bid was worth. */ + ad.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() => + ReportAdMobPaidEvent(adValue, AdFormatName, AdSourceName(ad.GetResponseInfo()))); + } +} diff --git a/Assets/Scripts/Arbiter/ArbiterRewardedController.cs.meta b/Assets/Scripts/Arbiter/ArbiterRewardedController.cs.meta new file mode 100644 index 0000000..da2e68e --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterRewardedController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 46097a0bb74d4ffcb7686d1ea4473df1 \ No newline at end of file diff --git a/Assets/Scripts/Arbiter/ArbiterScreen.cs b/Assets/Scripts/Arbiter/ArbiterScreen.cs new file mode 100644 index 0000000..69213f0 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterScreen.cs @@ -0,0 +1,487 @@ +using System.Collections; +using CloudX; +using CloudX.Demo; +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; +using UnityEngine; + +/* + * Arbiter/TPA demo entry point and integration template: CloudX and AdMob load + * in parallel for every placement and Trusted Arbiter (CloudXSdk.Arbiter) picks + * which one is shown. Interstitial and rewarded prepare the winner ahead of the + * placement; banner and MREC arbitrate and then render on a refresh cycle with + * auto-refresh off. The flow lives entirely in this folder (screen + a shared + * controller base + one controller per format) so it can be copied into a + * publisher app as-is; AdScreenUi is demo-only layout and is kept out on purpose. + * + * The status lines make the arbitration visible: which sides loaded, what the + * arbiter returned and over how many bids, and which platform is showing. + */ +[RequireComponent(typeof(AdScreenUi))] +public class ArbiterScreen : MonoBehaviour +{ + private const string TAG = "CloudXUnityDemo"; + private const float InitializationUiTimeoutSeconds = 15f; + + /* + * Retry policy after a load or show failure while no winner is prepared or + * shown: 2s, 4s, 8s ... capped, and reset once a load succeeds. Once a + * winner exists, the controllers re-request the missing network themselves + * on the next cycle, so a sustained no-fill never turns into a tight loop. + */ + private const float RetryBaseDelaySeconds = 2f; + private const float RetryMaxDelaySeconds = 60f; + + private AdScreenUi _ui; + private ArbiterInterstitialController _interstitial; + private ArbiterRewardedController _rewarded; + private ArbiterBannerController _banner; + private ArbiterMrecController _mrec; + private bool _cloudXInitAnswered; + private int _interstitialRetries; + private int _rewardedRetries; + private int _bannerRetries; + private int _mrecRetries; + private string _cloudXStatus = "CloudX: Initializing"; + private string _adMobStatus = "AdMob: Initializing"; + + private static void Log(string message) => Debug.Log($"[{TAG}][Arbiter] {message}"); + + void Awake() + { + _ui = GetComponent(); + } + + IEnumerator Start() + { + Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); + + _ui.Bind(new AdScreenUi.Actions + { + ShowBanner = ToggleBanner, + ToggleMrec = ToggleMrec, + ShowInterstitial = ShowInterstitial, + ShowRewarded = ShowRewarded, + /* The banner stays at the top in both orientations, so nothing to reflow. */ + OnOrientationChanged = _ => { }, + }); + _ui.SetActionsInteractable(false); +#if UNITY_IOS && !UNITY_EDITOR + PublishInitializationStatus("Requesting tracking permission"); +#endif + /* + * Resolve ATT before either SDK initializes. CloudX never prompts and + * treats an undetermined status as opted out, which suppresses fill on + * physical devices. + */ + yield return DemoAppTrackingiOS.EnsureRequested(); + + if (!DemoAppTrackingiOS.IsUsable(DemoAppTrackingiOS.Status)) + { + Log($"Tracking not authorized ({DemoAppTrackingiOS.Status}), leaving the UI disabled"); + _ui.SetInitializationStatus("Status: Tracking not authorized - ads cannot load"); + yield break; + } + + InitializeAdMob(); + InitializeCloudX(); + StartCoroutine(ReleaseActionsIfInitStalls()); + } + + /* The inline controllers' refresh clock. */ + void Update() + { + _banner?.Tick(Time.unscaledDeltaTime); + _mrec?.Tick(Time.unscaledDeltaTime); + } + + void OnDestroy() + { + CloudXInitializationCallbacks.OnSdkInitializedEvent -= OnCloudXInitialized; + CloudXInitializationCallbacks.OnSdkInitializationFailedEvent -= OnCloudXInitializationFailed; + + _interstitial?.Dispose(); + _interstitial = null; + _rewarded?.Dispose(); + _rewarded = null; + _banner?.Dispose(); + _banner = null; + _mrec?.Dispose(); + _mrec = null; + } + + /* + * Initialization + */ + + private void InitializeAdMob() + { + /* + * Google Mobile Ads queues loads issued before init completes, so the + * parallel loads can start as soon as CloudX is ready. Its callbacks + * arrive off the Unity main thread; anything that touches UI goes + * through ExecuteInUpdate, and the controllers do the same. + */ + MobileAds.Initialize(_ => MobileAdsEventExecutor.ExecuteInUpdate(() => + { + _adMobStatus = "AdMob: Ready"; + PublishInitializationStatus(); + })); + } + + private void InitializeCloudX() + { + PublishInitializationStatus(); + + if (CloudXSdk.IsInitialized()) + { + /* Already initialized by another screen in this session. */ + OnCloudXInitialized(new CloudXSdkConfiguration()); + return; + } + + CloudXSdk.SetMinLogLevel(CloudXLogLevel.Verbose); + CloudXSdk.SetHasUserConsent(true); + CloudXSdk.SetDoNotSell(false); + + CloudXInitializationCallbacks.OnSdkInitializedEvent += OnCloudXInitialized; + CloudXInitializationCallbacks.OnSdkInitializationFailedEvent += OnCloudXInitializationFailed; + + CloudXSdk.Initialize(CloudXInitializationConfiguration.Create(DemoConfig.AppKey).Build()); + } + + /* + * The arbiter is called only after initialization completed (docs). The + * controllers are created here, so their first Arbiter call cannot precede + * OnSdkInitialized. + */ + private void OnCloudXInitialized(CloudXSdkConfiguration _) + { + _cloudXInitAnswered = true; + + if (_interstitial != null) + { + /* + * The watchdog already gave up on CloudX and built AdMob-only + * controllers. Swapping them now would destroy an AdMob ad that + * may be on screen, so this session stays as it is and says so. + */ + Log("CloudX initialized after the watchdog; this session stays AdMob-only"); + _cloudXStatus = "CloudX: Initialized late - AdMob only this session"; + PublishInitializationStatus(); + return; + } + + Log("CloudX initialized"); + _cloudXStatus = "CloudX: Initialized"; + PublishInitializationStatus(); + CreateControllers(cloudXAvailable: true); + } + + private void OnCloudXInitializationFailed(CloudXError error) + { + Log($"CloudX initialization failed: {error}"); + _cloudXInitAnswered = true; + _cloudXStatus = $"CloudX: Failed ({error.ErrorCodeName})"; + PublishInitializationStatus(); + + /* + * The demo keeps working without CloudX: the controllers skip the CloudX + * leg, AdMob is the only candidate and wins every round locally. + */ + CreateControllers(cloudXAvailable: false); + } + + /* + * Releases the UI if CloudX Initialize never reports back. Ads may still + * load through AdMob alone in that case. + */ + private IEnumerator ReleaseActionsIfInitStalls() + { + yield return new WaitForSecondsRealtime(InitializationUiTimeoutSeconds); + + if (_cloudXInitAnswered) + { + yield break; + } + + Log($"No CloudX initialization result within {InitializationUiTimeoutSeconds}s, continuing with AdMob only"); + _cloudXStatus = "CloudX: No response"; + PublishInitializationStatus(); + CreateControllers(cloudXAvailable: false); + } + + private void CreateControllers(bool cloudXAvailable) + { + if (_interstitial != null) + { + return; + } + + _interstitial = new ArbiterInterstitialController( + ArbiterConfig.CloudXAdUnitOrInvalid(DemoConfig.InterstitialAdUnitId), + DemoConfig.AdMobInterstitialAdUnitId, + cloudXAvailable); + _interstitial.AdLoaded += platform => + { + _interstitialRetries = 0; + SetInterstitialStatus($"{platform} loaded"); + }; + _interstitial.AdLoadFailed += (platform, message) => + { + /* With a candidate, a load or a round pending, the controller gets there itself. */ + if (!_interstitial.NeedsRetry) + { + SetInterstitialStatus($"Load failed ({platform}): {message}"); + return; + } + + var delay = NextRetryDelay(ref _interstitialRetries); + SetInterstitialStatus($"Load failed ({platform}): {message}\nRetrying in {delay:0}s..."); + Invoke(nameof(LoadInterstitial), delay); + }; + _interstitial.ArbiterCompleted += (result, bidCount) => + SetInterstitialStatus(ArbiterSummary(result, bidCount)); + _interstitial.AdShown += platform => SetInterstitialStatus($"Showing ({platform})"); + _interstitial.AdShowFailed += (platform, message) => + { + var delay = NextRetryDelay(ref _interstitialRetries); + SetInterstitialStatus($"Show failed ({platform}): {message}\nRetrying in {delay:0}s..."); + Invoke(nameof(LoadInterstitial), delay); + }; + _interstitial.AdClosed += platform => + { + SetInterstitialStatus($"Closed ({platform})"); + /* Start the next cycle right away: reload what is missing, re-arbitrate. */ + LoadInterstitial(); + }; + _interstitial.AdClicked += platform => Log($"Interstitial clicked ({platform})"); + + _rewarded = new ArbiterRewardedController( + ArbiterConfig.CloudXAdUnitOrInvalid(DemoConfig.RewardedAdUnitId), + DemoConfig.AdMobRewardedAdUnitId, + cloudXAvailable); + _rewarded.AdLoaded += platform => + { + _rewardedRetries = 0; + SetRewardedStatus($"{platform} loaded"); + }; + _rewarded.AdLoadFailed += (platform, message) => + { + if (!_rewarded.NeedsRetry) + { + SetRewardedStatus($"Load failed ({platform}): {message}"); + return; + } + + var delay = NextRetryDelay(ref _rewardedRetries); + SetRewardedStatus($"Load failed ({platform}): {message}\nRetrying in {delay:0}s..."); + Invoke(nameof(LoadRewarded), delay); + }; + _rewarded.ArbiterCompleted += (result, bidCount) => + SetRewardedStatus(ArbiterSummary(result, bidCount)); + _rewarded.AdShown += platform => SetRewardedStatus($"Showing ({platform})"); + _rewarded.AdShowFailed += (platform, message) => + { + var delay = NextRetryDelay(ref _rewardedRetries); + SetRewardedStatus($"Show failed ({platform}): {message}\nRetrying in {delay:0}s..."); + Invoke(nameof(LoadRewarded), delay); + }; + _rewarded.AdClosed += platform => + { + SetRewardedStatus($"Closed ({platform})"); + LoadRewarded(); + }; + _rewarded.AdClicked += platform => Log($"Rewarded clicked ({platform})"); + _rewarded.RewardEarned += (platform, reward) => + { + Log($"Reward earned ({platform}): {reward}"); + SetRewardedStatus($"Reward: {reward} ({platform})"); + }; + + _banner = new ArbiterBannerController( + ArbiterConfig.CloudXAdUnitOrInvalid(DemoConfig.BannerAdUnitId), + DemoConfig.AdMobBannerAdUnitId, + cloudXAvailable, + ArbiterConfig.InlineRefreshIntervalSeconds); + _banner.AdLoaded += platform => + { + _bannerRetries = 0; + Log($"Banner: {platform} loaded"); + }; + _banner.AdLoadFailed += (platform, message) => + { + /* With a candidate, a load, a round or a view pending, the cycle re-requests fills itself. */ + if (!_banner.NeedsRetry) + { + Log($"Banner: load failed ({platform}): {message}"); + return; + } + + var delay = NextRetryDelay(ref _bannerRetries); + Log($"Banner: load failed ({platform}): {message}; retrying in {delay:0}s"); + Invoke(nameof(LoadBanner), delay); + }; + _banner.ArbiterCompleted += (result, bidCount) => Log($"Banner: {ArbiterSummary(result, bidCount)}"); + _banner.WinnerShown += platform => _ui.SetBannerButtonLabel( + platform == CloudXArbiterPlatform.None ? "Banner: no winner" : $"Hide Banner ({platform})"); + _banner.AdClicked += platform => Log($"Banner clicked ({platform})"); + + _mrec = new ArbiterMrecController( + ArbiterConfig.CloudXAdUnitOrInvalid(DemoConfig.MrecAdUnitId), + DemoConfig.AdMobMrecAdUnitId, + cloudXAvailable, + ArbiterConfig.InlineRefreshIntervalSeconds); + _mrec.AdLoaded += platform => + { + _mrecRetries = 0; + Log($"MREC: {platform} loaded"); + }; + _mrec.AdLoadFailed += (platform, message) => + { + if (!_mrec.NeedsRetry) + { + Log($"MREC: load failed ({platform}): {message}"); + return; + } + + var delay = NextRetryDelay(ref _mrecRetries); + Log($"MREC: load failed ({platform}): {message}; retrying in {delay:0}s"); + Invoke(nameof(LoadMrec), delay); + }; + _mrec.ArbiterCompleted += (result, bidCount) => Log($"MREC: {ArbiterSummary(result, bidCount)}"); + _mrec.WinnerShown += platform => _ui.SetMrecButtonLabel( + platform == CloudXArbiterPlatform.None ? "MREC: no winner" : $"Hide MREC ({platform})"); + _mrec.AdClicked += platform => Log($"MREC clicked ({platform})"); + + /* Parallel loads for every format; each arbitrates once its candidates settle. */ + LoadInterstitial(); + LoadRewarded(); + LoadBanner(); + LoadMrec(); + _ui.SetActionsInteractable(true); + } + + /* + * Button handlers + */ + + private void ShowInterstitial() + { + var winner = _interstitial.PreparedWinner; + + if (_interstitial.Show()) + { + Log($"Showing the interstitial ({winner})"); + return; + } + + /* + * No winner is prepared (or its ad is gone); in a real app the game flow + * would simply continue here. The demo reloads and says so. + */ + SetInterstitialStatus("No winner prepared; reloading"); + LoadInterstitial(); + } + + private void ShowRewarded() + { + var winner = _rewarded.PreparedWinner; + + if (_rewarded.Show()) + { + Log($"Showing the rewarded ad ({winner})"); + return; + } + + SetRewardedStatus("No winner prepared; reloading"); + LoadRewarded(); + } + + private void ToggleBanner() + { + if (_banner.IsShown) + { + _banner.Hide(); + _ui.SetBannerButtonLabel("Show Banner"); + return; + } + + /* WinnerShown updates the label once the round has rendered. */ + _ui.SetBannerButtonLabel("Banner: arbitrating..."); + _banner.Show(); + } + + private void ToggleMrec() + { + if (_mrec.IsShown) + { + _mrec.Hide(); + _ui.SetMrecButtonLabel("Show MREC"); + return; + } + + _ui.SetMrecButtonLabel("MREC: arbitrating..."); + _mrec.Show(); + } + + private static float NextRetryDelay(ref int retries) + { + var delay = Mathf.Min(RetryBaseDelaySeconds * Mathf.Pow(2f, retries), RetryMaxDelaySeconds); + retries++; + return delay; + } + + private static string ArbiterSummary(CloudXArbiterResult result, int bidCount) + { + var bids = bidCount == 1 ? "1 bid" : $"{bidCount} bids"; + return result.Platform == CloudXArbiterPlatform.None + ? $"Arbiter: no winner ({bids})" + : $"Arbiter: {result.Platform} ({bids})"; + } + + /* + * Named methods so terminal failures can retry via Invoke(nameof(...)). + */ + + private void LoadInterstitial() + { + _interstitial?.Load(); + } + + private void LoadRewarded() + { + _rewarded?.Load(); + } + + private void LoadBanner() + { + _banner?.Load(); + } + + private void LoadMrec() + { + _mrec?.Load(); + } + + /* + * Status plumbing + */ + + private void PublishInitializationStatus(string overrideText = null) + { + _ui.SetInitializationStatus(overrideText ?? $"{_cloudXStatus} | {_adMobStatus}"); + } + + private void SetInterstitialStatus(string text) + { + Log($"Interstitial: {text.Replace('\n', ' ')}"); + _ui.SetInterstitialStatus($"Inter: {text}"); + } + + private void SetRewardedStatus(string text) + { + Log($"Rewarded: {text.Replace('\n', ' ')}"); + _ui.SetRewardedStatus($"Rewarded: {text}"); + } +} diff --git a/Assets/Scripts/Arbiter/ArbiterScreen.cs.meta b/Assets/Scripts/Arbiter/ArbiterScreen.cs.meta new file mode 100644 index 0000000..97c0445 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterScreen.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: acafde15ea31470e81e2be191a938627 \ No newline at end of file diff --git a/Assets/Scripts/DemoConfig.cs b/Assets/Scripts/DemoConfig.cs index d80d42d..8840228 100644 --- a/Assets/Scripts/DemoConfig.cs +++ b/Assets/Scripts/DemoConfig.cs @@ -2,6 +2,12 @@ * Demo dashboard IDs so this sample runs without a CloudX account. * In your game, replace these with the app key and ad unit IDs from your * CloudX dashboard. Use one app key per process. + * + * The AdMob ids are Google's official TEST ad units, shared by the First Look + * and Arbiter/TPA demos. Replace them with your own AdMob ad units in a real + * integration, and set Automatic refresh to Disabled on the banner and MREC + * units in the AdMob console: the Unity plugin cannot control it, and a + * refreshing AdMob banner would replace the ad the demo flow decided to show. */ public static class DemoConfig { @@ -12,6 +18,10 @@ public static class DemoConfig public const string InterstitialAdUnitId = "9SizbPM3Dctz71WM2BKpi"; public const string AppOpenAdUnitId = "3evNMg9P4E1pgRPyAYk9O"; public const string RewardedAdUnitId = "7T2i4VWjsG2I4PM5vircU"; + + public const string AdMobInterstitialAdUnitId = "ca-app-pub-3940256099942544/4411468910"; + public const string AdMobRewardedAdUnitId = "ca-app-pub-3940256099942544/1712485313"; + public const string AdMobBannerAdUnitId = "ca-app-pub-3940256099942544/2934735716"; #else public const string AppKey = "0qE4q2MoJzoOkFQQKAtkt"; public const string BannerAdUnitId = "guDml31r4Ys6O6HroPJia"; @@ -19,5 +29,15 @@ public static class DemoConfig public const string InterstitialAdUnitId = "PwIOPhOD0KMCB_aqz8c89"; public const string AppOpenAdUnitId = "BI0Whd5_o8ZIxkdHBS7X_"; public const string RewardedAdUnitId = "LZrqb2oz47LMG_TaaVtaR"; + + public const string AdMobInterstitialAdUnitId = "ca-app-pub-3940256099942544/1033173712"; + public const string AdMobRewardedAdUnitId = "ca-app-pub-3940256099942544/5224354917"; + public const string AdMobBannerAdUnitId = "ca-app-pub-3940256099942544/6300978111"; #endif + + /* + * Google has no dedicated MREC test unit; its banner test unit returns a + * test ad at whatever AdSize is requested, so it serves the 300x250 MREC too. + */ + public const string AdMobMrecAdUnitId = AdMobBannerAdUnitId; } diff --git a/Assets/Scripts/FirstLook/FirstLookConfig.cs b/Assets/Scripts/FirstLook/FirstLookConfig.cs index 178e074..09027c0 100644 --- a/Assets/Scripts/FirstLook/FirstLookConfig.cs +++ b/Assets/Scripts/FirstLook/FirstLookConfig.cs @@ -1,30 +1,9 @@ /* - * Fallback-side configuration for the First Look demo. The CloudX ad unit ids - * come from DemoConfig; these are Google's official AdMob TEST ad unit ids. - * Replace them with your own AdMob ad units in a real integration. - * - * When you do, set Automatic refresh to Disabled on the banner and MREC units - * in the AdMob console. The Unity plugin cannot control it, and a refreshing - * AdMob banner would replace the ad that won the First Look pass. + * First Look demo switches. The CloudX and AdMob ad unit ids both come from + * DemoConfig (the AdMob ones are Google's official test units). */ public static class FirstLookConfig { -#if UNITY_IOS - public const string AdMobInterstitialAdUnitId = "ca-app-pub-3940256099942544/4411468910"; - public const string AdMobRewardedAdUnitId = "ca-app-pub-3940256099942544/1712485313"; - public const string AdMobBannerAdUnitId = "ca-app-pub-3940256099942544/2934735716"; -#else - public const string AdMobInterstitialAdUnitId = "ca-app-pub-3940256099942544/1033173712"; - public const string AdMobRewardedAdUnitId = "ca-app-pub-3940256099942544/5224354917"; - public const string AdMobBannerAdUnitId = "ca-app-pub-3940256099942544/6300978111"; -#endif - - /* - * Google has no dedicated MREC test unit; its banner test unit returns a - * test ad at whatever AdSize is requested, so it serves the 300x250 MREC too. - */ - public const string AdMobMrecAdUnitId = AdMobBannerAdUnitId; - /* * Flip to true to exercise the AdMob fallback path: CloudX is asked to fill * an unknown ad unit, fails to load, and the controllers fall back to AdMob. diff --git a/Assets/Scripts/FirstLook/FirstLookScreen.cs b/Assets/Scripts/FirstLook/FirstLookScreen.cs index 6a69eca..e41a081 100644 --- a/Assets/Scripts/FirstLook/FirstLookScreen.cs +++ b/Assets/Scripts/FirstLook/FirstLookScreen.cs @@ -209,7 +209,7 @@ private void CreateControllers(bool cloudXAvailable) _interstitial = new FirstLookInterstitialController( FirstLookConfig.CloudXAdUnitOrInvalid(DemoConfig.InterstitialAdUnitId), - FirstLookConfig.AdMobInterstitialAdUnitId, + DemoConfig.AdMobInterstitialAdUnitId, cloudXAvailable); _interstitial.AdLoaded += source => { @@ -239,7 +239,7 @@ private void CreateControllers(bool cloudXAvailable) _rewarded = new FirstLookRewardedController( FirstLookConfig.CloudXAdUnitOrInvalid(DemoConfig.RewardedAdUnitId), - FirstLookConfig.AdMobRewardedAdUnitId, + DemoConfig.AdMobRewardedAdUnitId, cloudXAvailable); _rewarded.AdLoaded += source => { @@ -273,7 +273,7 @@ private void CreateControllers(bool cloudXAvailable) _banner = new FirstLookBannerController( FirstLookConfig.CloudXAdUnitOrInvalid(DemoConfig.BannerAdUnitId), - FirstLookConfig.AdMobBannerAdUnitId, + DemoConfig.AdMobBannerAdUnitId, cloudXAvailable); _banner.AdLoaded += source => { @@ -295,7 +295,7 @@ private void CreateControllers(bool cloudXAvailable) _mrec = new FirstLookMrecController( FirstLookConfig.CloudXAdUnitOrInvalid(DemoConfig.MrecAdUnitId), - FirstLookConfig.AdMobMrecAdUnitId, + DemoConfig.AdMobMrecAdUnitId, cloudXAvailable); _mrec.AdLoaded += source => { diff --git a/Assets/Scripts/OptionsScreen.cs b/Assets/Scripts/OptionsScreen.cs index c4afbb2..af101c7 100644 --- a/Assets/Scripts/OptionsScreen.cs +++ b/Assets/Scripts/OptionsScreen.cs @@ -5,14 +5,16 @@ /* * Demo-only launch screen. Picks which demo flow to enter. General routes to * GeneralScene, where the CloudX integration sample lives; First Look routes to - * FirstLookScene, the CloudX-first-with-AdMob-fallback template. Arbiter/TPA is - * not implemented yet, so its button stays hidden. + * FirstLookScene, the CloudX-first-with-AdMob-fallback template; Arbiter/TPA + * routes to ArbiterScene, where CloudX and AdMob load in parallel and Trusted + * Arbiter picks the winner. */ public class OptionsScreen : MonoBehaviour { private const string TAG = "CloudXUnityDemo"; private const string GeneralSceneName = "GeneralScene"; private const string FirstLookSceneName = "FirstLookScene"; + private const string ArbiterSceneName = "ArbiterScene"; public Button generalButton; public Button firstLookButton; @@ -22,9 +24,7 @@ private void Start() { generalButton.onClick.AddListener(OpenGeneral); firstLookButton.onClick.AddListener(OpenFirstLook); - - /* Nothing to route to yet; hide the button rather than leave it dead. */ - arbiterTpaButton.gameObject.SetActive(false); + arbiterTpaButton.onClick.AddListener(OpenArbiter); } private void OpenGeneral() @@ -38,4 +38,10 @@ private void OpenFirstLook() Debug.Log($"[{TAG}] Opening the First Look demo"); SceneManager.LoadScene(FirstLookSceneName); } + + private void OpenArbiter() + { + Debug.Log($"[{TAG}] Opening the Arbiter/TPA demo"); + SceneManager.LoadScene(ArbiterSceneName); + } } diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset index df65263..50b9d20 100644 --- a/ProjectSettings/EditorBuildSettings.asset +++ b/ProjectSettings/EditorBuildSettings.asset @@ -14,5 +14,8 @@ EditorBuildSettings: - enabled: 1 path: Assets/Scenes/FirstLookScene.unity guid: 33c712e03aba7427695d0bd645dcd1f8 + - enabled: 1 + path: Assets/Scenes/ArbiterScene.unity + guid: 0365ea9803b44c7da3c9fc6ebe403f63 m_configObjects: {} m_UseUCBPForAssetBundles: 0 diff --git a/README.md b/README.md index dea491e..0df4ce8 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ Our complete CloudX Unity SDK integration guide is available on our docs site, [ ## Demo app This repository is also a runnable Unity demo project. It shows a working CloudX integration for -banner, MREC (the 300x250 medium rectangle), interstitial and rewarded ads, plus a First Look flow -that falls back to AdMob. +banner, MREC (the 300x250 medium rectangle), interstitial and rewarded ads, plus two ways of running +CloudX next to AdMob: a First Look flow that falls back to AdMob, and an Arbiter/TPA flow where both +load and Trusted Arbiter picks the winner. Requirements: @@ -27,8 +28,9 @@ choose, which is deliberate: the iOS tracking prompt and `CloudXSdk.Initialize` you picked, not to app start. ``` -OptionsScene ── General ──> GeneralScene (the full CloudX surface) - └─ First Look ──> FirstLookScene (CloudX first, AdMob fallback) +OptionsScene ── General ──> GeneralScene (the full CloudX surface) + ├─ First Look ──> FirstLookScene (CloudX first, AdMob fallback) + └─ Arbiter/TPA ──> ArbiterScene (CloudX and AdMob in parallel, arbiter picks) ``` There is no back navigation; relaunch the app to pick the other flow. @@ -38,19 +40,18 @@ There is no back navigation; relaunch the app to pick the other flow. | `Assets/Scenes/OptionsScene.unity` | `Assets/Scripts/OptionsScreen.cs` | Picking a flow. No SDK calls. | | `Assets/Scenes/GeneralScene.unity` | `Assets/Scripts/GeneralScreen.cs` | Every ad format, straight CloudX. | | `Assets/Scenes/FirstLookScene.unity` | `Assets/Scripts/FirstLook/` | CloudX first, AdMob as the fallback. | +| `Assets/Scenes/ArbiterScene.unity` | `Assets/Scripts/Arbiter/` | CloudX and AdMob in parallel, Trusted Arbiter picks. | -`Assets/Scripts/AdScreenUi.cs` is the layout shared by the two ad screens. It is demo-only: it wires +`Assets/Scripts/AdScreenUi.cs` is the layout shared by the three ad screens. It is demo-only: it wires buttons and reflows on rotate, and contains no SDK calls. Ignore it when reading the integration. ### Options screen -Options screen with General and First Look buttons +Options screen with General, First Look and Arbiter/TPA buttons `OptionsScene` is index 0 in the build settings, so it is what launches. Each button calls `SceneManager.LoadScene` with a scene name, which only resolves for scenes listed in -File > Build Settings, so all three are listed there. - -A third flow, Arbiter/TPA, is not implemented yet and its button stays hidden. +File > Build Settings, so all four are listed there. ### General screen @@ -132,7 +133,7 @@ screen, so the folder can be copied out whole: | `FirstLookRewardedController.cs` | The rewarded SDK calls, plus the reward callback. | | `FirstLookBannerController.cs` | The banner SDK calls. | | `FirstLookMrecController.cs` | The MREC SDK calls. | -| `FirstLookConfig.cs` | AdMob ad unit ids, and the fallback test switch below. | +| `FirstLookConfig.cs` | The fallback test switch below. | | `FirstLookScreen.cs` | Initializes both SDKs, wires the controllers to the buttons. | Each format is a thin subclass over a shared base, so the fallback rule is written once. To integrate @@ -141,7 +142,8 @@ one format, take four files: `FirstLookSource.cs`, `FirstLookAdController.cs`, t banner or MREC) and that format's controller. The bases are small and format-agnostic. To see the fallback path yourself, set `ForceCloudXNoFill = true` in `FirstLookConfig.cs` and rebuild. -It points CloudX at an unknown ad unit, so every CloudX load fails and AdMob serves instead. +It points CloudX at an unknown ad unit, so every CloudX load fails and AdMob serves instead. The AdMob +ad unit ids come from `Assets/Scripts/DemoConfig.cs`, next to the CloudX ones. First Look covers all four formats. Banner and MREC toggle Show/Hide, and the button label names the SDK that filled (e.g. `Hide Banner (CloudX)`). The banner sits at the top in both orientations; the @@ -170,13 +172,88 @@ restarts refresh on focus; First Look deliberately does not.) > The demo's Google test units are configured by Google, not by this project, so treat them only > as a way to see the fallback render; the setting above is about the units you replace them with. +### Arbiter/TPA screen + +Arbiter/TPA screen with the arbiter result in each status line + +Trusted Arbiter (TPA, third-party arbitration) is the other way to run CloudX next to an existing +mediation SDK. Where First Look asks CloudX first and touches AdMob only on a CloudX miss, the +Arbiter flow loads **both** at the same time and lets CloudX's arbiter decide which loaded ad is +shown. The full pattern is documented at +[https://docs.cloudx.io/en/unity/trusted-arbiter](https://docs.cloudx.io/en/unity/trusted-arbiter); +this screen is a working copy of it against AdMob, meant to be lifted into a publisher app. + +The rules the controllers implement: + +- CloudX and AdMob load in parallel. Once both have settled (loaded or failed), the loaded ones + become bids and `CloudXSdk.Arbiter` returns the platform to show. Nothing compares prices or times + the call out locally: the SDK owns both, and always completes. A single bid wins without a service + call; with no arbiter service the SDK falls back to the highest locally comparable price. +- **Interstitial and rewarded prepare the winner ahead of the placement.** The arbiter runs as soon + as the candidates settle and the result is stored; tapping Show shows the stored winner with no + network call, and returns `false` when no winner is prepared, so the game carries on. The cycle + restarts (reload what is missing, re-arbitrate) after the ad closes. +- **Banner and MREC arbitrate, then render.** The winner's view is shown from the arbiter callback; + the loser stays loaded but hidden, because showing it would fire an impression for a bid the + arbiter did not select. Auto-refresh is off on both sides and the controller drives the cycle: every + 25 seconds the shown winner (its fill was consumed by the impression) is reloaded while the loser + keeps its fill, and a new round runs over both as soon as the reload settles. The docs reload the + winner the moment its impression fires; the Unity plugins reload into the view that is on screen, + which replaced the visible creative within a second, so this demo reloads at the interval instead. +- **AdMob bids carry no price.** CloudX prices them from the revenue the app forwards after each AdMob + impression: every AdMob ad's `OnAdPaid` goes into `CloudXSdk.ReportRevenueData`. This is a required + part of the integration, not telemetry; without it CloudX never learns what AdMob pays and its + estimate for the AdMob bid never improves. +- If CloudX initialization fails outright, the controllers skip the CloudX leg; AdMob is the only + candidate and wins every round locally. + +The status lines show the arbitration as it happens: which sides loaded, what the arbiter returned and +over how many bids (`Arbiter: CloudX (2 bids)`), and which platform is showing. Banner and MREC toggle +Show/Hide and their labels name the platform on screen (`Hide Banner (AdMob)`), or read `no winner` +when a round selected nobody. + +Arbiter/TPA screen with the arbitrated banner and MREC on screen + +Everything the flow needs lives in `Assets/Scripts/Arbiter`, and none of it calls into the other +screens, so the folder can be copied out whole: + +| File | Role | +| --- | --- | +| `ArbiterAdController.cs` | Shared base: ids, the arbiter call, the AdMob bid, the paid-event forwarding, dispose. | +| `ArbiterFullscreenController.cs` | Base for interstitial and rewarded: parallel load, prepare the winner, show it at the placement. | +| `ArbiterInlineController.cs` | Base for banner and MREC: parallel load, arbitrate, render the winner, refresh cycle. | +| `ArbiterInterstitialController.cs` | The interstitial SDK calls. | +| `ArbiterRewardedController.cs` | The rewarded SDK calls, plus the reward callback. | +| `ArbiterBannerController.cs` | The banner SDK calls. | +| `ArbiterMrecController.cs` | The MREC SDK calls. | +| `ArbiterConfig.cs` | The refresh interval and the single-bid test switch below. | +| `ArbiterScreen.cs` | Initializes both SDKs, wires the controllers to the buttons, feeds the refresh clock. | + +To integrate one format, take three files: `ArbiterAdController.cs`, the family base +(`ArbiterFullscreenController.cs` or `ArbiterInlineController.cs`) and that format's controller. +The ad unit ids come from `Assets/Scripts/DemoConfig.cs`. + +To see the single-bid path yourself, set `ForceCloudXNoFill = true` in `ArbiterConfig.cs` and rebuild. +It points CloudX at an unknown ad unit, so AdMob is the only bid in every round and the SDK selects it +without a service call. + +> **Two things the code cannot do for you.** +> +> Trusted Arbiter has to be enabled for your app in the CloudX dashboard. Until it is, the SDK still +> answers every `Arbiter` call, but from its local fallback: the highest locally comparable price +> among the bids, in which an AdMob bid with no reported revenue history yet cannot win. +> +> Disable **Automatic refresh** on your AdMob banner and MREC ad units, exactly as for First Look +> (see the callout above). The arbiter cycle owns refresh here; an AdMob unit that refreshes on its +> own swaps the creative behind the arbiter's back. + ### Google Mobile Ads dependency -The First Look flow needs the Google Mobile Ads Unity plugin, which this project pulls in as a +The First Look and Arbiter/TPA flows need the Google Mobile Ads Unity plugin, which this project pulls in as a package (`Packages/manifest.json`) along with the External Dependency Manager it requires. Unity resolves both on open, so no manual import step is needed. -If you copy the First Look folder into your own project, add the same plugin there; the CloudX SDK +If you copy the First Look or Arbiter folder into your own project, add the same plugin there; the CloudX SDK itself does not depend on it. ### Using your own CloudX app @@ -194,8 +271,8 @@ Project Settings > Player > Signing. It ships empty on purpose. Bid requests are authorized per app key and bundle identifier, so both have to match your dashboard app or the SDK gets no fill. -The AdMob ad units in `FirstLookConfig.cs` are Google's official test units and stay valid as they -are; replace them with your own AdMob units when you take this into production, and set +The AdMob ad units in `DemoConfig.cs` are Google's official test units and stay valid as they are; +replace them with your own AdMob units when you take this into production, and set **Automatic refresh** to Disabled on the banner and MREC ones (see the First Look section for why). ### iOS target SDK diff --git a/docs/images/arbiter-inline.png b/docs/images/arbiter-inline.png new file mode 100644 index 0000000000000000000000000000000000000000..309cc72e776143ecf1d211d3a2f92d830fcec36f GIT binary patch literal 63208 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 OfR$f7FIRl^>AwI?RoHa^ literal 10884 zcmeHtXHZk!yKg`g#8*KDQIM{nD4c{ zQ9=90rDF=wypP!5-?J-r{qe%?LZ@+&fXKUO37WR{5BJPJGQXd(3-Vvk6JdX?-TD2& zJ@!j4#XXAFf>9YFHN>rEjVZO5Med3bf@>s;tV5&J)O6V~YgtWuvz0aKiW_gMO(ot0 z95(5I*j0`%bNjBEVtJ#rGeHuDULSbaN;!CEAOE~7#BRa<>SFm9Eso?f+8(8Gl73uz z$Hf9OkI{K=7=sWg=Tbk3^IncdTL^CIe!jw{r{Z{PJpS(>e|7Rd<|lmmaJL@!Gw7AT z@yxRWZ4_>93Vy#;YHQRl_g)`U6b`3+h~VH2+uLs*D7DFp?~@AXZt;zayKq%b=J!XW zOJcc+Ly1R|+H7NUR1UrQ_Pdjz+_*3ABU&xT-21R|$7WkJucgdN;bTSaDm5^$>!jiB zbk`*#;m0HZ|L224JA8Z$FOIivZbZuW!e zYFCd=BKL{Th2jfiF2^Xcdk39O4@&D?orIiVTh2A7aGDaW?qM53jS2_pUjoPA$!GZ` zP<{=VK`X^S~a}q)^1W^*%5kHQtEdX;}czP?cjSsrwY8o zthdw(lA^l(Dw5Kz-aM^BsT|Ivenj!}@u6Sz=C7b2-$^}0wiWOpvx(Jx7_gz``fz9o zReWfgT{j~WZu~j>Hk-lnaxNXw-1uIv8$SF@+81%iL~`P@=^{~f-V17k=x&!4lVVY+ z9ym#N*4bre*aj?@z&+h-`n5leK-g)R*lLFjb_|;Og&5b(r^oQKw48hdojd!|>b`Q? z8Ncp`oLE&4{X8R8<1<{yO1&^UVdNl(!J!=?J;a0XYRsQMgcB^nX>m=m!1#7}Ls8(t0i7O1WLVj^c-JyE54*3XseMdLR0tgE+|$`f_QVm zg-j>Ih{E@4hL5-I8wR9g@d@k`{tWc*T=irTuZTwMeKjQw)=dSXzp*-Q8;DjWIbm$E z=#lLRE4be*EQs@z=I&X&!>{2($tu`^zAV3! zKoRMLnQZ7nfT-r)lpmGm7&dDf8}ibntba-T{-ciRv7c^y7@?%E$uni+ zhYPpO8u+b=I#BhqZZol#3@r%q4?f|b?zq-EnD`K%x>)H1ea%DU>qa6iYH)Yn!ca5i z6Yl)azN1AfS1W!Tm{u}Tj?CDrXBm;_+GG1#qi*Ti)_J*V)4P*)ehznp#1+&>a&6Yx z(kaD*uGEFlrEK-q&(w+qQmHN@c%MY(Nr!8~%;AMkM@np9I@Nc~)WfS7`S}|LhO57R z-3cQ!)Os*0w#G5X;bdq6`Y-~9NXB*KY=T?D=IG1hr1D{xQr9kI*kr+vim!IA`)csq z+R6y>YZwiS$@5Dgbqc+v3ho{^JV{l2k!5D)l7e;6jk3}?oX&N}#grd(vXGE2*#=bg zzA#Ux?6h<6yMKafH}S;^u5xXp%iv!`ofonW>~UR&gam)A2^wgu8w)+`bIvb2(vV8Zsi71? zX{VY_<}dKg%fk3Oj(!9W*HD$6Xg3`53l!TK za{c*bhU56QLTPGl4Ov_d#<@RNUUY(}QA!ySFWuYt+0eUjmqz-YMXJ$Q^9#GWF-05Wlpcw1(bS`U)4$y)(Vz0q)!G; zR|?^p7xfipsO3B8aQ~lDVdO$8OUV(hRv}4RMDn=AM6Je^H>P3qd9F0HBkb@i<&o=Z zyAZrH(fl}tqCG!t;(+>5o^M77Ia(+iz^JAMsa*PGgM6Oh>ZFJ6sb!7*nJ$pg|w(0S|b6ylMZgJR;Hl z;#Ol8oyewb*InwQ1f&l+SUJ4w5Im?kCq@5rdWQO27y|zE4;$CFyC8)7OOpxSyNMtP zY5lw?Fzd$><*r&3V)0y96X@p(uJpFsIe^34zlRTSbG zKyMEkYER`@MC{%m|L5}9jTl!P#$)D(aqj-C76}Yz;2V8yQuxY!J__$YcL?btKD{IlB2Du%QX)%{5;^MR0Q7imn6e9oIUt9!Bj z^DU-Qrg@k47J||;b8-&JDXl*xL-brbp2|uF3Mt@l6}(}g$6Lmh@DV$j?WDnLP)xRL z;6`=jpj|hKA5U6J19-eh_`2K~Rb}OY8k^eA&8~<&*ucB)?!_IVj<)`_Yu7Rc2M4p~ zLkut1A(7cyk+i#BUe!Ia`9tAXsVrq>gPZthGF4AIA50ke(>nEj~WJeS3o* z_k?;~fU=*mIEIiedUnS;Zy-ZDq9nwz{$c6`#lD_Ud+5YWXBc@oUI>AH? zPZA{agnZ{{BNU@IVl{G` zd@t}r)Q8DpY7_s&}cWGrI6d$=#-?~UDZAcxQs4&f#Fw+>q)UA!#K_Z(e z>tni)hVoYIazaon%7nQ|j+{#w5^45mxPY)z%b7OY>SL8J!0hNSD(X( z<JQL|0GG)`~H6hjW5EQr1q7jBStGIJ2RrcHykF8D#<{yppE5YajMA zPJl5Pw$K(pV$pY63lgrK%CZ~DGE6=%ZdGybw2Nt)ae;qN>^sSmh9HCn8lzqLA5~m% zhBKT=1%jgiz151}R-EW9kKCQ@KXLoPXHffQBZk53W^VaMdS?-ZiuW(nT}~x&UrJLEPq#%lY(_PuSMaXW>x)5k{_>v0WRAvdCdF~d`uQ_4%g@%Yo$c+#Ft)yLpx0Clct6bNay|BI z?sFt6`BjIv(EfA}O{I`N@8IuRrC3p&MT47hol_}jB-U%r@^T|fbINj0oL-$ak_A}+ ze~r{@IB-%PxBfj$b8gP=dG;WlLHiT7U7IwxFmZ3X*w%Ea|9n2=+Xd+-^}hH*j3iBj-8ZX7(`vlsp6QennSY#>en-7m`OUF|FR1`dsA$~}V`qFa z7lYPfcTe;wMr->3>Q|QeCiWm1Bq4O|JmJneWkBbG{116=7y!j${^VfsQ9;0JDcm=? z&I1_M$8CV(eVn4b(IDxWPmw)=vMYpxiuEL4y4*eMrZ+%#>sP?pg zcF&?qnWs%Iea7i_w3o67`?f!BYoGk_msXymbS>5|qd-GUfWJKGO|CJ5+MuxAY?#GZ zQ`Nf0nM#!AcS$;}CW3$HW228Xg-=eZpnaO`FoitbvZB}LuJ`s5GSosu-F<^0kDW^s^N2Ke-!bMuAV!uSd+LO>6 z&l1j6^6+`0PpA9m#g*M*hDnVcY;e^>8y~3l?9Sn^^Mo~6wZ0x}?^9SI>mt$7o9?PP zShi4jwh3Skj!yLfb4a~1V~CJ^GudHBB)(TY4HHH6C9yl zf)%XvGs!{APA5);uriSX)YQ3_Np;~7Ef)!T9_r3gfHXX$vf=YpzV?is_^|NmOVQ}+ z>l)^$rqkNX!?qSh<}yGK$zX;vwtWGMHQ8jYWf%j7`Z8EF^r#$(M8-m?e}eP!@;nYm zD=aw9(+9#{%{cE)mk+T2V|Q1T)v{vjQM-HVlW_BF*eA zMwlaeSm9&4^Tx{sbnPcvhX1u>p!UMBc_5rJqOo&3_g29Bh*)?=W_!O)Dj$ zO!r@T2_l&?*RJ6b=m!ssPgHR{vjpe1nI_fRq3ub@b%(}pN&7?oQ4{82As(zt z^Fbq;(yh3s`PUskp<6?L#BdJsMEyQ;;r6o+zVj_Z_zJ~W8ynt)soI=^C>kMZGR?S- zvm`+@${yCke0o9gQL=NyRx?YCY(!hr^3w&fWc*SkOy97Fi0m+fbssz$L)ea_$+s_` z85{ElFu~P3rNN=epeSw=L>Icf&>mH$wcWPV zd9V1AxbPSGj!gw{*CB1XNA8*(k}u2FlRXp{l5_j_=Ocjt*`Xgnml_G3wg(!N%d^sSg9N(j%1m_NTR?Pq4_AxsV5+uqX0#>r|;41h51hFA&Z`rI!f!1%j}|Jok##^4*pCKm0Riu7};#UNW}LfM&ZtLL)z{Z{q^{> zgOwa6A2R|^FA}&<9}l7chWMIBQhfCNchVDeoT!TL+mWTZrr|e$RWSgCB9OHWf0q(b~hTFRtJb)#@yF@H2$kj8)vr{Y9UOQ`YR+o^B{!UyhPg5Nz9thO@+DtRsbyQN`4@$Z+&z zu`p4t_qp?X_}$u+?RQBwsPytc!lY6U0o@+O3Z@wre=JI~B9CSH3s2b?^Mg@@d^;uiEfe~rDGxV5 z*2(m62V=uvm$wrRv0Cy&?V2sLcmp*i!%Kl)m||H_Bwsrs9vIO%x2~wd?#168#xJQ- zrv#L*F6n>}8Sn0i^1cF2p8EfJyDFZ}0hsxPLZr6XBj6lX@j+gK_8DM@cYKh-7V{Qx zw(rqiBJ4&0#kq84f|KK+R5beLk&3YY9%$h7T|(=ra^vM>dTh;ni|SqXZ2!o6!*w1J z<8$6A&Hu$t0^QsUvajPWC1tDh%z{nh%rHSyb8;cdSjS2)lPomtcwv3BL;H?uLwR*y zbvV*--rLT-J|wRN;7f4x=1+A=w@H)qz^{eo9X!868S*d8Cz1j}i+r|P)FD;?+THSl zYpB`)z#3KJv9K1|YO+`8O-KOQAEK#<*880YC zbw@GH_-T(%L}G8G6V$ERI#^Vuz2exOuw%+r-pRrpk(hgZkyjt+u$)XSmCv+Bn*i*F zk$(ayADz4m;gEHnxoId?$i9Zje25@(jWgNNRzE8J*snS@x42wyOwfJ>l1M=FCPa&~ zzw)iJFXN8kWz#!_5I@DH3!IkU|9F?59gtj<(7nhW&n4Da@(>}zaR;!LUi>YNe@<(A z==Mt3aJ>X}>aYF@;5rYK8sii7Yk0W;Q0Bexi}!{e2yv1>$y%HjkccaM6Mq0qCnR&O z`i>YN(e-$x`x%%{#rnEs+~0-#)yQ8T`5QHV^T^-K`WI{dm(cg!Nv`EUztLAdzeJm@ zCRK2w+#F(~f-#Nb5?+se-_U3V10D+7aDO6BF(gy!pm8AJs=I{$%g6oB0{E z__|C!0W`WK%d)B7A{$wkMM9eG#ur%Ioa?a1;wr~pyI$$-y>LOL>dxwSh`co1?VV4h zPDRfrTN+j7jnPsB#-vFF4G`dgujcZ4%eXoLo&@c5Z40-IoY0xiXVkWTM>Pfx**dhh zSQw;jij%?i@?SYaGgf?L$~sN@2ApL==6x1soUb3D*$k}8HaC`|Y!!~`lNMhbBiI&Y zb!&1%cYa0;R17JP*Vfz*7a9ASf`(1P&I}~0KdPbsG+cXdX92{<^*o&Y!oc85#C&1q ze0kZWHG}yAC6A=?8fOm^KLb=w2jQt(?mdPt_+ZCQz*yJnb$+;Kd4zt3Z|&JxSIDx# zYO-(D)toO~-k0+0GX{~D8%yR)>v7EBB3EbYOF=o-%e)5@(+P&!KhB=5s;vzweN>_` z9kTXiyR+rMxozO}x{|;5oA>OY!}+sP)LHIOaBJ0y<8XVBr?4YcYiJF96gWEg0lWI7 zr=uUWODPJ6Fe}T-ZuP4*{^vog_B{?`6B8$|_JfBOh^q9Bhq;-2;j&Kc{y|k`WzA2Y dE;0{~7{B25z*w94{`*U(k-nK8^3H=7{|5(@g`EHZ From ddebdf99e9e21fa8430367cb9b01df9b645f8565 Mon Sep 17 00:00:00 2001 From: Anton Urankar <124867229+antonurankar-moloco@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:11:01 +0200 Subject: [PATCH 2/3] Harden the Arbiter demo cycle after review - Fullscreen: track the show in progress so a second tap or a retry cannot reload or destroy the ad that is opening; the close or show-failure callback restarts the cycle. - Inline: when the interval elapses, hide the consumed winner before its reload (an in-place reload rendered a creative no round had selected) and re-request every network without a fill, not only the winner. A round invalidated by Hide() resumes arbitration if Show() was tapped again meanwhile. - Screen: banner and MREC labels carry the arbiter result and bid count, since those formats have no status line. --- Assets/Scripts/Arbiter/ArbiterAdController.cs | 13 ++++ .../Arbiter/ArbiterFullscreenController.cs | 43 ++++++++++--- .../Arbiter/ArbiterInlineController.cs | 62 +++++++++++-------- Assets/Scripts/Arbiter/ArbiterScreen.cs | 50 +++++++++++---- README.md | 16 ++--- 5 files changed, 129 insertions(+), 55 deletions(-) diff --git a/Assets/Scripts/Arbiter/ArbiterAdController.cs b/Assets/Scripts/Arbiter/ArbiterAdController.cs index c81d207..2bcc24d 100644 --- a/Assets/Scripts/Arbiter/ArbiterAdController.cs +++ b/Assets/Scripts/Arbiter/ArbiterAdController.cs @@ -103,9 +103,22 @@ protected void RunArbiter(List bids, Action bids, Action onResult) { if (CloudXAvailable) diff --git a/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs index 468df33..c0fab30 100644 --- a/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs +++ b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs @@ -25,6 +25,8 @@ public abstract class ArbiterFullscreenController : ArbiterAdController private bool _cloudXSettled; private bool _adMobSettled; private CloudXArbiterResult _nextWinner; + /* A show call was made and neither close nor show-failure has arrived yet. */ + private bool _isShowing; protected ArbiterFullscreenController( string cloudXAdUnitId, @@ -42,24 +44,30 @@ protected ArbiterFullscreenController( ? null : _nextWinner.Platform; + /* True between a show call and its close or show-failure callback. */ + public bool IsShowing => _isShowing; + /* - * True when nothing is loaded, loading or decided, so only a fresh Load() - * can move things forward. The screen's backoff retry keys off this: while a - * candidate, a load or a round is pending, the controller gets there itself. + * True when nothing is loaded, loading, decided or showing, so only a fresh + * Load() can move things forward. The screen's backoff retry keys off this: + * while a candidate, a load, a round or a show is pending, the controller + * gets there itself. */ public bool NeedsRetry => - !IsDisposed && !ArbiterInFlight && PreparedWinner == null + !IsDisposed && !ArbiterInFlight && !_isShowing && PreparedWinner == null && !IsLoadingCloudX && !IsLoadingAdMob && !CloudXHasBid && !AdMobCanShow(); private bool CloudXHasBid => LoadedCloudXAd != null && CloudXIsReady(); /* * Loads whichever network does not hold a fill. After one side failed, a - * retry reloads only that side and re-arbitrates when it settles. + * retry reloads only that side and re-arbitrates when it settles. While an + * ad is showing nothing is loaded: the shown AdMob object must not be + * destroyed under the user, and the close callback restarts the cycle. */ public override void Load() { - if (IsDisposed || ArbiterInFlight) + if (IsDisposed || ArbiterInFlight || _isShowing) { return; } @@ -93,11 +101,13 @@ public override void Load() /* * Shows the stored winner. A stale winner (its ad expired or was consumed) - * is dropped and false is returned; the caller reloads. + * is dropped and false is returned; the caller reloads. While a show is in + * progress a second call is ignored and returns false; check IsShowing to + * tell the two apart. */ public bool Show() { - if (IsDisposed || _nextWinner == null) + if (IsDisposed || _isShowing || _nextWinner == null) { return false; } @@ -108,12 +118,14 @@ public bool Show() switch (winner) { case CloudXArbiterPlatform.CloudX when CloudXHasBid: + _isShowing = true; CloudXShow(); return true; case CloudXArbiterPlatform.CloudX: LoadedCloudXAd = null; return false; case CloudXArbiterPlatform.AdMob when AdMobCanShow(): + _isShowing = true; AdMobShow(); return true; case CloudXArbiterPlatform.AdMob: @@ -157,8 +169,19 @@ private void MaybePrepareWinner() } protected void RaiseAdShown(CloudXArbiterPlatform platform) => AdShown?.Invoke(platform); - protected void RaiseAdShowFailed(CloudXArbiterPlatform platform, string message) => AdShowFailed?.Invoke(platform, message); - protected void RaiseAdClosed(CloudXArbiterPlatform platform) => AdClosed?.Invoke(platform); + + /* Show-failure and close end the show; the flag clears before subscribers run so they may Load(). */ + protected void RaiseAdShowFailed(CloudXArbiterPlatform platform, string message) + { + _isShowing = false; + AdShowFailed?.Invoke(platform, message); + } + + protected void RaiseAdClosed(CloudXArbiterPlatform platform) + { + _isShowing = false; + AdClosed?.Invoke(platform); + } /* * CloudX callback handlers, shared by both fullscreen formats. The concrete diff --git a/Assets/Scripts/Arbiter/ArbiterInlineController.cs b/Assets/Scripts/Arbiter/ArbiterInlineController.cs index 3ba730d..9101a92 100644 --- a/Assets/Scripts/Arbiter/ArbiterInlineController.cs +++ b/Assets/Scripts/Arbiter/ArbiterInlineController.cs @@ -16,19 +16,21 @@ * 1. The shown winner's impression marks its fill as consumed; the loser keeps * its fill. * 2. Rounds repeat on RefreshIntervalSeconds (docs: 20-30 s). When the - * interval elapses the consumed winner (and any network without a fill) is - * reloaded, and the round runs once those loads settle. Settled loads never - * start a round on their own once something is on screen, so there is - * exactly one arbiter call per interval and never one over a half-loaded - * candidate set. + * interval elapses the shown view is hidden, the consumed winner and any + * network without a fill are reloaded, and the round runs once those loads + * settle. Settled loads never start a round on their own once something is + * on screen, so there is exactly one arbiter call per interval and never + * one over a half-loaded candidate set. * 3. Only the new winner's view is shown; the other is hidden. * * One deliberate deviation from the docs' cycle, which reloads the winner the - * moment its impression fires: both Unity plugins reload into the view that is - * on screen, so an immediate reload replaced the visible creative within a - * second of showing it (verified on device, with a second AdMob paid event). - * Reloading at the interval keeps each shown creative up for the whole - * interval; the cost is the load latency before the swap. + * moment its impression fires: both Unity plugins reload into the existing + * view, so a reload of the view on screen replaced the visible creative within + * a second of showing it and fired a second AdMob paid event (verified on + * device) - an impression for an ad no round had selected. Here the shown + * creative stays up for the whole interval, is hidden before its reload, and + * the next round decides what appears; the cost is the load latency during + * which the slot is empty. * * CloudX inline auto-refresh is opt-out: the concrete's CloudXCreateAndLoad calls * Stop*AutoRefresh before create and nothing here calls Start*AutoRefresh. AdMob @@ -169,10 +171,10 @@ public void Hide() _wantShown = false; _refreshTimer = 0f; InvalidateInFlightArbiter(); - HideBothViews(); - /* Before the shown platform is forgotten: a consumed fill must not be re-shown later. */ + /* A consumed fill must not be re-shown later; hides the views either way. */ ReloadConsumedWinner(); _shownPlatform = null; + HideBothViews(); } /* Advances the refresh clock; called every frame by the owner. */ @@ -187,16 +189,24 @@ public void Tick(float deltaSeconds) if (_refreshTimer >= _refreshIntervalSeconds) { + /* + * Interval elapsed: take the consumed winner down and request every + * missing fill (docs step 3: re-request only the networks that did + * not fill). Load() is a no-op for a side that is loaded or loading, + * so a round follows as soon as anything outstanding settles. + */ ReloadConsumedWinner(); + Load(); TryRunRound(); } } /* - * Requests the next fill for the winner whose impression fired. Runs when - * the interval elapses (or the ad is hidden), never right after the - * impression - see the class note. Load() also re-requests any network that - * did not fill last round. + * Drops the fill of the winner whose impression fired and hides its view, so + * the reload cannot render a creative no round has selected. Runs when the + * interval elapses (or the ad is hidden), never right after the impression + * - see the class note. Nothing is on screen afterwards, so the next round + * is due as soon as the loads settle. */ private void ReloadConsumedWinner() { @@ -216,6 +226,8 @@ private void ReloadConsumedWinner() _adMobLoaded = false; } + _shownPlatform = null; + HideBothViews(); Load(); } @@ -253,16 +265,9 @@ private void TryRunRound() if (bids.Count == 0) { /* - * Nothing to arbitrate. With a stale creative still on screen, keep it - * up and re-request both fills on the interval; with nothing shown, - * the screen's backoff retry owns the reload. + * Nothing to arbitrate: both loads failed. The screen's backoff retry + * owns the reload (NeedsRetry is true here). */ - if (_shownPlatform != null) - { - _refreshTimer = 0f; - Load(); - } - return; } @@ -301,6 +306,13 @@ private void OnRoundDecided(CloudXArbiterResult result) } } + /* + * Hide() invalidated a round while it was in flight and Show() may have been + * tapped again since; that Show() found the arbiter busy, so the round it + * asked for starts here. + */ + protected override void OnArbiterResultInvalidated() => TryRunRound(); + private void ShowingWinner(CloudXArbiterPlatform platform) { _shownPlatform = platform; diff --git a/Assets/Scripts/Arbiter/ArbiterScreen.cs b/Assets/Scripts/Arbiter/ArbiterScreen.cs index 69213f0..1ef3328 100644 --- a/Assets/Scripts/Arbiter/ArbiterScreen.cs +++ b/Assets/Scripts/Arbiter/ArbiterScreen.cs @@ -42,6 +42,8 @@ public class ArbiterScreen : MonoBehaviour private int _rewardedRetries; private int _bannerRetries; private int _mrecRetries; + private int _bannerBidCount; + private int _mrecBidCount; private string _cloudXStatus = "CloudX: Initializing"; private string _adMobStatus = "AdMob: Initializing"; @@ -322,9 +324,13 @@ private void CreateControllers(bool cloudXAvailable) Log($"Banner: load failed ({platform}): {message}; retrying in {delay:0}s"); Invoke(nameof(LoadBanner), delay); }; - _banner.ArbiterCompleted += (result, bidCount) => Log($"Banner: {ArbiterSummary(result, bidCount)}"); - _banner.WinnerShown += platform => _ui.SetBannerButtonLabel( - platform == CloudXArbiterPlatform.None ? "Banner: no winner" : $"Hide Banner ({platform})"); + _banner.ArbiterCompleted += (result, bidCount) => + { + _bannerBidCount = bidCount; + Log($"Banner: {ArbiterSummary(result, bidCount)}"); + }; + /* The button is the banner's status line: platform on screen and the bids it beat. */ + _banner.WinnerShown += platform => _ui.SetBannerButtonLabel(InlineLabel("Banner", platform, _bannerBidCount)); _banner.AdClicked += platform => Log($"Banner clicked ({platform})"); _mrec = new ArbiterMrecController( @@ -349,9 +355,12 @@ private void CreateControllers(bool cloudXAvailable) Log($"MREC: load failed ({platform}): {message}; retrying in {delay:0}s"); Invoke(nameof(LoadMrec), delay); }; - _mrec.ArbiterCompleted += (result, bidCount) => Log($"MREC: {ArbiterSummary(result, bidCount)}"); - _mrec.WinnerShown += platform => _ui.SetMrecButtonLabel( - platform == CloudXArbiterPlatform.None ? "MREC: no winner" : $"Hide MREC ({platform})"); + _mrec.ArbiterCompleted += (result, bidCount) => + { + _mrecBidCount = bidCount; + Log($"MREC: {ArbiterSummary(result, bidCount)}"); + }; + _mrec.WinnerShown += platform => _ui.SetMrecButtonLabel(InlineLabel("MREC", platform, _mrecBidCount)); _mrec.AdClicked += platform => Log($"MREC clicked ({platform})"); /* Parallel loads for every format; each arbitrates once its candidates settle. */ @@ -376,6 +385,12 @@ private void ShowInterstitial() return; } + if (_interstitial.IsShowing) + { + /* A second tap while the ad is opening; nothing to reload. */ + return; + } + /* * No winner is prepared (or its ad is gone); in a real app the game flow * would simply continue here. The demo reloads and says so. @@ -394,6 +409,11 @@ private void ShowRewarded() return; } + if (_rewarded.IsShowing) + { + return; + } + SetRewardedStatus("No winner prepared; reloading"); LoadRewarded(); } @@ -432,13 +452,17 @@ private static float NextRetryDelay(ref int retries) return delay; } - private static string ArbiterSummary(CloudXArbiterResult result, int bidCount) - { - var bids = bidCount == 1 ? "1 bid" : $"{bidCount} bids"; - return result.Platform == CloudXArbiterPlatform.None - ? $"Arbiter: no winner ({bids})" - : $"Arbiter: {result.Platform} ({bids})"; - } + private static string Bids(int bidCount) => bidCount == 1 ? "1 bid" : $"{bidCount} bids"; + + private static string ArbiterSummary(CloudXArbiterResult result, int bidCount) => + result.Platform == CloudXArbiterPlatform.None + ? $"Arbiter: no winner ({Bids(bidCount)})" + : $"Arbiter: {result.Platform} ({Bids(bidCount)})"; + + private static string InlineLabel(string format, CloudXArbiterPlatform platform, int bidCount) => + platform == CloudXArbiterPlatform.None + ? $"{format}: no winner ({Bids(bidCount)})" + : $"Hide {format} ({platform}, {Bids(bidCount)})"; /* * Named methods so terminal failures can retry via Invoke(nameof(...)). diff --git a/README.md b/README.md index 0df4ce8..2a0f622 100644 --- a/README.md +++ b/README.md @@ -196,10 +196,11 @@ The rules the controllers implement: - **Banner and MREC arbitrate, then render.** The winner's view is shown from the arbiter callback; the loser stays loaded but hidden, because showing it would fire an impression for a bid the arbiter did not select. Auto-refresh is off on both sides and the controller drives the cycle: every - 25 seconds the shown winner (its fill was consumed by the impression) is reloaded while the loser - keeps its fill, and a new round runs over both as soon as the reload settles. The docs reload the - winner the moment its impression fires; the Unity plugins reload into the view that is on screen, - which replaced the visible creative within a second, so this demo reloads at the interval instead. + 25 seconds the shown winner (its fill was consumed by the impression) is hidden and reloaded, any + network without a fill is re-requested, the loser keeps its fill, and a new round runs over all of + them as soon as the loads settle. The docs reload the winner the moment its impression fires; the + Unity plugins reload into the existing view, which replaced the visible creative within a second + and fired an impression no round had selected, so this demo hides and reloads at the interval instead. - **AdMob bids carry no price.** CloudX prices them from the revenue the app forwards after each AdMob impression: every AdMob ad's `OnAdPaid` goes into `CloudXSdk.ReportRevenueData`. This is a required part of the integration, not telemetry; without it CloudX never learns what AdMob pays and its @@ -208,9 +209,10 @@ The rules the controllers implement: candidate and wins every round locally. The status lines show the arbitration as it happens: which sides loaded, what the arbiter returned and -over how many bids (`Arbiter: CloudX (2 bids)`), and which platform is showing. Banner and MREC toggle -Show/Hide and their labels name the platform on screen (`Hide Banner (AdMob)`), or read `no winner` -when a round selected nobody. +over how many bids (`Arbiter: CloudX (2 bids)`), and which platform is showing. Banner and MREC have no +status line of their own, so their buttons carry it: they toggle Show/Hide and the label names the +platform on screen and the bids it beat (`Hide Banner (AdMob, 2 bids)`), or reads `no winner` when a +round selected nobody. Arbiter/TPA screen with the arbitrated banner and MREC on screen From 56e32d93eace8297e89116a681a716e9ddbd8081 Mon Sep 17 00:00:00 2001 From: Anton Urankar <124867229+antonurankar-moloco@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:49:21 +0200 Subject: [PATCH 3/3] Hide inline views once on Hide when the consumed winner is reloaded --- .../Scripts/Arbiter/ArbiterInlineController.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Assets/Scripts/Arbiter/ArbiterInlineController.cs b/Assets/Scripts/Arbiter/ArbiterInlineController.cs index 9101a92..eec7670 100644 --- a/Assets/Scripts/Arbiter/ArbiterInlineController.cs +++ b/Assets/Scripts/Arbiter/ArbiterInlineController.cs @@ -171,10 +171,14 @@ public void Hide() _wantShown = false; _refreshTimer = 0f; InvalidateInFlightArbiter(); - /* A consumed fill must not be re-shown later; hides the views either way. */ - ReloadConsumedWinner(); + + /* A consumed fill must not be re-shown later; the reload path hides the views itself. */ + if (!ReloadConsumedWinner()) + { + HideBothViews(); + } + _shownPlatform = null; - HideBothViews(); } /* Advances the refresh clock; called every frame by the owner. */ @@ -206,13 +210,14 @@ public void Tick(float deltaSeconds) * the reload cannot render a creative no round has selected. Runs when the * interval elapses (or the ad is hidden), never right after the impression * - see the class note. Nothing is on screen afterwards, so the next round - * is due as soon as the loads settle. + * is due as soon as the loads settle. Returns whether there was a consumed + * winner to take down. */ - private void ReloadConsumedWinner() + private bool ReloadConsumedWinner() { if (!_winnerConsumed) { - return; + return false; } _winnerConsumed = false; @@ -229,6 +234,7 @@ private void ReloadConsumedWinner() _shownPlatform = null; HideBothViews(); Load(); + return true; } /*