diff --git a/Assets/AnyThinkPlugin/AnyThinkAds/Api/ATSDKAPI.cs b/Assets/AnyThinkPlugin/AnyThinkAds/Api/ATSDKAPI.cs index 93f60ef..5770dac 100644 --- a/Assets/AnyThinkPlugin/AnyThinkAds/Api/ATSDKAPI.cs +++ b/Assets/AnyThinkPlugin/AnyThinkAds/Api/ATSDKAPI.cs @@ -229,6 +229,20 @@ namespace AnyThinkAds.Api client.setLocation(longitude, latitude); } + public static void start() + { + client.start(); + } + + public static bool isCnSDK() + { + return client.isCnSDK(); + } + + public static void setLocalStrategyAssetPath(string assetPath) + { + client.setLocalStrategyAssetPath(assetPath); + } + } } - diff --git a/Assets/AnyThinkPlugin/AnyThinkAds/Common/IATSDKAPIClient.cs b/Assets/AnyThinkPlugin/AnyThinkAds/Common/IATSDKAPIClient.cs index 5783db0..a8d7cba 100644 --- a/Assets/AnyThinkPlugin/AnyThinkAds/Common/IATSDKAPIClient.cs +++ b/Assets/AnyThinkPlugin/AnyThinkAds/Common/IATSDKAPIClient.cs @@ -31,5 +31,8 @@ namespace AnyThinkAds.Common void getArea(ATGetAreaListener listener); void setWXStatus(bool install); void setLocation(double longitude, double latitude); + void start(); + bool isCnSDK(); + void setLocalStrategyAssetPath(string assetPath); } } diff --git a/Assets/AnyThinkPlugin/AnyThinkAds/Platform/ATAdsClientFactory.cs b/Assets/AnyThinkPlugin/AnyThinkAds/Platform/ATAdsClientFactory.cs index dfd4504..cf6be1e 100644 --- a/Assets/AnyThinkPlugin/AnyThinkAds/Platform/ATAdsClientFactory.cs +++ b/Assets/AnyThinkPlugin/AnyThinkAds/Platform/ATAdsClientFactory.cs @@ -162,6 +162,9 @@ namespace AnyThinkAds public void getArea(ATGetAreaListener listener) { } public void setWXStatus(bool install) { } public void setLocation(double longitude, double latitude) { } + public void start() { } + public bool isCnSDK() { return false; } + public void setLocalStrategyAssetPath(string assetPath) { } public void showDebuggerUI() {} public void showDebuggerUI(string debugKey) {} } @@ -490,4 +493,4 @@ namespace AnyThinkAds public void entryScenarioWithPlacementID(string placementId, string scenarioID) {} } -} \ No newline at end of file +} diff --git a/Assets/AnyThinkPlugin/AnyThinkAds/Platform/Android/ATSDKAPIClient.cs b/Assets/AnyThinkPlugin/AnyThinkAds/Platform/Android/ATSDKAPIClient.cs index 9be644b..702cef1 100644 --- a/Assets/AnyThinkPlugin/AnyThinkAds/Platform/Android/ATSDKAPIClient.cs +++ b/Assets/AnyThinkPlugin/AnyThinkAds/Platform/Android/ATSDKAPIClient.cs @@ -9,11 +9,13 @@ namespace AnyThinkAds.Android public class ATSDKAPIClient : AndroidJavaProxy, IATSDKAPIClient { private AndroidJavaObject sdkInitHelper; + private AndroidJavaClass sdkClass; private ATSDKInitListener sdkInitListener; public ATSDKAPIClient () : base("com.anythink.unitybridge.sdkinit.SDKInitListener") { this.sdkInitHelper = new AndroidJavaObject( "com.anythink.unitybridge.sdkinit.SDKInitHelper", this); + this.sdkClass = new AndroidJavaClass("com.anythink.core.api.ATSDK"); } public void initSDK(string appId, string appKey) @@ -390,5 +392,59 @@ namespace AnyThinkAds.Android Debug.Log("ATSDKAPIClient : error." + e.Message); } } + + public void start() + { + try + { + if (this.sdkClass != null) + { + this.sdkClass.CallStatic("start"); + } + } + catch (System.Exception e) + { + System.Console.WriteLine("Exception caught: {0}", e); + Debug.Log("ATSDKAPIClient : start error." + e.Message); + } + } + + public bool isCnSDK() + { + try + { + if (this.sdkClass != null) + { + return this.sdkClass.CallStatic("isCnSDK"); + } + } + catch (System.Exception e) + { + System.Console.WriteLine("Exception caught: {0}", e); + Debug.Log("ATSDKAPIClient : isCnSDK error." + e.Message); + } + + return false; + } + + public void setLocalStrategyAssetPath(string assetPath) + { + try + { + if (this.sdkClass != null) + { + using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (AndroidJavaObject currentActivity = unityPlayer.GetStatic("currentActivity")) + { + this.sdkClass.CallStatic("setLocalStrategyAssetPath", currentActivity, assetPath); + } + } + } + catch (System.Exception e) + { + System.Console.WriteLine("Exception caught: {0}", e); + Debug.Log("ATSDKAPIClient : setLocalStrategyAssetPath error." + e.Message); + } + } } } diff --git a/Assets/AnyThinkPlugin/AnyThinkAds/Platform/iOS/ATSDKAPIClient.cs b/Assets/AnyThinkPlugin/AnyThinkAds/Platform/iOS/ATSDKAPIClient.cs index 3209269..2308e8f 100644 --- a/Assets/AnyThinkPlugin/AnyThinkAds/Platform/iOS/ATSDKAPIClient.cs +++ b/Assets/AnyThinkPlugin/AnyThinkAds/Platform/iOS/ATSDKAPIClient.cs @@ -193,6 +193,21 @@ namespace AnyThinkAds.iOS { Debug.Log("Unity:ATSDKAPIClient::setLocation()"); ATManager.setLocation(longitude, latitude); } + + public void start() + { + Debug.Log("Unity:ATSDKAPIClient::start() noop on iOS"); + } + + public bool isCnSDK() + { + return false; + } + + public void setLocalStrategyAssetPath(string assetPath) + { + Debug.Log("Unity:ATSDKAPIClient::setLocalStrategyAssetPath() noop on iOS"); + } //iOS显示Debugger UI public void showDebuggerUI() diff --git a/Assets/CHANGELOG.md b/Assets/CHANGELOG.md new file mode 100644 index 0000000..1ebe48a --- /dev/null +++ b/Assets/CHANGELOG.md @@ -0,0 +1,14 @@ +# [1.4.7] + +### 新增 + +* 支持中国区 `ATSDK.start()`、本地策略资源路径和自动加载相关配置。 +* 新增激励/插屏广告场景显式上报能力,播放链不再自动补记场景到达。 +* 增加 Android IMGUI 调试样例场景、日志导出和官方 DebugUI 手动入口。 + +### 修复 + +* 激励和插屏切换为更稳的自动加载/预热策略,补强首次播放容错。 +* 修复插屏关闭、展示失败和播放失败回调链不完整的问题。 +* 调整 debug 行为:`topon.debug=true` 仅开启日志,不再自动弹出官方 DebugUI。 + diff --git a/Assets/Plugins/Android/anythink_adx_sdk_kuying_6.5.56_necessary.aar b/Assets/Plugins/Android/anythink_adx_sdk_kuying_6.5.56_necessary.aar deleted file mode 100644 index 125cfb3..0000000 Binary files a/Assets/Plugins/Android/anythink_adx_sdk_kuying_6.5.56_necessary.aar and /dev/null differ diff --git a/Assets/Plugins/Android/anythink_adx_sdk_kuying_6.5.56_necessary.aar.meta b/Assets/Plugins/Android/anythink_adx_sdk_kuying_6.5.56_necessary.aar.meta deleted file mode 100644 index 90eaf3c..0000000 --- a/Assets/Plugins/Android/anythink_adx_sdk_kuying_6.5.56_necessary.aar.meta +++ /dev/null @@ -1,32 +0,0 @@ -fileFormatVersion: 2 -guid: 89da22c2144f47ea823de749873367b4 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Android: Android - second: - enabled: 1 - settings: {} - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Plugins/Android/anythink_network_adx_kuying_sdk_necessary_6.5.56.1.1.aar b/Assets/Plugins/Android/anythink_network_adx_kuying_sdk_necessary_6.5.56.1.1.aar deleted file mode 100644 index a6bc1e6..0000000 Binary files a/Assets/Plugins/Android/anythink_network_adx_kuying_sdk_necessary_6.5.56.1.1.aar and /dev/null differ diff --git a/Assets/Plugins/Android/anythink_network_adx_kuying_sdk_necessary_6.5.56.1.1.aar.meta b/Assets/Plugins/Android/anythink_network_adx_kuying_sdk_necessary_6.5.56.1.1.aar.meta deleted file mode 100644 index d5834ff..0000000 --- a/Assets/Plugins/Android/anythink_network_adx_kuying_sdk_necessary_6.5.56.1.1.aar.meta +++ /dev/null @@ -1,32 +0,0 @@ -fileFormatVersion: 2 -guid: 417c2272592f46b0b62da859413203f3 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Android: Android - second: - enabled: 1 - settings: {} - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Samples.meta b/Assets/Samples.meta new file mode 100644 index 0000000..e90fab9 --- /dev/null +++ b/Assets/Samples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 744cdc59a095c7f43a810b4344dfbdf4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/IAAAdDebugSample.meta b/Assets/Samples/IAAAdDebugSample.meta new file mode 100644 index 0000000..45c812e --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eeb4602ce24931f4b85a300e6c6826ed +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/IAAAdDebugSample/Configs.meta b/Assets/Samples/IAAAdDebugSample/Configs.meta new file mode 100644 index 0000000..7f4ec72 --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/Configs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1385bb12efc24cb43a93bb40ace7ea14 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset b/Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset new file mode 100644 index 0000000..2005c79 --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset @@ -0,0 +1,48 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 98b31b835f394db4a51957c3f0f3ac32, type: 3} + m_Name: IAAAdDebugSampleConfig + m_EditorClassIdentifier: + ConfigName: IAA Ad Debug Sample + Id: your_taku_app_id + Key: your_taku_app_key + Key2: + BaseAwardAdKeyValue: + key: rewarded + value: your_rewarded_placement_id + BaseInteractionAdKeyValue: + key: interstitial + value: your_interstitial_placement_id + BaseSplashAdKeyValue: + key: splash + value: optional_splash_placement + CommonKeyValues: + - key: topon.debug + value: true + - key: topon.auto_open_debugger_ui + value: false + - key: topon.rewarded_auto_load + value: true + - key: topon.rewarded_prewarm_on_init + value: true + - key: topon.rewarded_max_load_attempts + value: 3 + - key: topon.rewarded_load_retry_delay_ms + value: 1000 + - key: topon.interstitial_auto_load + value: true + - key: topon.interstitial_prewarm_on_init + value: true + - key: topon.interstitial_max_load_attempts + value: 2 + - key: topon.interstitial_load_retry_delay_ms + value: 500 diff --git a/Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset.meta b/Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset.meta new file mode 100644 index 0000000..48b64fc --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5acf750d60576674597ee314d6a39cf8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/IAAAdDebugSample/README.md b/Assets/Samples/IAAAdDebugSample/README.md new file mode 100644 index 0000000..98207b8 --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/README.md @@ -0,0 +1,112 @@ +# IAA Ad Debug Sample + +## 文件位置 + +- 场景: + - `Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity` +- 示例配置: + - `Assets/Samples/IAAAdDebugSample/Configs/IAAAdDebugSampleConfig.asset` + +## 使用步骤 + +1. 打开示例配置资产 `IAAAdDebugSampleConfig.asset` +2. 填入以下字段: + - `Id` = Taku AppId + - `Key` = Taku AppKey + - `BaseAwardAdKeyValue.value` = 激励视频广告位ID + - `BaseInteractionAdKeyValue.value` = 插屏广告位ID +3. 打开示例场景 `IAAAdDebugSample.unity` +4. 运行到 Android 手机测试 + +## 当前默认策略 + +- SDK Debug + - `topon.debug=true` + - `topon.auto_open_debugger_ui=false` +- 激励视频 + - 自动加载:开启 + - 初始化预热:开启 + - 最大加载尝试次数:3 + - 首轮失败补偿:1000ms + +- 插屏广告 + - 自动加载:开启 + - 初始化预热:开启 + - 最大加载尝试次数:2 + - 首轮失败补偿:500ms + +## 场景里的 IMGUI 面板支持 + +- 初始化 ADManager +- 打开官方 DebugUI +- 手动 Enter Rewarded / Interstitial Scene +- 手动 Load 激励 / 插屏 +- AsyncPlay 激励 / 插屏 +- 查看当前屏幕分辨率和 SafeArea +- 查看当前策略状态 +- 手动刷新状态快照,避免高频状态查询刷满日志 +- 查看 Rewarded / Interstitial 的状态 JSON +- 一键复制全部日志 +- 自动写入本地 session log 文件 +- 一键导出当前面板可见日志到单独文件 +- 一键复制日志文件路径 +- 查看诊断日志 + +## 关于 Debug 模式 + +- `topon.debug=true` 只用于打开 SDK 调试日志 +- 不会再因为 debug 模式自动弹出官方 DebugUI +- 官方 DebugUI 改为手动点击 `Open DebugUI` + +## 场景到达测试方式 + +推荐验证两条链路: + +1. 先点击 `Enter Rewarded Scene`,再点击 `Async Play Rewarded` +2. 不点击 `Enter Rewarded Scene`,直接点击 `Async Play Rewarded` + +当前实现逻辑: + +- 如果提前调用了 `EnterAdScenario` + - 播放时不会重复上报场景 +- 如果没有提前调用 + - 播放时不再自动补“场景到达”统计 + +## 建议测试流程 + +1. 打开场景并运行到真机 +2. 点击 `Initialize ADManager` +3. 点击 `Open DebugUI`,确认 SDK Debug 面板可打开 +4. 点击 `Enter Rewarded Scene` +5. 点击 `Async Play Rewarded` +6. 点击 `Enter Interstitial Scene` +7. 点击 `Async Play Interstitial` +8. 点击 `Copy All Logs` +9. 将日志直接粘贴给 Codex 分析 + +如果日志太长,不适合走剪贴板: + +1. 点击 `Export Logs To File` +2. 点击 `Copy Log File Path` +3. 到真机的 `Application.persistentDataPath/IAAAdDebugLogs` 下取出日志文件 +4. 将文件内容发给 Codex 分析 + +## 关于分辨率 + +当前 IMGUI 布局按 `1080 x 2340` 纵向手机做参考设计,并根据 `SafeArea` 自动缩放,适合作为真机调试面板。 + +## 关于签名 + +当前示例只负责在 Unity 内和 Android 调试时验证广告链路。 + +- 不需要官方提供签名 +- 只有在你要正式导出 APK / AAB 时,才需要你自己的包名和签名配置 + +## 关于日志文件位置 + +- 运行时会自动创建一份 session log +- 目录:`Application.persistentDataPath/IAAAdDebugLogs` +- Android 上通常类似: + - `/storage/emulated/0/Android/data/<包名>/files/IAAAdDebugLogs` +- 当前测试包名是: + - `com.foldcc.starveg` diff --git a/Assets/Samples/IAAAdDebugSample/README.md.meta b/Assets/Samples/IAAAdDebugSample/README.md.meta new file mode 100644 index 0000000..62aef6f --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a116cafc1b2af4441a5b9b3213aa0b1a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/IAAAdDebugSample/Scenes.meta b/Assets/Samples/IAAAdDebugSample/Scenes.meta new file mode 100644 index 0000000..2a7aab1 --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 79b714e45b20df34490a0062dcbafdd6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity b/Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity new file mode 100644 index 0000000..3a3abde --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity @@ -0,0 +1,274 @@ +%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: 9 + 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: 12 + m_GIWorkflowMode: 1 + 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_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + 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: 0} +--- !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 &320557560 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 320557563} + - component: {fileID: 320557562} + - component: {fileID: 320557561} + 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 &320557561 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 320557560} + m_Enabled: 1 +--- !u!20 &320557562 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 320557560} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, 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: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &320557563 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 320557560} + 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 &924544598 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 924544600} + - component: {fileID: 924544599} + m_Layer: 0 + m_Name: IAA Ad Debug Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &924544599 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 924544598} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 53b297b9bf0d4cfca2b77af0fd74c95c, type: 3} + m_Name: + m_EditorClassIdentifier: + adConfig: {fileID: 11400000, guid: 5acf750d60576674597ee314d6a39cf8, type: 2} + userId: debug_user_001 + rewardedScenario: reward_debug + interstitialScenario: interstitial_debug + autoInitialize: 1 + forcePortrait: 1 + captureUnityLogs: 1 + showVerboseState: 1 +--- !u!4 &924544600 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 924544598} + 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!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 320557563} + - {fileID: 924544600} diff --git a/Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity.meta b/Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity.meta new file mode 100644 index 0000000..4ab58c7 --- /dev/null +++ b/Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f2af01c304b689445904b3b93d20ff83 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Topon_Adapter/Editor/IAAAdAndroidBuild.cs b/Assets/Topon_Adapter/Editor/IAAAdAndroidBuild.cs new file mode 100644 index 0000000..d537cc3 --- /dev/null +++ b/Assets/Topon_Adapter/Editor/IAAAdAndroidBuild.cs @@ -0,0 +1,166 @@ +#if UNITY_EDITOR +using System; +using System.IO; +using System.Reflection; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +public static class IAAAdAndroidBuild +{ + private const string OutputDirectory = "Build/Android"; + private const string OutputApkPath = OutputDirectory + "/IAAAdDebugSample-starveg.apk"; + private const string KeystorePath = "Build/Android/keys/starveg-test.keystore"; + private const string KeystorePassword = "Foldcc123!"; + private const string KeyAliasName = "starvegtest"; + private const string PackageName = "com.foldcc.starveg"; + + [MenuItem("Topon/Build IAA Android Test APK")] + public static void BuildAndroidTestApk() + { + EnsureBuildFolders(); + ConfigurePlayerSettings(); + var resolverState = CaptureResolverState(); + + try + { + ApplyBatchFriendlyResolverSettings(); + + var scenes = new[] { "Assets/Samples/IAAAdDebugSample/Scenes/IAAAdDebugSample.unity" }; + var options = new BuildPlayerOptions + { + scenes = scenes, + target = BuildTarget.Android, + locationPathName = OutputApkPath, + options = BuildOptions.None + }; + + var report = BuildPipeline.BuildPlayer(options); + if (report.summary.result != BuildResult.Succeeded) + { + throw new Exception( + $"Android APK build failed: {report.summary.result} ({report.summary.totalErrors} errors)"); + } + + Debug.Log($"[IAA Build] APK generated: {Path.GetFullPath(OutputApkPath)}"); + } + finally + { + RestoreResolverState(resolverState); + } + } + + public static void BuildAndroidTestApkInBatchMode() + { + BuildAndroidTestApk(); + EditorApplication.Exit(0); + } + + private static void ConfigurePlayerSettings() + { + PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, PackageName); + PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait; + PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARMv7 | AndroidArchitecture.ARM64; + PlayerSettings.Android.useCustomKeystore = true; + PlayerSettings.Android.keystoreName = Path.GetFullPath(KeystorePath); + PlayerSettings.Android.keystorePass = KeystorePassword; + PlayerSettings.Android.keyaliasName = KeyAliasName; + PlayerSettings.Android.keyaliasPass = KeystorePassword; + PlayerSettings.bundleVersion = string.IsNullOrWhiteSpace(PlayerSettings.bundleVersion) + ? "1.0.0" + : PlayerSettings.bundleVersion; + PlayerSettings.Android.bundleVersionCode = Mathf.Max(1, PlayerSettings.Android.bundleVersionCode); + AssetDatabase.SaveAssets(); + } + + private static void EnsureBuildFolders() + { + Directory.CreateDirectory(Path.GetFullPath(OutputDirectory)); + Directory.CreateDirectory(Path.GetFullPath(Path.GetDirectoryName(KeystorePath) ?? OutputDirectory)); + } + + private static ResolverState CaptureResolverState() + { + var settingsType = GetResolverSettingsType(); + return new ResolverState + { + EnableAutoResolution = GetResolverBool(settingsType, "EnableAutoResolution"), + AutoResolveOnBuild = GetResolverBool(settingsType, "AutoResolveOnBuild"), + PromptBeforeAutoResolution = GetResolverBool(settingsType, "PromptBeforeAutoResolution") + }; + } + + private static void ApplyBatchFriendlyResolverSettings() + { + var settingsType = GetResolverSettingsType(); + SetResolverBool(settingsType, "EnableAutoResolution", false); + SetResolverBool(settingsType, "AutoResolveOnBuild", false); + SetResolverBool(settingsType, "PromptBeforeAutoResolution", false); + } + + private static void RestoreResolverState(ResolverState state) + { + var settingsType = GetResolverSettingsType(); + SetResolverBool(settingsType, "EnableAutoResolution", state.EnableAutoResolution); + SetResolverBool(settingsType, "AutoResolveOnBuild", state.AutoResolveOnBuild); + SetResolverBool(settingsType, "PromptBeforeAutoResolution", state.PromptBeforeAutoResolution); + } + + private static Type GetResolverSettingsType() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var type = assembly.GetType("GooglePlayServices.SettingsDialog"); + if (type != null) + { + return type; + } + } + + return null; + } + + private static bool GetResolverBool(Type settingsType, string propertyName) + { + if (settingsType == null) + { + return false; + } + + var property = settingsType.GetProperty( + propertyName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (property == null) + { + return false; + } + + return (bool)property.GetValue(null, null); + } + + private static void SetResolverBool(Type settingsType, string propertyName, bool value) + { + if (settingsType == null) + { + return; + } + + var property = settingsType.GetProperty( + propertyName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (property == null) + { + return; + } + + property.SetValue(null, value, null); + } + + private struct ResolverState + { + public bool EnableAutoResolution; + public bool AutoResolveOnBuild; + public bool PromptBeforeAutoResolution; + } +} +#endif diff --git a/Assets/Topon_Adapter/Editor/IAAAdAndroidBuild.cs.meta b/Assets/Topon_Adapter/Editor/IAAAdAndroidBuild.cs.meta new file mode 100644 index 0000000..c47d859 --- /dev/null +++ b/Assets/Topon_Adapter/Editor/IAAAdAndroidBuild.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97e92d5544ec4fc9adf7571f9f4ea241 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Topon_Adapter/Editor/IAAAdDebugSampleCreator.cs b/Assets/Topon_Adapter/Editor/IAAAdDebugSampleCreator.cs new file mode 100644 index 0000000..92b6792 --- /dev/null +++ b/Assets/Topon_Adapter/Editor/IAAAdDebugSampleCreator.cs @@ -0,0 +1,159 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.IO; +using Runtime.ADAggregator; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +public static class IAAAdDebugSampleCreator +{ + private const string SampleRoot = "Assets/Samples/IAAAdDebugSample"; + private const string SampleSceneDirectory = SampleRoot + "/Scenes"; + private const string SampleConfigDirectory = SampleRoot + "/Configs"; + private const string SampleScenePath = SampleSceneDirectory + "/IAAAdDebugSample.unity"; + private const string SampleConfigPath = SampleConfigDirectory + "/IAAAdDebugSampleConfig.asset"; + + [MenuItem("Topon/Create IAA IMGUI Debug Sample")] + public static void CreateOrUpdateSample() + { + EnsureDirectory(SampleRoot); + EnsureDirectory(SampleSceneDirectory); + EnsureDirectory(SampleConfigDirectory); + + var config = LoadOrCreateConfig(); + CreateOrUpdateScene(config); + AppendSceneToBuildSettings(SampleScenePath); + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + EditorUtility.DisplayDialog( + "IAA Sample Created", + $"Scene: {SampleScenePath}\nConfig: {SampleConfigPath}", + "OK"); + } + + public static void CreateOrUpdateSampleInBatchMode() + { + CreateOrUpdateSample(); + EditorApplication.Exit(0); + } + + private static ADConfig LoadOrCreateConfig() + { + var config = AssetDatabase.LoadAssetAtPath(SampleConfigPath); + if (config == null) + { + config = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(config, SampleConfigPath); + } + + config.ConfigName = "IAA Ad Debug Sample"; + config.Id = string.IsNullOrWhiteSpace(config.Id) ? "fill_taku_app_id" : config.Id; + config.Key = string.IsNullOrWhiteSpace(config.Key) ? "fill_taku_app_key" : config.Key; + config.Key2 = string.IsNullOrWhiteSpace(config.Key2) ? string.Empty : config.Key2; + config.BaseAwardAdKeyValue = config.BaseAwardAdKeyValue ?? new AdKeyValue(); + config.BaseAwardAdKeyValue.key = "rewarded"; + config.BaseAwardAdKeyValue.value = string.IsNullOrWhiteSpace(config.BaseAwardAdKeyValue.value) + ? "fill_rewarded_placement" + : config.BaseAwardAdKeyValue.value; + + config.BaseInteractionAdKeyValue = config.BaseInteractionAdKeyValue ?? new AdKeyValue(); + config.BaseInteractionAdKeyValue.key = "interstitial"; + config.BaseInteractionAdKeyValue.value = string.IsNullOrWhiteSpace(config.BaseInteractionAdKeyValue.value) + ? "fill_interstitial_placement" + : config.BaseInteractionAdKeyValue.value; + + config.BaseSplashAdKeyValue = config.BaseSplashAdKeyValue ?? new AdKeyValue(); + config.BaseSplashAdKeyValue.key = "splash"; + config.BaseSplashAdKeyValue.value = string.IsNullOrWhiteSpace(config.BaseSplashAdKeyValue.value) + ? "optional_splash_placement" + : config.BaseSplashAdKeyValue.value; + + config.CommonKeyValues ??= new List(); + SetOrAddCommonKey(config, "topon.rewarded_auto_load", "true"); + SetOrAddCommonKey(config, "topon.rewarded_prewarm_on_init", "true"); + SetOrAddCommonKey(config, "topon.rewarded_max_load_attempts", "3"); + SetOrAddCommonKey(config, "topon.rewarded_load_retry_delay_ms", "1000"); + SetOrAddCommonKey(config, "topon.interstitial_auto_load", "true"); + SetOrAddCommonKey(config, "topon.interstitial_prewarm_on_init", "true"); + SetOrAddCommonKey(config, "topon.interstitial_max_load_attempts", "2"); + SetOrAddCommonKey(config, "topon.interstitial_load_retry_delay_ms", "500"); + + EditorUtility.SetDirty(config); + return config; + } + + private static void CreateOrUpdateScene(ADConfig config) + { + var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single); + scene.name = "IAAAdDebugSample"; + + var root = new GameObject("IAA Ad Debug Sample"); + var gui = root.AddComponent(); + gui.adConfig = config; + gui.userId = "debug_user_001"; + gui.rewardedScenario = "reward_debug"; + gui.interstitialScenario = "interstitial_debug"; + gui.autoInitialize = true; + gui.forcePortrait = true; + gui.captureUnityLogs = true; + gui.showVerboseState = true; + + EditorSceneManager.SaveScene(scene, SampleScenePath); + } + + private static void AppendSceneToBuildSettings(string scenePath) + { + var scenes = new List(EditorBuildSettings.scenes); + var exists = false; + for (var i = 0; i < scenes.Count; i++) + { + if (scenes[i].path != scenePath) + { + continue; + } + + scenes[i] = new EditorBuildSettingsScene(scenePath, true); + exists = true; + break; + } + + if (!exists) + { + scenes.Add(new EditorBuildSettingsScene(scenePath, true)); + } + + EditorBuildSettings.scenes = scenes.ToArray(); + } + + private static void SetOrAddCommonKey(ADConfig config, string key, string value) + { + foreach (var item in config.CommonKeyValues) + { + if (item != null && item.key == key) + { + item.value = value; + return; + } + } + + config.CommonKeyValues.Add(new AdKeyValue + { + key = key, + value = value + }); + } + + private static void EnsureDirectory(string assetPath) + { + if (AssetDatabase.IsValidFolder(assetPath)) + { + return; + } + + Directory.CreateDirectory(Path.GetFullPath(assetPath)); + AssetDatabase.Refresh(); + } +} +#endif diff --git a/Assets/Topon_Adapter/Editor/IAAAdDebugSampleCreator.cs.meta b/Assets/Topon_Adapter/Editor/IAAAdDebugSampleCreator.cs.meta new file mode 100644 index 0000000..4e8992c --- /dev/null +++ b/Assets/Topon_Adapter/Editor/IAAAdDebugSampleCreator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c488fb4879e14f5cb366ce084ff2b748 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Topon_Adapter/Editor/IAABatchBuildBootstrap.cs b/Assets/Topon_Adapter/Editor/IAABatchBuildBootstrap.cs new file mode 100644 index 0000000..bf50c26 --- /dev/null +++ b/Assets/Topon_Adapter/Editor/IAABatchBuildBootstrap.cs @@ -0,0 +1,78 @@ +#if UNITY_EDITOR +using System; +using System.IO; +using UnityEditor; +using UnityEngine; + +[InitializeOnLoad] +public static class IAABatchBuildBootstrap +{ + private const string TaskFilePath = "Temp/iaa-batch-task.txt"; + private static bool _subscribed; + private static bool _taskRunning; + + static IAABatchBuildBootstrap() + { + if (!Application.isBatchMode) + { + return; + } + + if (_subscribed) + { + return; + } + + _subscribed = true; + EditorApplication.update += ExecutePendingTaskWhenReady; + } + + private static void ExecutePendingTaskWhenReady() + { + if (!Application.isBatchMode || _taskRunning) + { + return; + } + + var fullPath = Path.GetFullPath(TaskFilePath); + if (!File.Exists(fullPath)) + { + return; + } + + if (EditorApplication.isCompiling || EditorApplication.isUpdating) + { + return; + } + + _taskRunning = true; + EditorApplication.update -= ExecutePendingTaskWhenReady; + + var task = File.ReadAllText(fullPath).Trim(); + File.Delete(fullPath); + + try + { + switch (task) + { + case "create-sample": + IAAAdDebugSampleCreator.CreateOrUpdateSample(); + break; + case "build-apk": + IAAAdAndroidBuild.BuildAndroidTestApk(); + break; + default: + Debug.LogWarning($"[IAA Batch] Unknown task: {task}"); + break; + } + + EditorApplication.Exit(0); + } + catch (Exception exception) + { + Debug.LogException(exception); + EditorApplication.Exit(1); + } + } +} +#endif diff --git a/Assets/Topon_Adapter/Editor/IAABatchBuildBootstrap.cs.meta b/Assets/Topon_Adapter/Editor/IAABatchBuildBootstrap.cs.meta new file mode 100644 index 0000000..d0b92b9 --- /dev/null +++ b/Assets/Topon_Adapter/Editor/IAABatchBuildBootstrap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33e31dbdc20d47a78340d49f58d81ef9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Topon_Adapter/Editor/Topon_Adapter.Editor.asmdef b/Assets/Topon_Adapter/Editor/Topon_Adapter.Editor.asmdef index 667b14f..397491e 100644 --- a/Assets/Topon_Adapter/Editor/Topon_Adapter.Editor.asmdef +++ b/Assets/Topon_Adapter/Editor/Topon_Adapter.Editor.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "", "references": [ "GUID:8a3d1447e0a3bdf4fa07035516da8b62", + "GUID:3198a86b02613024e960e3d04a9638cd", "GUID:483a01338fa974b4498cd71261d6e8b9", "GUID:87bccae0237fd4a41ac446d6636f95e0" ], @@ -17,4 +18,4 @@ "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Assets/Topon_Adapter/Runtime/Samples.meta b/Assets/Topon_Adapter/Runtime/Samples.meta new file mode 100644 index 0000000..59bd31f --- /dev/null +++ b/Assets/Topon_Adapter/Runtime/Samples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b14a1e102081b4246a56a26fba537991 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Topon_Adapter/Runtime/Samples/IAAAdDebugSampleGui.cs b/Assets/Topon_Adapter/Runtime/Samples/IAAAdDebugSampleGui.cs new file mode 100644 index 0000000..b9b181f --- /dev/null +++ b/Assets/Topon_Adapter/Runtime/Samples/IAAAdDebugSampleGui.cs @@ -0,0 +1,615 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AnyThinkAds.Api; +using Runtime.ADAggregator; +using UnityEngine; +using UnityEngine.EventSystems; + +public sealed class IAAAdDebugSampleGui : MonoBehaviour +{ + private const float ReferenceWidth = 1080f; + private const float ReferenceHeight = 2340f; + private const int MaxLogEntries = 120; + private const float StatusRefreshInterval = 1f; + + public ADConfig adConfig; + public string userId = "debug_user_001"; + public string rewardedScenario = "reward_debug"; + public string interstitialScenario = "interstitial_debug"; + public bool autoInitialize = true; + public bool forcePortrait = true; + public bool captureUnityLogs = true; + public bool showVerboseState = true; + public bool openDebugUiOnButton = true; + public bool autoWriteLogsToFile = true; + + private readonly List _logs = new List(MaxLogEntries); + private Vector2 _pageScroll; + private Vector2 _logScroll; + private Vector2 _rewardedStatusScroll; + private Vector2 _interstitialStatusScroll; + private bool _initInvoked; + private bool _initCallbackReceived; + private bool _stylesInitialized; + private GUIStyle _titleStyle; + private GUIStyle _sectionStyle; + private GUIStyle _textStyle; + private GUIStyle _buttonStyle; + private GUIStyle _textFieldStyle; + private GUIStyle _logStyle; + private bool _rewardedReadyCache; + private bool _interstitialReadyCache; + private string _rewardedStatusCache = "Not refreshed yet."; + private string _interstitialStatusCache = "Not refreshed yet."; + private float _nextStatusRefreshTime; + private string _sessionLogFilePath; + private string _sessionLogDirectory; + + private void Awake() + { + Application.targetFrameRate = 60; + Screen.sleepTimeout = SleepTimeout.NeverSleep; + if (forcePortrait) + { + Screen.orientation = ScreenOrientation.Portrait; + } + + if (captureUnityLogs) + { + Application.logMessageReceived += OnLogMessageReceived; + } + + ADManager.Instance.GLOBAL_ShowAwardVideoBefore += OnRewardedBefore; + ADManager.Instance.GLOBAL_ShowAwardVideoComplete += OnRewardedComplete; + InitializeLogFile(); + AppendLog("IAA Ad Debug Sample is ready."); + } + + private void Start() + { + if (autoInitialize) + { + TryInitialize(); + } + + RefreshStatusCaches(true); + } + + private void OnDestroy() + { + if (captureUnityLogs) + { + Application.logMessageReceived -= OnLogMessageReceived; + } + + ADManager.Instance.GLOBAL_ShowAwardVideoBefore -= OnRewardedBefore; + ADManager.Instance.GLOBAL_ShowAwardVideoComplete -= OnRewardedComplete; + } + + private void OnGUI() + { + EnsureStyles(); + + var safeArea = Screen.safeArea; + var scaleX = safeArea.width / ReferenceWidth; + var scaleY = safeArea.height / ReferenceHeight; + var previousMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.TRS( + new Vector3(safeArea.x, safeArea.y, 0f), + Quaternion.identity, + new Vector3(scaleX, scaleY, 1f)); + + GUILayout.BeginArea(new Rect(24f, 24f, ReferenceWidth - 48f, ReferenceHeight - 48f), GUI.skin.box); + _pageScroll = GUILayout.BeginScrollView(_pageScroll, false, true); + DrawHeader(); + DrawSetupSection(); + DrawStatusSection(); + DrawControlSection(); + DrawLogSection(); + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + GUI.matrix = previousMatrix; + } + + private void DrawHeader() + { + GUILayout.Label("IAA Ad Debug Sample", _titleStyle); + GUILayout.Label( + $"Screen: {Screen.width} x {Screen.height} | SafeArea: {Screen.safeArea.width:0} x {Screen.safeArea.height:0}", + _textStyle); + GUILayout.Label( + $"Reference Layout: {ReferenceWidth:0} x {ReferenceHeight:0} | Orientation: {Screen.orientation}", + _textStyle); + GUILayout.Space(10f); + } + + private void DrawSetupSection() + { + GUILayout.Label("Setup", _sectionStyle); + GUILayout.Label( + $"ADConfig Asset: {(adConfig != null ? adConfig.name : "Missing")} | Init Invoked: {_initInvoked} | Init Callback: {_initCallbackReceived}", + _textStyle); + + userId = DrawLabeledTextField("User ID", userId); + rewardedScenario = DrawLabeledTextField("Rewarded Scenario", rewardedScenario); + interstitialScenario = DrawLabeledTextField("Interstitial Scenario", interstitialScenario); + + var rewardedPlacement = adConfig?.BaseAwardAdKeyValue?.value ?? string.Empty; + var interstitialPlacement = adConfig?.BaseInteractionAdKeyValue?.value ?? string.Empty; + GUILayout.Label($"Rewarded Placement: {DisplayValue(rewardedPlacement)}", _textStyle); + GUILayout.Label($"Interstitial Placement: {DisplayValue(interstitialPlacement)}", _textStyle); + GUILayout.Space(6f); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Initialize ADManager", _buttonStyle, GUILayout.Height(78f))) + { + TryInitialize(); + } + if (GUILayout.Button("Open DebugUI", _buttonStyle, GUILayout.Height(78f))) + { + if (EnsureInitialized("Open DebugUI")) + { + OpenDebugUi(); + } + } + GUILayout.EndHorizontal(); + + GUILayout.Space(10f); + } + + private void DrawStatusSection() + { + GUILayout.Label("Runtime Status", _sectionStyle); + + if (_initInvoked) + { + RefreshStatusCaches(); + GUILayout.Label($"Rewarded Ready: {_rewardedReadyCache}", _textStyle); + GUILayout.Label($"Interstitial Ready: {_interstitialReadyCache}", _textStyle); + } + else + { + GUILayout.Label("Rewarded Ready: not initialized", _textStyle); + GUILayout.Label("Interstitial Ready: not initialized", _textStyle); + } + + var options = ToponAdController.CurrentOptions; + if (options != null) + { + GUILayout.Label( + $"Rewarded Auto/Prewarm/Retry: {options.RewardedAutoLoad}/{options.RewardedPrewarmOnInit}/{options.RewardedMaxLoadAttempts}@{options.RewardedLoadRetryDelayMs}ms", + _textStyle); + GUILayout.Label( + $"Interstitial Auto/Prewarm/Retry: {options.InterstitialAutoLoad}/{options.InterstitialPrewarmOnInit}/{options.InterstitialMaxLoadAttempts}@{options.InterstitialLoadRetryDelayMs}ms", + _textStyle); + GUILayout.Label( + $"Local Strategy Asset: {DisplayValue(options.LocalStrategyAssetPath)}", + _textStyle); + } + + if (showVerboseState && _initInvoked) + { + RefreshStatusCaches(); + GUILayout.Space(6f); + if (GUILayout.Button("Refresh Status Snapshot", _buttonStyle, GUILayout.Height(64f))) + { + RefreshStatusCaches(true); + AppendLog("Manual status snapshot refreshed."); + } + + GUILayout.Label("Rewarded Status", _sectionStyle); + _rewardedStatusScroll = GUILayout.BeginScrollView(_rewardedStatusScroll, GUILayout.Height(240f)); + GUILayout.Label(_rewardedStatusCache, _logStyle); + GUILayout.EndScrollView(); + + GUILayout.Label("Interstitial Status", _sectionStyle); + _interstitialStatusScroll = GUILayout.BeginScrollView(_interstitialStatusScroll, GUILayout.Height(220f)); + GUILayout.Label(_interstitialStatusCache, _logStyle); + GUILayout.EndScrollView(); + } + + GUILayout.Space(10f); + } + + private void DrawControlSection() + { + GUILayout.Label("Ad Actions", _sectionStyle); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Enter Rewarded Scene", _buttonStyle, GUILayout.Height(84f))) + { + if (EnsureInitialized("Enter Rewarded Scene")) + { + ADManager.Instance.EnterAdScenario(AD_Type.AwardVideo, rewardedScenario); + AppendLog($"Enter rewarded scene requested. scenario={rewardedScenario}"); + } + } + + if (GUILayout.Button("Enter Interstitial Scene", _buttonStyle, GUILayout.Height(84f))) + { + if (EnsureInitialized("Enter Interstitial Scene")) + { + ADManager.Instance.EnterAdScenario(AD_Type.Interaction, interstitialScenario); + AppendLog($"Enter interstitial scene requested. scenario={interstitialScenario}"); + } + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Load Rewarded", _buttonStyle, GUILayout.Height(84f))) + { + if (EnsureInitialized("Load Rewarded")) + { + ADManager.Instance.LoadAD(AD_Type.AwardVideo); + AppendLog("Manual rewarded load requested."); + } + } + + if (GUILayout.Button("Async Play Rewarded", _buttonStyle, GUILayout.Height(84f))) + { + if (EnsureInitialized("Play Rewarded")) + { + ADManager.Instance.AsyncPlayAD(AD_Type.AwardVideo, rewardedScenario, result => + { + AppendLog($"Rewarded callback: {result}"); + }); + AppendLog($"AsyncPlayAD rewarded requested. scenario={rewardedScenario}"); + } + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Load Interstitial", _buttonStyle, GUILayout.Height(84f))) + { + if (EnsureInitialized("Load Interstitial")) + { + ADManager.Instance.LoadAD(AD_Type.Interaction); + AppendLog("Manual interstitial load requested."); + } + } + + if (GUILayout.Button("Async Play Interstitial", _buttonStyle, GUILayout.Height(84f))) + { + if (EnsureInitialized("Play Interstitial")) + { + ADManager.Instance.AsyncPlayAD(AD_Type.Interaction, interstitialScenario, result => + { + AppendLog($"Interstitial callback: {result}"); + }); + AppendLog($"AsyncPlayAD interstitial requested. scenario={interstitialScenario}"); + } + } + GUILayout.EndHorizontal(); + + GUILayout.Space(10f); + } + + private void DrawLogSection() + { + GUILayout.Label("Diagnostics", _sectionStyle); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Clear Logs", _buttonStyle, GUILayout.Height(70f))) + { + _logs.Clear(); + AppendLog("Logs cleared."); + } + + if (GUILayout.Button("Copy All Logs", _buttonStyle, GUILayout.Height(70f))) + { + GUIUtility.systemCopyBuffer = string.Join("\n", _logs); + AppendLog($"Copied {_logs.Count} log lines to clipboard."); + } + + if (GUILayout.Button("Log Current Strategy", _buttonStyle, GUILayout.Height(70f))) + { + var options = ToponAdController.CurrentOptions; + if (options == null) + { + AppendLog("Current strategy unavailable: controller not initialized."); + } + else + { + AppendLog( + $"Strategy => RewardedAuto={options.RewardedAutoLoad}, RewardedPrewarm={options.RewardedPrewarmOnInit}, InterAuto={options.InterstitialAutoLoad}, InterPrewarm={options.InterstitialPrewarmOnInit}"); + } + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Export Logs To File", _buttonStyle, GUILayout.Height(70f))) + { + ExportVisibleLogsToFile(); + } + + if (GUILayout.Button("Copy Log File Path", _buttonStyle, GUILayout.Height(70f))) + { + if (string.IsNullOrWhiteSpace(_sessionLogFilePath)) + { + AppendLog("Log file path unavailable."); + } + else + { + GUIUtility.systemCopyBuffer = _sessionLogFilePath; + AppendLog($"Copied log file path: {_sessionLogFilePath}"); + } + } + GUILayout.EndHorizontal(); + + GUILayout.Label($"Log File: {DisplayValue(_sessionLogFilePath)}", _textStyle); + + _logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.Height(520f)); + foreach (var line in _logs) + { + GUILayout.Label(line, _logStyle); + } + GUILayout.EndScrollView(); + } + + private string DrawLabeledTextField(string label, string value) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(label, _textStyle, GUILayout.Width(260f)); + value = GUILayout.TextField(value ?? string.Empty, _textFieldStyle, GUILayout.Height(64f)); + GUILayout.EndHorizontal(); + return value; + } + + private bool TryInitialize() + { + if (_initInvoked) + { + AppendLog("Initialize skipped: ADManager already initialized by sample."); + return true; + } + + if (adConfig == null) + { + AppendLog("Initialize failed: sample ADConfig asset is missing."); + return false; + } + + if (string.IsNullOrWhiteSpace(adConfig.Id) || string.IsNullOrWhiteSpace(adConfig.Key)) + { + AppendLog("Initialize warning: Taku AppId/AppKey is empty. Please fill the sample ADConfig asset."); + } + + _initInvoked = true; + var controller = new ToponAdController(); + ADManager.Instance.Init(() => + { + _initCallbackReceived = true; + AppendLog("ADManager.Init callback received."); + }, userId, adConfig, controller); + AppendLog($"ADManager.Init invoked with userId={userId}"); + AppendLog($"Rewarded placement={adConfig.BaseAwardAdKeyValue?.value}, interstitial placement={adConfig.BaseInteractionAdKeyValue?.value}"); + return true; + } + + private bool EnsureInitialized(string actionName) + { + if (TryInitialize()) + { + return true; + } + + AppendLog($"{actionName} cancelled: initialization failed."); + return false; + } + + private void RefreshStatusCaches(bool force = false) + { + if (!force && Time.unscaledTime < _nextStatusRefreshTime) + { + return; + } + + _nextStatusRefreshTime = Time.unscaledTime + StatusRefreshInterval; + _rewardedReadyCache = _initInvoked && ADManager.Instance.IsRealy(AD_Type.AwardVideo); + _interstitialReadyCache = _initInvoked && ADManager.Instance.IsRealy(AD_Type.Interaction); + _rewardedStatusCache = GetRewardedStatusText(); + _interstitialStatusCache = GetInterstitialStatusText(); + } + + private string GetRewardedStatusText() + { + var placement = adConfig?.BaseAwardAdKeyValue?.value; + if (string.IsNullOrWhiteSpace(placement)) + { + return "No rewarded placement configured."; + } + + var options = ToponAdController.CurrentOptions; + if (options?.RewardedAutoLoad ?? true) + { + return ATRewardedAutoVideo.Instance.checkAutoAdStatus(placement); + } + + return ATRewardedVideo.Instance.checkAdStatus(placement); + } + + private string GetInterstitialStatusText() + { + var placement = adConfig?.BaseInteractionAdKeyValue?.value; + if (string.IsNullOrWhiteSpace(placement)) + { + return "No interstitial placement configured."; + } + + var options = ToponAdController.CurrentOptions; + if (options?.InterstitialAutoLoad ?? true) + { + return ATInterstitialAutoAd.Instance.checkAutoAdStatus(placement); + } + + return ATInterstitialAd.Instance.checkAdStatus(placement); + } + + private void OnRewardedBefore(string placementId, string scenario) + { + AppendLog($"Rewarded before show => placement={placementId}, scenario={scenario}"); + } + + private void OnRewardedComplete(bool completed) + { + AppendLog($"Rewarded global complete => reward={completed}"); + } + + private void OnLogMessageReceived(string condition, string stackTrace, LogType type) + { + if (type != LogType.Error && type != LogType.Exception && type != LogType.Warning) + { + if (!condition.Contains("[Topon]") && + !condition.Contains("ATSDK") && + !condition.Contains("Rewarded") && + !condition.Contains("Interstitial") && + !condition.Contains("Taku")) + { + return; + } + } + + AppendLog($"[{type}] {condition}"); + } + + private void OpenDebugUi() + { + var options = ToponAdController.CurrentOptions; + if (!openDebugUiOnButton) + { + AppendLog("DebugUI launch disabled by sample toggle."); + return; + } + + if (options != null && !string.IsNullOrWhiteSpace(options.DebuggerKey)) + { + ATSDKAPI.showDebuggerUI(options.DebuggerKey); + AppendLog($"Open DebugUI with debugger key={options.DebuggerKey}"); + return; + } + + ATSDKAPI.showDebuggerUI(); + AppendLog("Open DebugUI with current SDK configuration."); + } + + private void AppendLog(string message) + { + var line = $"{DateTime.Now:HH:mm:ss} {message}"; + if (_logs.Count >= MaxLogEntries) + { + _logs.RemoveAt(0); + } + + _logs.Add(line); + TryAppendLineToFile(line); + _logScroll.y = float.MaxValue; + } + + private void InitializeLogFile() + { + try + { + _sessionLogDirectory = Path.Combine(Application.persistentDataPath, "IAAAdDebugLogs"); + Directory.CreateDirectory(_sessionLogDirectory); + var fileName = $"iaa-ad-debug-{DateTime.Now:yyyyMMdd-HHmmss}.log"; + _sessionLogFilePath = Path.Combine(_sessionLogDirectory, fileName); + File.WriteAllText(_sessionLogFilePath, + $"IAA Ad Debug Sample Log{Environment.NewLine}" + + $"Created: {DateTime.Now:yyyy-MM-dd HH:mm:ss}{Environment.NewLine}" + + $"persistentDataPath: {Application.persistentDataPath}{Environment.NewLine}" + + $"----------------------------------------{Environment.NewLine}"); + } + catch (Exception exception) + { + _sessionLogFilePath = null; + Debug.LogWarning($"[IAA Sample] Failed to initialize log file: {exception.Message}"); + } + } + + private void TryAppendLineToFile(string line) + { + if (!autoWriteLogsToFile || string.IsNullOrWhiteSpace(_sessionLogFilePath)) + { + return; + } + + try + { + File.AppendAllText(_sessionLogFilePath, line + Environment.NewLine); + } + catch (Exception exception) + { + Debug.LogWarning($"[IAA Sample] Failed to append log file: {exception.Message}"); + } + } + + private void ExportVisibleLogsToFile() + { + try + { + if (string.IsNullOrWhiteSpace(_sessionLogDirectory)) + { + InitializeLogFile(); + } + + var exportPath = Path.Combine( + _sessionLogDirectory ?? Application.persistentDataPath, + $"iaa-ad-debug-export-{DateTime.Now:yyyyMMdd-HHmmss}.log"); + File.WriteAllLines(exportPath, _logs); + AppendLog($"Exported {_logs.Count} visible log lines to file: {exportPath}"); + } + catch (Exception exception) + { + AppendLog($"Export log file failed: {exception.Message}"); + } + } + + private static string DisplayValue(string value) + { + return string.IsNullOrWhiteSpace(value) ? "" : value; + } + + private void EnsureStyles() + { + if (_stylesInitialized) + { + return; + } + + _stylesInitialized = true; + _titleStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 54, + fontStyle = FontStyle.Bold, + alignment = TextAnchor.MiddleCenter, + wordWrap = true + }; + _sectionStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 34, + fontStyle = FontStyle.Bold, + wordWrap = true + }; + _textStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 28, + wordWrap = true + }; + _buttonStyle = new GUIStyle(GUI.skin.button) + { + fontSize = 28, + wordWrap = true + }; + _textFieldStyle = new GUIStyle(GUI.skin.textField) + { + fontSize = 28 + }; + _logStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 24, + wordWrap = true, + richText = false + }; + } +} diff --git a/Assets/Topon_Adapter/Runtime/Samples/IAAAdDebugSampleGui.cs.meta b/Assets/Topon_Adapter/Runtime/Samples/IAAAdDebugSampleGui.cs.meta new file mode 100644 index 0000000..9a8a894 --- /dev/null +++ b/Assets/Topon_Adapter/Runtime/Samples/IAAAdDebugSampleGui.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53b297b9bf0d4cfca2b77af0fd74c95c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Topon_Adapter/Runtime/Scripts/AwardVideoPlayer.cs b/Assets/Topon_Adapter/Runtime/Scripts/AwardVideoPlayer.cs index 2eb40b5..a439f3d 100644 --- a/Assets/Topon_Adapter/Runtime/Scripts/AwardVideoPlayer.cs +++ b/Assets/Topon_Adapter/Runtime/Scripts/AwardVideoPlayer.cs @@ -7,12 +7,15 @@ using UnityEngine; public class AwardVideoPlayer : ADPlayer , ATRewardedVideoListener { private ATRewardedVideo _atRewardedVideo; + private ATRewardedAutoVideo _atRewardedAutoVideo; private Action _onVideoComplete; private ADListenerAggregator _aggregator; + private bool _autoLoadRegistered; public override void OnInit() { this._atRewardedVideo = ATRewardedVideo.Instance; + this._atRewardedAutoVideo = ATRewardedAutoVideo.Instance; // this._atRewardedVideo.client.setListener(this); //由于新版本广告sdk弃用该方式,通过以下方式重新桥接监听事件 this._aggregator = new ADListenerAggregator(); this._aggregator.BindAwardVideoListener(this._atRewardedVideo.client , this); @@ -30,7 +33,15 @@ public class AwardVideoPlayer : ADPlayer , ATRewardedVideoListener this.adListener.onClose = onClose; this.adListener.onVideoComplete = this.OnVideoComplete; var json = new Dictionary { { AnyThinkAds.Api.ATConst.SCENARIO, this.AdScene } }; - this._atRewardedVideo.showAd(this.Key , json); + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atRewardedAutoVideo.showAutoAd(this.Key, json); + } + else + { + this._atRewardedVideo.showAd(this.Key , json); + } } } @@ -42,18 +53,54 @@ public class AwardVideoPlayer : ADPlayer , ATRewardedVideoListener public override bool IsReadly() { - return this.curState == 2; + if (UseAutoLoad()) + { + if (this._atRewardedAutoVideo != null && + this._atRewardedAutoVideo.autoLoadRewardedVideoReadyForPlacementID(this.Key)) + { + this.curState = 2; + return true; + } + + return this.curState == 2; + } + + if (this.curState == 2) + { + return true; + } + + if (this._atRewardedVideo != null && this._atRewardedVideo.hasAdReady(this.Key)) + { + this.curState = 2; + return true; + } + + return false; } public override void LoadAD() { if (curState == 0) { + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atRewardedAutoVideo.setAutoLocalExtra(this.Key, BuildRewardedExtra()); + this.curState = this._atRewardedAutoVideo.autoLoadRewardedVideoReadyForPlacementID(this.Key) ? 2 : 1; + return; + } + + if (this._atRewardedVideo != null && this._atRewardedVideo.hasAdReady(this.Key)) + { + this.curState = 2; + return; + } + { Dictionary jsonmap = new Dictionary(); //ATConst.USERID_KEY必传,用于标识每个用户;ATConst.USER_EXTRA_DATA为可选参数,传入后将透传到开发者的服务器 - jsonmap.Add(ATConst.USERID_KEY, ADManager.Instance.UserId); - jsonmap.Add(ATConst.USER_EXTRA_DATA, "user_extra_data"); + jsonmap = BuildRewardedExtra(); curState = 1; this._atRewardedVideo.loadVideoAd(this.Key, jsonmap); } @@ -130,5 +177,63 @@ public class AwardVideoPlayer : ADPlayer , ATRewardedVideoListener } #endregion + + public override void OnPlayRequestStarted() + { + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atRewardedAutoVideo?.setAutoLocalExtra(this.Key, BuildRewardedExtra()); + } + } + + public override int MaxLoadAttempts => ToponAdController.CurrentOptions?.RewardedMaxLoadAttempts ?? base.MaxLoadAttempts; + + public override float LoadRetryDelaySeconds => + Math.Max(0f, (ToponAdController.CurrentOptions?.RewardedLoadRetryDelayMs ?? 750) / 1000f); + + public override bool AutoPreloadOnInit => ToponAdController.CurrentOptions?.RewardedPrewarmOnInit ?? true; + + private bool UseAutoLoad() + { + return ToponAdController.CurrentOptions?.RewardedAutoLoad ?? true; + } + + private void EnsureAutoLoadRegistered() + { + if (_autoLoadRegistered) + { + return; + } + + this._atRewardedAutoVideo?.addAutoLoadAdPlacementID(new[] { this.Key }); + _autoLoadRegistered = true; + } + + private Dictionary BuildRewardedExtra() + { + return new Dictionary + { + { ATConst.USERID_KEY, ADManager.Instance.UserId }, + { ATConst.USER_EXTRA_DATA, "user_extra_data" } + }; + } + + public override void EnterAdScenario(string scenario) + { + if (string.IsNullOrWhiteSpace(scenario) || string.Equals(scenario, "__default__", StringComparison.Ordinal)) + { + return; + } + + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atRewardedAutoVideo?.entryAutoAdScenarioWithPlacementID(this.Key, scenario); + return; + } + + this._atRewardedVideo?.entryScenarioWithPlacementID(this.Key, scenario); + } -} \ No newline at end of file +} diff --git a/Assets/Topon_Adapter/Runtime/Scripts/InteractionPlayer.cs b/Assets/Topon_Adapter/Runtime/Scripts/InteractionPlayer.cs index 06d8567..dfd3787 100644 --- a/Assets/Topon_Adapter/Runtime/Scripts/InteractionPlayer.cs +++ b/Assets/Topon_Adapter/Runtime/Scripts/InteractionPlayer.cs @@ -7,28 +7,83 @@ using UnityEngine; public class InteractionPlayer : ADPlayer , ATInterstitialAdListener { private ATInterstitialAd _atInterstitialAd; + private ATInterstitialAutoAd _atInterstitialAutoAd; private ADListenerAggregator _aggregator; + private bool _autoLoadRegistered; public override void OnInit() { this._atInterstitialAd = ATInterstitialAd.Instance; + this._atInterstitialAutoAd = ATInterstitialAutoAd.Instance; this._aggregator = new ADListenerAggregator(); this._aggregator.BindInterstitialAdListener(this._atInterstitialAd.client,this); } public override void ShowAD(Action onClose, Action onVideoComplete) { - if (curState == 2) + if (this.IsReadly()) { - ATInterstitialAd.Instance.showInterstitialAd(this.Key); + this.adListener.onClose = onClose; + this.adListener.onVideoComplete = onVideoComplete; + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atInterstitialAutoAd.showAutoAd(this.Key, BuildInterstitialExtra()); + } + else + { + ATInterstitialAd.Instance.showInterstitialAd(this.Key); + } curState = 0; } } + + public override bool IsReadly() + { + if (this.curState == 2) + { + return true; + } + + if (UseAutoLoad()) + { + if (this._atInterstitialAutoAd != null && + this._atInterstitialAutoAd.autoLoadInterstitialAdReadyForPlacementID(this.Key)) + { + this.curState = 2; + return true; + } + + return false; + } + + if (this._atInterstitialAd != null && this._atInterstitialAd.hasInterstitialAdReady(this.Key)) + { + this.curState = 2; + return true; + } + + return false; + } public override void LoadAD() { if (curState == 0) { + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atInterstitialAutoAd.setAutoLocalExtra(this.Key, BuildInterstitialExtra()); + this.curState = this._atInterstitialAutoAd.autoLoadInterstitialAdReadyForPlacementID(this.Key) ? 2 : 1; + return; + } + + if (this._atInterstitialAd != null && this._atInterstitialAd.hasInterstitialAdReady(this.Key)) + { + this.curState = 2; + return; + } + { //加载广告 Dictionary jsonmap = new Dictionary(); @@ -61,6 +116,8 @@ public class InteractionPlayer : ADPlayer , ATInterstitialAdListener public void onInterstitialAdFailedToShow(string placementId) { + this.curState = 0; + this.adListener.OnShowError(); } public void onInterstitialAdClose(string placementId, ATCallbackInfo callbackInfo) @@ -83,6 +140,8 @@ public class InteractionPlayer : ADPlayer , ATInterstitialAdListener public void onInterstitialAdFailedToPlayVideo(string placementId, string code, string message) { + this.curState = 0; + this.adListener.OnShowError(); } public void startLoadingADSource(string placementId, ATCallbackInfo callbackInfo) @@ -112,4 +171,61 @@ public class InteractionPlayer : ADPlayer , ATInterstitialAdListener } #endregion -} \ No newline at end of file + + public override void OnPlayRequestStarted() + { + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atInterstitialAutoAd?.setAutoLocalExtra(this.Key, BuildInterstitialExtra()); + } + } + + public override int MaxLoadAttempts => ToponAdController.CurrentOptions?.InterstitialMaxLoadAttempts ?? base.MaxLoadAttempts; + + public override float LoadRetryDelaySeconds => + Math.Max(0f, (ToponAdController.CurrentOptions?.InterstitialLoadRetryDelayMs ?? 500) / 1000f); + + public override bool AutoPreloadOnInit => ToponAdController.CurrentOptions?.InterstitialPrewarmOnInit ?? true; + + private bool UseAutoLoad() + { + return ToponAdController.CurrentOptions?.InterstitialAutoLoad ?? true; + } + + private void EnsureAutoLoadRegistered() + { + if (_autoLoadRegistered) + { + return; + } + + this._atInterstitialAutoAd?.addAutoLoadAdPlacementID(new[] { this.Key }); + _autoLoadRegistered = true; + } + + private Dictionary BuildInterstitialExtra() + { + return new Dictionary + { + { ATConst.SCENARIO, this.AdScene ?? string.Empty } + }; + } + + public override void EnterAdScenario(string scenario) + { + if (string.IsNullOrWhiteSpace(scenario) || string.Equals(scenario, "__default__", StringComparison.Ordinal)) + { + return; + } + + if (UseAutoLoad()) + { + EnsureAutoLoadRegistered(); + this._atInterstitialAutoAd?.entryAutoAdScenarioWithPlacementID(this.Key, scenario); + return; + } + + this._atInterstitialAd?.entryScenarioWithPlacementID(this.Key, scenario); + } +} diff --git a/Assets/Topon_Adapter/Runtime/Scripts/ToponAdController.cs b/Assets/Topon_Adapter/Runtime/Scripts/ToponAdController.cs index 449dd4d..782bdad 100644 --- a/Assets/Topon_Adapter/Runtime/Scripts/ToponAdController.cs +++ b/Assets/Topon_Adapter/Runtime/Scripts/ToponAdController.cs @@ -7,6 +7,7 @@ public class ToponAdController : IAdController { public static string LastDetectedArea { get; private set; } public static string LastAreaError { get; private set; } + public static ToponControllerOptions CurrentOptions { get; private set; } private Action _maskAction; private Action _logEventAction; @@ -20,14 +21,16 @@ public class ToponAdController : IAdController _adConfig = adConfig; _controllerOptions = ToponControllerOptions.Resolve(adConfig, args); + CurrentOptions = _controllerOptions; ApplyPreInitOptions(_controllerOptions); var isDebug = _controllerOptions.Debug ?? false; ATSDKAPI.setLogDebug(isDebug); ATSDKAPI.initSDK(adConfig.Id , adConfig.Key); + StartChinaMainlandSdkIfNeeded(); ApplyPostInitOptions(_controllerOptions); - if (isDebug) + if (_controllerOptions.AutoOpenDebuggerUI) { ShowAndroidTest (); } @@ -103,6 +106,11 @@ public class ToponAdController : IAdController return; } + if (!string.IsNullOrWhiteSpace(options.LocalStrategyAssetPath)) + { + ATSDKAPI.setLocalStrategyAssetPath(options.LocalStrategyAssetPath); + } + if (options.InitCustomMap != null && options.InitCustomMap.Count > 0) { ATSDKAPI.initCustomMap(options.InitCustomMap); @@ -163,6 +171,26 @@ public class ToponAdController : IAdController } } + private static void StartChinaMainlandSdkIfNeeded() + { +#if UNITY_ANDROID && !UNITY_EDITOR + try + { + if (!ATSDKAPI.isCnSDK()) + { + return; + } + + ATSDKAPI.start(); + Debug.Log("[Topon] Called ATSDK.start() for China mainland SDK."); + } + catch (Exception exception) + { + Debug.LogWarning($"[Topon] Failed to call ATSDK.start(): {exception.Message}"); + } +#endif + } + private sealed class ToponAreaListener : ATGetAreaListener { private readonly ToponAdController _controller; diff --git a/Assets/Topon_Adapter/Runtime/Scripts/ToponControllerOptions.cs b/Assets/Topon_Adapter/Runtime/Scripts/ToponControllerOptions.cs index 9ac57b1..90dd5ab 100644 --- a/Assets/Topon_Adapter/Runtime/Scripts/ToponControllerOptions.cs +++ b/Assets/Topon_Adapter/Runtime/Scripts/ToponControllerOptions.cs @@ -11,6 +11,7 @@ public sealed class ToponControllerOptions public const string SubChannelKey = "topon.sub_channel"; public const string DebugKey = "topon.debug"; public const string DebuggerKeyKey = "topon.debugger_key"; + public const string AutoOpenDebuggerUiKey = "topon.auto_open_debugger_ui"; public const string SDKAreaKey = "topon.sdk_area"; public const string LongitudeKey = "topon.longitude"; public const string LatitudeKey = "topon.latitude"; @@ -19,11 +20,21 @@ public sealed class ToponControllerOptions public const string QueryAreaOnInitKey = "topon.query_area_on_init"; public const string InitCustomMapKey = "topon.custom_map"; public const string RewardedCustomDataKey = "topon.rewarded_custom_data"; + public const string RewardedAutoLoadKey = "topon.rewarded_auto_load"; + public const string RewardedPrewarmOnInitKey = "topon.rewarded_prewarm_on_init"; + public const string RewardedMaxLoadAttemptsKey = "topon.rewarded_max_load_attempts"; + public const string RewardedLoadRetryDelayMsKey = "topon.rewarded_load_retry_delay_ms"; + public const string InterstitialAutoLoadKey = "topon.interstitial_auto_load"; + public const string InterstitialPrewarmOnInitKey = "topon.interstitial_prewarm_on_init"; + public const string InterstitialMaxLoadAttemptsKey = "topon.interstitial_max_load_attempts"; + public const string InterstitialLoadRetryDelayMsKey = "topon.interstitial_load_retry_delay_ms"; + public const string LocalStrategyAssetPathKey = "topon.local_strategy_asset_path"; public string Channel { get; set; } public string SubChannel { get; set; } public bool? Debug { get; set; } public string DebuggerKey { get; set; } + public bool AutoOpenDebuggerUI { get; set; } public int? SDKArea { get; set; } public double? Longitude { get; set; } public double? Latitude { get; set; } @@ -34,6 +45,15 @@ public sealed class ToponControllerOptions public Dictionary RewardedCustomData { get; set; } public Action OnAreaReceived { get; set; } public Action OnAreaError { get; set; } + public bool RewardedAutoLoad { get; set; } = true; + public bool RewardedPrewarmOnInit { get; set; } = true; + public int RewardedMaxLoadAttempts { get; set; } = 3; + public int RewardedLoadRetryDelayMs { get; set; } = 1000; + public bool InterstitialAutoLoad { get; set; } = true; + public bool InterstitialPrewarmOnInit { get; set; } = true; + public int InterstitialMaxLoadAttempts { get; set; } = 2; + public int InterstitialLoadRetryDelayMs { get; set; } = 500; + public string LocalStrategyAssetPath { get; set; } public static ToponControllerOptions Resolve(ADConfig adConfig, object[] args) { @@ -85,6 +105,7 @@ public sealed class ToponControllerOptions SubChannel = GetString(map, SubChannelKey, "sub_channel") ?? SubChannel; Debug = GetBool(map, DebugKey, "debug") ?? Debug; DebuggerKey = GetString(map, DebuggerKeyKey) ?? DebuggerKey; + AutoOpenDebuggerUI = GetBool(map, AutoOpenDebuggerUiKey) ?? AutoOpenDebuggerUI; SDKArea = GetInt(map, SDKAreaKey) ?? SDKArea; Longitude = GetDouble(map, LongitudeKey) ?? Longitude; Latitude = GetDouble(map, LatitudeKey) ?? Latitude; @@ -93,6 +114,15 @@ public sealed class ToponControllerOptions QueryAreaOnInit = GetBool(map, QueryAreaOnInitKey) ?? QueryAreaOnInit; InitCustomMap = MergeMaps(InitCustomMap, GetPrefixedMap(map, InitCustomMapKey + ".")); RewardedCustomData = MergeMaps(RewardedCustomData, GetPrefixedMap(map, RewardedCustomDataKey + ".")); + RewardedAutoLoad = GetBool(map, RewardedAutoLoadKey) ?? RewardedAutoLoad; + RewardedPrewarmOnInit = GetBool(map, RewardedPrewarmOnInitKey) ?? RewardedPrewarmOnInit; + RewardedMaxLoadAttempts = GetInt(map, RewardedMaxLoadAttemptsKey) ?? RewardedMaxLoadAttempts; + RewardedLoadRetryDelayMs = GetInt(map, RewardedLoadRetryDelayMsKey) ?? RewardedLoadRetryDelayMs; + InterstitialAutoLoad = GetBool(map, InterstitialAutoLoadKey) ?? InterstitialAutoLoad; + InterstitialPrewarmOnInit = GetBool(map, InterstitialPrewarmOnInitKey) ?? InterstitialPrewarmOnInit; + InterstitialMaxLoadAttempts = GetInt(map, InterstitialMaxLoadAttemptsKey) ?? InterstitialMaxLoadAttempts; + InterstitialLoadRetryDelayMs = GetInt(map, InterstitialLoadRetryDelayMsKey) ?? InterstitialLoadRetryDelayMs; + LocalStrategyAssetPath = GetString(map, LocalStrategyAssetPathKey) ?? LocalStrategyAssetPath; } private void ApplyLegacyArgs(object[] args) @@ -124,6 +154,7 @@ public sealed class ToponControllerOptions SubChannel = explicitOptions.SubChannel ?? SubChannel; Debug = explicitOptions.Debug ?? Debug; DebuggerKey = explicitOptions.DebuggerKey ?? DebuggerKey; + AutoOpenDebuggerUI = explicitOptions.AutoOpenDebuggerUI || AutoOpenDebuggerUI; SDKArea = explicitOptions.SDKArea ?? SDKArea; Longitude = explicitOptions.Longitude ?? Longitude; Latitude = explicitOptions.Latitude ?? Latitude; @@ -174,6 +205,7 @@ public sealed class ToponControllerOptions SubChannel = GetString(map, SubChannelKey, "sub_channel") ?? SubChannel; Debug = GetBool(map, DebugKey, "debug") ?? Debug; DebuggerKey = GetString(map, DebuggerKeyKey) ?? DebuggerKey; + AutoOpenDebuggerUI = GetBool(map, AutoOpenDebuggerUiKey) ?? AutoOpenDebuggerUI; SDKArea = GetInt(map, SDKAreaKey) ?? SDKArea; Longitude = GetDouble(map, LongitudeKey) ?? Longitude; Latitude = GetDouble(map, LatitudeKey) ?? Latitude; @@ -182,6 +214,15 @@ public sealed class ToponControllerOptions QueryAreaOnInit = GetBool(map, QueryAreaOnInitKey) ?? QueryAreaOnInit; InitCustomMap = MergeMaps(InitCustomMap, GetPrefixedMap(map, InitCustomMapKey + ".")); RewardedCustomData = MergeMaps(RewardedCustomData, GetPrefixedMap(map, RewardedCustomDataKey + ".")); + RewardedAutoLoad = GetBool(map, RewardedAutoLoadKey) ?? RewardedAutoLoad; + RewardedPrewarmOnInit = GetBool(map, RewardedPrewarmOnInitKey) ?? RewardedPrewarmOnInit; + RewardedMaxLoadAttempts = GetInt(map, RewardedMaxLoadAttemptsKey) ?? RewardedMaxLoadAttempts; + RewardedLoadRetryDelayMs = GetInt(map, RewardedLoadRetryDelayMsKey) ?? RewardedLoadRetryDelayMs; + InterstitialAutoLoad = GetBool(map, InterstitialAutoLoadKey) ?? InterstitialAutoLoad; + InterstitialPrewarmOnInit = GetBool(map, InterstitialPrewarmOnInitKey) ?? InterstitialPrewarmOnInit; + InterstitialMaxLoadAttempts = GetInt(map, InterstitialMaxLoadAttemptsKey) ?? InterstitialMaxLoadAttempts; + InterstitialLoadRetryDelayMs = GetInt(map, InterstitialLoadRetryDelayMsKey) ?? InterstitialLoadRetryDelayMs; + LocalStrategyAssetPath = GetString(map, LocalStrategyAssetPathKey) ?? LocalStrategyAssetPath; } private static string GetString(IDictionary map, params string[] keys) diff --git a/Assets/package.json b/Assets/package.json index 6969eac..d9db7be 100644 --- a/Assets/package.json +++ b/Assets/package.json @@ -2,7 +2,7 @@ "name": "com.commercialization.topon", "displayName": "Commercialization.topon", "description": "基于topon的广告sdk封装,依赖基础商业化模块", - "version": "1.4.6", + "version": "1.4.7", "unity": "2021.1", "license": "MIT", "repository": { @@ -16,7 +16,7 @@ }, "dependencies": { - "com.foldcc.cc-framework.commercialization" : "http://private.lightyears.ltd:18640/foldcc/CC-Framework.Commercialization.git#1.0.5" + "com.foldcc.cc-framework.commercialization" : "http://private.lightyears.ltd:18640/foldcc/CC-Framework.Commercialization.git#1.0.12" }, "keywords": [ "Framework" diff --git a/IAA广告架构重构实施指南.md b/IAA广告架构重构实施指南.md new file mode 100644 index 0000000..c8793d7 --- /dev/null +++ b/IAA广告架构重构实施指南.md @@ -0,0 +1,613 @@ +# IAA广告架构重构实施指南 + +## 1. 目标 + +本文档的目标不是讨论“架构是否优雅”,而是以 **IAA 收益最大化** 为第一原则,对当前广告接入架构进行诊断、排序和重构规划。 + +核心目标: + +1. 提高激励视频和插屏的首播成功率、Ready Rate、Show Rate、Reward Rate。 +2. 将当前旧版 `load/show/listener` 心智升级为新版 Taku/TopOn 的 **生命周期 + 自动加载 + 场景统计 + 状态查询 + 策略化调度**。 +3. 保留业务层统一入口 `ADManager`,但重做中下层能力和策略层。 +4. 把当前“联调环境”和“实际发布包依赖”统一起来,避免版本漂移导致的收益判断失真。 + +--- + +## 2. 当前架构快照 + +### 2.1 运行时主链路 + +```mermaid +flowchart LR + A["业务层\nADManager.AsyncPlayAD"] --> B["商业化抽象层\nADPlayer / AsyncAdPlayer"] + B --> C["平台实现层\nToponAdController / AwardVideoPlayer / InteractionPlayer"] + C --> D["Unity桥\nATSDKAPI / Rewarded / Interstitial"] + D --> E["原生桥\nSDKInitHelper / VideoHelper / VideoAutoAdHelper"] + E --> F["Taku/TopOn 原生 SDK\n微信 / 抖音 / 小程序 / 下载链路"] +``` + +### 2.2 当前仓库角色 + +- 抽象层本地源码:`Packages/CC-Framework.Commercialization` +- 当前平台实现层:`Assets/Topon_Adapter` +- 当前官方 Unity 插件:`Assets/AnyThinkPlugin` +- 当前 Android 额外插件与资源:`Assets/Plugins/Android` + +### 2.3 当前已知版本状态 + +- 本地抽象层版本:`1.0.12` + - 文件:[Packages/CC-Framework.Commercialization/Assets/package.json](F:/UnityWork/Commercialization.topon/Packages/CC-Framework.Commercialization/Assets/package.json) +- 当前 Topon 包版本:`1.4.7` + - 文件:[Assets/package.json](F:/UnityWork/Commercialization.topon/Assets/package.json) +- 当前 Topon 包对抽象层的发布依赖是:`1.0.12` + - 文件同上 +- 当前联调工程和发布依赖已对齐到同一抽象层版本,版本漂移问题已收敛。 +- Android 接入侧需注意: + - 当前中国区 Gromore/CSJ 组合要求 `minSdkVersion >= 24` + +--- + +## 3. 术语速查 + +### 3.1 Placement(广告位) + +- 一个 placement 就是一个固定广告机会点的唯一 ID。 +- 例如: + - 复活激励 + - 双倍奖励激励 + - 关卡结尾插屏 + +### 3.2 Scenario(广告场景) + +- placement 是固定广告位,scenario 是这次广告触发的业务上下文。 +- 例如: + - `revive` + - `double_coin` + - `level_fail_exit` +- 它主要用于: + - 漏斗分析 + - 场景收益对比 + - 定位具体场景的展示问题 + +### 3.3 load / ready / show + +- `load`:发起请求并缓存广告 +- `ready`:当前已有可展示广告缓存 +- `show`:真正展示广告 + +### 3.4 Auto Load(自动加载) + +- SDK 自动维护某个 placement 的缓存和补量 +- 更适合高频高价值激励位 + +### 3.5 Waterfall(瀑布流) + +- 按优先级依次请求广告源 +- 优先级和广告源配置主要在后台控制 + +### 3.6 Bidding(竞价) + +- 广告源实时回传价格 +- 平台再选择最优广告展示 + +### 3.7 eCPM + +- 每千次展示收益 +- 是重要指标,但不能脱离: + - Ready Rate + - Show Rate + - Fill Rate + - Reward Completion + 单独看 + +### 3.8 Fill Rate(填充率) + +- 发起请求后,最终拿到可展示广告的比例 + +### 3.9 Show Rate(展示率) + +- 已到达广告场景或已请求后,最终真正展示成功的比例 + +### 3.10 Impression(展示) + +- 广告真正展示给用户一次 +- 收益通常基于展示结算,而不是基于请求结算 + +### 3.11 Report API + +- 广告平台收益与展示数据接口 +- 用于自动价格、收益归因、Waterfall 对账和优化 + +### 3.12 Local Extra / Custom Map + +- `local extra`:请求级参数,通常带用户透传、业务信息 +- `custom map`:初始化级参数,通常带渠道、用户分组、实验标记 + +### 3.13 为什么“预加载影响 eCPM”是旧命题 + +- 旧时代常说“不要太早 load,会伤 eCPM” +- 现在更准确的说法是: + - 问题不在预加载本身 + - 问题在于是否 **策略化预热** +- 真正影响收益的是: + - 预热时机不对 + - placement 太多 + - 长时间不展示 + - 后台策略和客户端行为脱节 + +--- + +## 4. 官方推荐生命周期 + +### 4.1 推荐调用顺序 + +```mermaid +sequenceDiagram + participant App as App启动 + participant Privacy as 隐私同意 + participant SDK as Taku SDK + participant Policy as 客户端策略层 + participant Game as 广告业务逻辑 + + App->>Privacy: 展示隐私协议 + Privacy-->>App: 用户同意 + App->>SDK: ATSDK.init(...) + App->>SDK: ATSDK.start()(中国区) + App->>Policy: 注册核心placement自动加载/预热 + Game->>Policy: 到达广告场景 entryScenario + Policy->>SDK: ready/status 检查 + alt 已ready + Policy->>SDK: show + else 未ready但在容错窗口 + Policy->>SDK: 短等待 / 补偿重试 + else 超时 + Policy-->>Game: 播放失败 + end +``` + +### 4.2 推荐自动调用的动作 + +这些动作建议由底层 provider 自动做掉,而不是依赖业务层自己记忆: + +1. 中国区 `ATSDK.start()` +2. 核心激励位注册 auto load +3. 启动后预热高价值 placement +4. 核心 placement 的诊断日志与状态快照 +5. `show start` 后安排下一轮预热 +6. 首轮失败进入短重试窗口 + +### 4.3 业务层不应该感知的内容 + +以下内容应由策略层 / 平台层封装: + +1. placement 是否自动加载 +2. 何时预热 +3. 是否可以等待更高价缓存 +4. 冷启动是否容许重试 +5. 微信/抖音/小程序/下载的具体差异 + +--- + +## 5.1 当前已接入的策略 Key(第二阶段) + +目前已在 `ToponControllerOptions` 中接入的高收益策略 key: + +- `topon.rewarded_auto_load` + - 是否开启激励自动加载 + - 当前建议默认:`true` +- `topon.rewarded_prewarm_on_init` + - 初始化后是否自动预热激励位 + - 当前建议默认:`true` +- `topon.rewarded_max_load_attempts` + - 单次播放请求允许的最大加载尝试次数 + - 当前建议默认:`3` +- `topon.rewarded_load_retry_delay_ms` + - 激励首次失败后的短补偿等待时间 + - 当前建议默认:`1000` +- `topon.interstitial_auto_load` + - 是否开启插屏自动加载 + - 当前建议默认:`true` +- `topon.interstitial_prewarm_on_init` + - 初始化后是否自动预热插屏位 + - 当前建议默认:`true` +- `topon.interstitial_max_load_attempts` + - 单次插屏播放请求允许的最大加载尝试次数 + - 当前建议默认:`2` +- `topon.interstitial_load_retry_delay_ms` + - 插屏首轮失败后的短补偿等待时间 + - 当前建议默认:`500` +- `topon.local_strategy_asset_path` + - Android 本地策略资源路径 +- `topon.auto_open_debugger_ui` + - 是否在初始化后自动打开官方 DebugUI + - 当前建议默认:`false` + +适用方式: + +- 通过 `ADConfig.CommonKeyValues` +- 或通过 `ToponControllerOptions` +- 或额外传入 `IDictionary` + +推荐默认示例: + +```text +topon.rewarded_auto_load=true +topon.rewarded_prewarm_on_init=true +topon.rewarded_max_load_attempts=3 +topon.rewarded_load_retry_delay_ms=1000 +topon.interstitial_auto_load=true +topon.interstitial_prewarm_on_init=true +topon.interstitial_max_load_attempts=2 +topon.interstitial_load_retry_delay_ms=500 +topon.auto_open_debugger_ui=false +``` + +调试说明: + +- `topon.debug=true` + - 当前只用于开启 SDK 调试日志 + - 不再默认自动弹出官方 DebugUI +- 官方 DebugUI 建议在真机样例或专用调试入口中手动打开 + +--- + +## 5. 官方最新 SDK 能力,与当前项目的差距 + +结合官方文档和当前插件/反编译结果,当前 Taku/TopOn 体系已经具备以下关键能力,但当前项目没有充分利用: + +### 3.1 `ATSDK.start()` + +- 官方中国区 SDK 初始化文档要求 `ATSDK.init(...)` 后建议调用 `ATSDK.start()`。 +- 反编译验证:`ATSDK.start()` 在当前 `anythink_core` 中真实存在,且是无参静态方法。 +- 当前缺陷: + - `ToponAdController` 只调了 `ATSDKAPI.initSDK(...)` + - Unity 壳 `ATSDKAPI.cs` 没有暴露 `start` + +### 3.2 自动加载能力 + +- 已有能力: + - `ATRewardedAutoVideo` + - `ATInterstitialAutoAd` +- 当前缺陷: + - 当前 `AwardVideoPlayer` / `InteractionPlayer` 仍是手工状态机模式 + - 没有利用自动加载做高频 placement 的持续预热 + +### 3.3 状态查询能力 + +- 已有能力: + - `hasAdReady` + - `checkAdStatus` + - `getValidAdCaches` +- 当前缺陷: + - 未接入策略层 + - 无法做“软等待”“缓存质量判断”“首轮失败补偿” + +### 3.4 广告场景统计能力 + +- 官方推荐使用 `entryAdScenario` 做漏斗分析 +- 当前缺陷: + - `adScene` 只在 `show` 阶段透传 + - 没有将“用户到达广告机会点”单独建模 + +### 3.5 本地策略能力 + +- 官方原生 SDK 有 `ATSDK.setLocalStrategyAssetPath(...)` +- 当前缺陷: + - Unity 壳没暴露 + - 客户端无法加载本地预置策略 + +### 3.6 流量分组 / 规则能力 + +- 当前已接的弱能力: + - `channel / sub_channel` + - `custom_map` + - `placement custom data` +- 当前缺陷: + - 只是透传,不是可运营的“收益策略层” + +--- + +## 6. 现有问题,按收入影响从大到小排序 + +### P0. 首播请求容错太差,直接损失收入 + +表现: + +- 激励视频第一次点击经常失败 +- 冷启动期首轮加载不稳定 + +根因: + +- `AsyncAdPlayer` 仍是“单次播放只给一次首轮 load 机会” +- 首次 `load` 比后续 `load` 多做了一层 `VideoHelper / ATRewardVideoAd` 初始化 +- 冷启动期没有任何“延迟补偿 / 软等待 / 重试窗口” + +影响: + +- 直接损失首播展示 +- 高价值激励点收益打折 +- 拉低用户广告可接受度和视频完成率 + +### P1. 高价值 placement 没有自动加载,Ready Rate 偏低 + +表现: + +- 核心激励位依赖点击时加载 +- 就绪完全由手工 load 时机决定 + +影响: + +- 展示机会点到来时,经常没 ready +- 收入天花板偏低 + +### P2. 没有完整接入场景统计,无法做精细化收益调优 + +影响: + +- 无法准确判断问题在“场景设计”还是“缓存策略” +- 客户端和后台漏斗对不上 + +### P3. 生命周期抽象过粗,不适配新版 SDK + +表现: + +- `init` 被调用 != 广告系统真正 ready +- 当前实现无法表达 `start`、本地策略、自动加载启动等生命周期 + +### P4. 插屏回调链本身存在缺陷 + +表现: + +- `InteractionPlayer.ShowAD(...)` 没接 `onClose/onVideoComplete` +- `onInterstitialAdFailedToShow` 等回调是空实现 + +影响: + +- 插屏统计、重试、展示完成事件不可靠 + +### P5. 版本漂移导致实验结果不可信 + +表现: + +- 本地抽象层、联调工程、发布依赖三个版本不统一 + +影响: + +- 本地验证表现无法可信映射到业务项目 + +--- + +## 7. 对“预加载影响 eCPM”的判断 + +这是一个 **旧时代常见结论,但今天已经不能直接这么说**。 + +更准确的说法是: + +- **无脑预加载本身不是收益最优策略** +- 但“预加载会直接伤 eCPM”这个说法过于粗暴,已经过时 + +当前更合理的理解: + +1. 预加载是 IAA 的基础能力,不做高价值位预热,收入上限一定低。 +2. 问题不在“要不要预加载”,而在“何时预加载、预加载多少、多久不展示视为无效”。 +3. 真正会伤收益的不是“有预加载”,而是: + - 预热过早 + - 长时间不展示 + - placement 太多导致缓存浪费 + - 高价值场景没有拿到新鲜缓存 + - 客户端策略和后台 Waterfall 配置脱节 + +最优结论: + +- **高价值高频位**:必须预热,甚至自动加载 +- **中频位**:场景前置预热 +- **低频位**:Just-in-time load + 短等待容错 +- **插屏**:策略化预热,不建议全局常驻缓存所有位 + +--- + +## 8. 最优指南:面向 IAA 的广告策略 + +### 6.1 激励视频 + +推荐策略: + +1. 核心激励位默认使用自动加载 +2. 启动后预热 +3. 用户进入关键广告场景时调用 `entryAdScenario` +4. 播放前先读 `ready/status` +5. 首次失败给短重试窗口,不要直接判死 +6. `onRewardedVideoAdPlayStart` 后立即安排下一轮预热 + +### 6.2 插屏 + +推荐策略: + +1. 重要插屏位可使用自动加载 +2. 低频插屏采用场景前置预热 +3. 插屏失败要完整上报 show fail 和 close 结果 +4. 插屏不应该沿用激励的奖励回调模型 + +### 6.3 后台协同 + +推荐策略: + +1. 开启报表 API +2. 配置自动价格 +3. 做流量分组 +4. 用 A/B 测试验证: + - 自动加载 vs 手工加载 + - 启动预热 vs 场景预热 + - 软等待 0/300/800ms + +### 8.4 常见产品策略方式 + +#### 策略 A:核心激励常驻预热 + +- 适用: + - 复活激励 + - 双倍奖励激励 +- 特点: + - 就绪率高 + - 收入稳定 + +#### 策略 B:场景前置预热 + +- 适用: + - 结算页激励 + - 阶段性插屏 +- 特点: + - 更省请求 + - 更接近真实展示时机 + +#### 策略 C:点击兜底加载 + 软等待 + +- 适用: + - 低频长尾位 +- 特点: + - 节省资源 + - 不适合核心高价值激励位 + +#### 策略 D:自动加载 + 场景感知展示 + +- 适用: + - 高频核心激励 +- 特点: + - 是当前推荐的主流 IAA 策略 + - 平衡 ready rate 和展示体验 + +#### 策略 E:更高价广告软等待 + +- 适用: + - 激励价值极高的场景 +- 特点: + - 可能提高单次展示收益 +- 风险: + - 等待过久伤转化 + - 必须通过 A/B 测试验证 + +--- + +## 9. 推荐的新架构 + +```mermaid +flowchart LR + A["业务层\nADManager.AsyncPlayAD"] --> B["策略层\nPlacementPolicy / RevenuePolicy"] + B --> C["Provider Runtime\nRewardedRuntime / InterstitialRuntime"] + C --> D["Platform Adapter\nTopOn/Taku"] + D --> E["Unity SDK Bridge"] + E --> F["Native SDK"] +``` + +### 7.1 抽象层职责 + +- 保留 `ADManager` 作为统一入口 +- 去掉“只懂 0/1/2 状态”的单薄模型 +- 引入 placement 级策略 + +### 7.2 平台层职责 + +- 管理 SDK 生命周期 +- 管理自动加载/手工加载 +- 管理状态查询和场景统计 +- 处理微信/抖音/小程序/下载相关链路 + +### 7.3 策略层职责 + +- placement 是否自动加载 +- 预热时机 +- soft wait 时长 +- load fail 重试策略 +- show 冷却和频控 + +--- + +## 10. 实施计划 + +### 阶段 1:P0 止血 + +目标:解决首播失败和关键回调缺陷 + +实施项: + +1. 将工程切换到本地 `CC-Framework.Commercialization` +2. 扩展 Unity 层,补 `ATSDK.start()` +3. `ToponAdController` 中接入 `start` 生命周期 +4. 激励首播增加冷启动补偿和短重试窗口 +5. 插屏回调链修正 + +### 阶段 2:P1 收益增强 + +目标:提高 Ready Rate 和 Show Rate + +实施项: + +1. 核心激励位切自动加载 +2. 插屏支持自动加载或策略化预热 +3. 接 `entryAdScenario` +4. 接状态查询、缓存查询 + +### 阶段 3:P2 架构升级 + +目标:建立长期可运营体系 + +实施项: + +1. 新增策略层 +2. placement 级配置 +3. 本地策略 / 后端策略下发 +4. 数据埋点和 BI 打通 + +--- + +## 11. 第一阶段具体改动清单 + +### 9.1 必改 + +- `Packages/manifest.json` + - 切本地包依赖 +- `Packages/CC-Framework.Commercialization/Assets/Runtime/ADAggregator/*` + - 升级 `AsyncAdPlayer` +- `Assets/Topon_Adapter/Runtime/Scripts/ToponAdController.cs` + - 补 `start` +- `Assets/Topon_Adapter/Runtime/Scripts/AwardVideoPlayer.cs` + - 首播容错、场景统计 +- `Assets/Topon_Adapter/Runtime/Scripts/InteractionPlayer.cs` + - 回调完整化 + +### 9.2 推荐 + +- 增加 `ToponSdkLifecycle` 或 `ToponStrategyConfig` +- 增加 `RewardedPlacementPolicy` +- 为激励和插屏补统一诊断日志 + +--- + +## 12. 产品 / 运营 / 后端建议 + +### 10.1 产品侧 + +- 明确每个激励位的触发场景和价值等级 +- 区分“高价值激励位”和“长尾激励位” +- 插屏不要用“一刀切”频率 + +### 10.2 运营侧 + +- 配合场景统计做 placement 漏斗 +- 按渠道分组优化 Waterfall +- 做自动价格和 A/B 实验 + +### 10.3 后端侧 + +- 下发 placement 策略 +- 拉通报表 API +- 统一客户端和平台收益口径 +- 对激励奖励做服务端幂等和风控 + +--- + +## 13. 一句话结论 + +当前最优方案不是继续修补旧版手工状态机,而是: + +**保留 `ADManager` 统一入口,重做中下层为“策略层 + 平台能力层”,把新版 Taku/TopOn 的 `start / 自动加载 / 场景统计 / 状态查询 / 本地策略` 真正接进来。** diff --git a/README.md b/README.md index 55c590d..d87c02b 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ - 包版本号以 `Assets/package.json` 为准。 - 业务项目引用的也是 `Assets/package.json` 对应 tag。 - 本地工程里的 `Packages/manifest.json` 只用于当前源码工程联调,不参与发布。 +- 当前发布版本: + - `com.commercialization.topon`:`1.4.7` + - `com.foldcc.cc-framework.commercialization`:`1.0.12` ### 2.3 发布前检查 @@ -49,6 +52,7 @@ 2. `Assets/package.json` 里的核心依赖版本与本地验证环境一致。 3. 所有本次升级需要发布的内容都在 `Assets` 下。 4. `Update Upm.bat` 执行前,工作区已整理干净。 +5. Android 业务项目的 `minSdkVersion` 已不低于 `24`。 ## 3. 目录职责 @@ -207,6 +211,7 @@ EDM4U 依赖解析器,负责消费 `Dependencies.xml` 并拉取 Android/iOS - `topon.sub_channel` - `topon.debug` - `topon.debugger_key` +- `topon.auto_open_debugger_ui` - `topon.sdk_area` - `topon.longitude` - `topon.latitude` @@ -215,9 +220,21 @@ EDM4U 依赖解析器,负责消费 `Dependencies.xml` 并拉取 Android/iOS - `topon.query_area_on_init` - `topon.custom_map.` - `topon.rewarded_custom_data.` +- `topon.rewarded_auto_load` +- `topon.rewarded_prewarm_on_init` +- `topon.rewarded_max_load_attempts` +- `topon.rewarded_load_retry_delay_ms` +- `topon.interstitial_auto_load` +- `topon.interstitial_prewarm_on_init` +- `topon.interstitial_max_load_attempts` +- `topon.interstitial_load_retry_delay_ms` +- `topon.local_strategy_asset_path` 说明: +- `topon.debug=true` 现在只打开 SDK 调试日志。 +- 官方 DebugUI 默认不再自动弹出,如需使用,手动调用或在样例面板点击 `Open DebugUI`。 + - `topon.exclude_bundle_ids` - 用于全局排除指定广告主 bundle id - `topon.rewarded_exclude_ad_source_ids`