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..2bcc24d --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterAdController.cs @@ -0,0 +1,217 @@ +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); + } + else + { + OnArbiterResultInvalidated(); + } + }); + } + + /* + * A result arrived for a round that Hide() invalidated while it was in + * flight. Nothing may be shown from it, but the family may need to start the + * round the user asked for in the meantime. + */ + protected virtual void OnArbiterResultInvalidated() + { + } + + 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..c0fab30 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterFullscreenController.cs @@ -0,0 +1,285 @@ +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; + /* A show call was made and neither close nor show-failure has arrived yet. */ + private bool _isShowing; + + 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 between a show call and its close or show-failure callback. */ + public bool IsShowing => _isShowing; + + /* + * 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 && !_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. 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 || _isShowing) + { + 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. 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 || _isShowing || _nextWinner == null) + { + return false; + } + + var winner = _nextWinner.Platform; + _nextWinner = null; + + 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: + 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); + + /* 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 + * 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..eec7670 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterInlineController.cs @@ -0,0 +1,488 @@ +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 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 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 + * 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(); + + /* A consumed fill must not be re-shown later; the reload path hides the views itself. */ + if (!ReloadConsumedWinner()) + { + HideBothViews(); + } + + _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) + { + /* + * 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(); + } + } + + /* + * 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. Returns whether there was a consumed + * winner to take down. + */ + private bool ReloadConsumedWinner() + { + if (!_winnerConsumed) + { + return false; + } + + _winnerConsumed = false; + + if (_shownPlatform == CloudXArbiterPlatform.CloudX) + { + _cloudXLoaded = false; + } + else if (_shownPlatform == CloudXArbiterPlatform.AdMob) + { + _adMobLoaded = false; + } + + _shownPlatform = null; + HideBothViews(); + Load(); + return true; + } + + /* + * 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: both loads failed. The screen's backoff retry + * owns the reload (NeedsRetry is true here). + */ + 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; + } + } + + /* + * 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; + _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..1ef3328 --- /dev/null +++ b/Assets/Scripts/Arbiter/ArbiterScreen.cs @@ -0,0 +1,511 @@ +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 int _bannerBidCount; + private int _mrecBidCount; + 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) => + { + _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( + 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) => + { + _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. */ + 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; + } + + 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. + */ + SetInterstitialStatus("No winner prepared; reloading"); + LoadInterstitial(); + } + + private void ShowRewarded() + { + var winner = _rewarded.PreparedWinner; + + if (_rewarded.Show()) + { + Log($"Showing the rewarded ad ({winner})"); + return; + } + + if (_rewarded.IsShowing) + { + 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 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(...)). + */ + + 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 deleted file mode 100644 index c2f6aef..0000000 --- a/Assets/Scripts/FirstLook/FirstLookConfig.cs +++ /dev/null @@ -1,24 +0,0 @@ -/* - * The AdMob ad unit ids the First Look demo falls back to. 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 unit 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. - * - * The banner pass cooldown is not here; it lives in FirstLookBannerCycle, - * which paces it. - * - * https://docs.cloudx.io/en/unity/integrations/first-look - */ -public static class FirstLookConfig -{ -#if UNITY_IOS - public const string AdMobInterstitialAdUnitId = "ca-app-pub-3940256099942544/4411468910"; - public const string AdMobBannerAdUnitId = "ca-app-pub-3940256099942544/2934735716"; -#else - public const string AdMobInterstitialAdUnitId = "ca-app-pub-3940256099942544/1033173712"; - public const string AdMobBannerAdUnitId = "ca-app-pub-3940256099942544/6300978111"; -#endif -} diff --git a/Assets/Scripts/FirstLook/FirstLookConfig.cs.meta b/Assets/Scripts/FirstLook/FirstLookConfig.cs.meta deleted file mode 100644 index 2719335..0000000 --- a/Assets/Scripts/FirstLook/FirstLookConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2e6f9d5b672fa491d9309917aaacecc2 \ No newline at end of file diff --git a/Assets/Scripts/FirstLook/FirstLookScreen.cs b/Assets/Scripts/FirstLook/FirstLookScreen.cs index 329c0b3..0bd76a3 100644 --- a/Assets/Scripts/FirstLook/FirstLookScreen.cs +++ b/Assets/Scripts/FirstLook/FirstLookScreen.cs @@ -217,7 +217,7 @@ private void CreateControllers(bool cloudXAvailable) _interstitial = new FirstLookInterstitialController( DemoConfig.InterstitialAdUnitId, - FirstLookConfig.AdMobInterstitialAdUnitId, + DemoConfig.AdMobInterstitialAdUnitId, cloudXAvailable); _interstitial.AdLoaded += source => { @@ -273,7 +273,7 @@ private void CreateControllers(bool cloudXAvailable) LoadInterstitial(); _banner.Begin( DemoConfig.BannerAdUnitId, - FirstLookConfig.AdMobBannerAdUnitId, + DemoConfig.AdMobBannerAdUnitId, cloudXAvailable); _ui.SetActionsInteractable(true); } 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 3187ac8..314bdee 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 gives CloudX the first chance and 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 @@ -136,7 +137,6 @@ screen: | `FirstLookBannerController.cs` | Which SDK fills a banner pass, self-contained. | | `FirstLookBannerCycle.cs` | When the next pass starts: the clock the controller has no way to keep. | | `FirstLookSource.cs` | The `CloudX` / `AdMob` enum every event reports. | -| `FirstLookConfig.cs` | The AdMob fallback ad unit ids. | | `FirstLookScreen.cs` | Initializes both SDKs, wires the controller and the cycle to the buttons. | **To integrate the interstitial, copy two files:** `FirstLookInterstitialController.cs` and @@ -197,13 +197,90 @@ to `StopBannerAutoRefresh` - so the controller calls that before create and neve > 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 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 + 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 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 + +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 @@ -221,9 +298,9 @@ 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 -**Automatic refresh** to Disabled on the banner one (see the First Look section for why). +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 0000000..309cc72 Binary files /dev/null and b/docs/images/arbiter-inline.png differ diff --git a/docs/images/arbiter-screen.png b/docs/images/arbiter-screen.png new file mode 100644 index 0000000..ac0311e Binary files /dev/null and b/docs/images/arbiter-screen.png differ diff --git a/docs/images/options-screen.png b/docs/images/options-screen.png index ca915de..a137240 100644 Binary files a/docs/images/options-screen.png and b/docs/images/options-screen.png differ