AdManager.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import { _decorator, Component } from 'cc';
  2. import { AnalyticsManager } from '../Utils/AnalyticsManager';
  3. const { ccclass } = _decorator;
  4. interface RewardedVideoAd {
  5. show(): Promise<void>;
  6. load(): Promise<void>;
  7. onLoad(callback: (res?: any) => void): void;
  8. onError(callback: (err: any) => void): void;
  9. onClose(callback: (res: { isEnded: boolean }) => void): void;
  10. offLoad(callback?: (res?: any) => void): void;
  11. offError(callback?: (err: any) => void): void;
  12. offClose(callback?: (res: { isEnded: boolean }) => void): void;
  13. }
  14. declare const wx: {
  15. createRewardedVideoAd(options: { adUnitId: string; multiton?: boolean }): RewardedVideoAd;
  16. };
  17. @ccclass('AdManager')
  18. export class AdManager {
  19. private static instance: AdManager = null;
  20. private videoAd: RewardedVideoAd = null;
  21. private readonly AD_UNIT_ID = 'adunit-ab2f3b2d24b4f214'; // 替换为你的广告位ID
  22. private constructor() {
  23. // 延迟初始化广告,避免在微信环境未完全准备好时初始化
  24. this.delayedInitialize();
  25. }
  26. public static getInstance(): AdManager {
  27. if (!AdManager.instance) {
  28. AdManager.instance = new AdManager();
  29. }
  30. return AdManager.instance;
  31. }
  32. /**
  33. * 静态方法:初始化广告管理器
  34. * 建议在游戏启动或场景初始化时调用
  35. */
  36. public static initialize(): void {
  37. AdManager.getInstance();
  38. console.log('[AdManager] 广告管理器已初始化');
  39. }
  40. /**
  41. * 延迟初始化广告
  42. * 解决微信小游戏环境下广告组件初始化时机问题
  43. */
  44. private delayedInitialize() {
  45. // 延迟500ms初始化,确保微信环境完全准备好
  46. setTimeout(() => {
  47. this.initializeAd();
  48. }, 500);
  49. }
  50. private initializeAd() {
  51. // 检查是否在微信小游戏环境
  52. if (typeof wx !== 'undefined' && wx.createRewardedVideoAd) {
  53. try {
  54. // 创建激励视频广告实例,提前初始化
  55. this.videoAd = wx.createRewardedVideoAd({
  56. adUnitId: this.AD_UNIT_ID
  57. });
  58. // 设置全局错误处理
  59. this.setupGlobalErrorHandling();
  60. // 预加载广告
  61. this.preloadAd();
  62. console.log('[AdManager] 激励视频广告初始化成功');
  63. } catch (error) {
  64. console.error('[AdManager] 激励视频广告初始化失败:', error);
  65. // 初始化失败时,延迟重试
  66. this.retryInitialize();
  67. }
  68. } else {
  69. console.log('[AdManager] 非微信小游戏环境,跳过广告初始化');
  70. }
  71. }
  72. /**
  73. * 重试初始化广告
  74. * 当首次初始化失败时进行重试
  75. */
  76. private retryInitialize() {
  77. setTimeout(() => {
  78. console.log('[AdManager] 重试初始化广告');
  79. this.initializeAd();
  80. }, 2000);
  81. }
  82. /**
  83. * 设置全局广告错误处理
  84. * 处理广告拉取异常情况
  85. */
  86. private setupGlobalErrorHandling() {
  87. if (!this.videoAd) return;
  88. // 设置全局错误监听
  89. this.videoAd.onError((err: any) => {
  90. console.error('[AdManager] 广告全局错误:', err);
  91. // 根据错误码处理不同情况
  92. if (err.errCode) {
  93. switch (err.errCode) {
  94. case 1000:
  95. console.log('[AdManager] 后端接口调用失败');
  96. break;
  97. case 1001:
  98. console.log('[AdManager] 参数错误');
  99. break;
  100. case 1002:
  101. console.log('[AdManager] 广告单元无效');
  102. break;
  103. case 1003:
  104. console.log('[AdManager] 内部错误');
  105. break;
  106. case 1004:
  107. console.log('[AdManager] 无合适的广告');
  108. // 无广告返回时,建议隐藏激励视频入口
  109. this.handleNoAdAvailable();
  110. break;
  111. case 1005:
  112. console.log('[AdManager] 广告组件审核中');
  113. break;
  114. case 1006:
  115. console.log('[AdManager] 广告组件被驳回');
  116. break;
  117. case 1007:
  118. console.log('[AdManager] 广告组件被封禁');
  119. break;
  120. case 1008:
  121. console.log('[AdManager] 广告单元已关闭');
  122. break;
  123. case 1009:
  124. console.log('[AdManager] 广告单元为适配审核中,请勿调用show()');
  125. break;
  126. default:
  127. console.log('[AdManager] 未知错误码:', err.errCode);
  128. break;
  129. }
  130. }
  131. });
  132. }
  133. /**
  134. * 处理无广告可用的情况
  135. * 建议隐藏激励视频入口
  136. */
  137. private handleNoAdAvailable() {
  138. console.log('[AdManager] 无合适广告,建议隐藏激励视频入口');
  139. // 这里可以发送事件通知UI隐藏广告入口
  140. // 或者设置一个标志位供外部查询
  141. }
  142. /**
  143. * 检查是否有广告可用
  144. * @returns 是否有广告可用
  145. */
  146. public isAdAvailable(): boolean {
  147. return this.videoAd !== null;
  148. }
  149. /**
  150. * 检查广告是否已经初始化完成
  151. * @returns 广告是否已初始化
  152. */
  153. public isAdInitialized(): boolean {
  154. return this.videoAd !== null;
  155. }
  156. /**
  157. * 获取广告初始化状态
  158. * @returns 广告状态信息
  159. */
  160. public getAdStatus(): { initialized: boolean; available: boolean } {
  161. const initialized = this.videoAd !== null;
  162. return {
  163. initialized,
  164. available: initialized
  165. };
  166. }
  167. /**
  168. * 显示激励视频广告
  169. * @param onReward 广告观看完成后的回调
  170. * @param onError 广告显示失败的回调
  171. */
  172. public showRewardedVideoAd(
  173. onReward: () => void,
  174. onError?: (error: any) => void
  175. ): void {
  176. if (!this.videoAd) {
  177. console.log('[AdManager] 广告未初始化,等待初始化完成...');
  178. // 等待广告初始化完成后再显示
  179. this.waitForAdInitialization(() => {
  180. this.showRewardedVideoAd(onReward, onError);
  181. }, onReward);
  182. return;
  183. }
  184. // 设置广告事件监听
  185. const onLoadHandler = (res?: any) => {
  186. console.log('[AdManager] 广告加载成功', res);
  187. if (res && res.useFallbackSharePage) {
  188. console.log('[AdManager] 使用兜底分享页');
  189. }
  190. };
  191. const onErrorHandler = (err: any) => {
  192. console.error('[AdManager] 广告加载失败:', err);
  193. // 根据错误码决定处理方式
  194. let shouldGiveReward = false;
  195. if (err.errCode) {
  196. switch (err.errCode) {
  197. case 1004: // 无合适的广告
  198. console.log('[AdManager] 无合适广告,不给予奖励');
  199. shouldGiveReward = false;
  200. break;
  201. case 1000: // 后端接口调用失败
  202. case 1003: // 内部错误
  203. console.log('[AdManager] 系统错误,给予奖励');
  204. shouldGiveReward = true;
  205. break;
  206. default:
  207. console.log('[AdManager] 其他错误,给予奖励');
  208. shouldGiveReward = true;
  209. break;
  210. }
  211. } else {
  212. // 没有错误码的情况,默认给予奖励
  213. shouldGiveReward = true;
  214. }
  215. if (onError) {
  216. onError(err);
  217. } else if (shouldGiveReward) {
  218. console.log('[AdManager] 广告失败但给予奖励');
  219. onReward();
  220. } else {
  221. console.log('[AdManager] 广告失败且不给予奖励');
  222. }
  223. };
  224. const onCloseHandler = (res: { isEnded: boolean }) => {
  225. console.log('[AdManager] 广告关闭:', res);
  226. // 清理事件监听
  227. this.videoAd.offLoad(onLoadHandler);
  228. this.videoAd.offError(onErrorHandler);
  229. this.videoAd.offClose(onCloseHandler);
  230. // 根据广告是否完整观看来决定是否给予奖励
  231. if (res.isEnded) {
  232. console.log('[AdManager] 用户完整观看广告,给予奖励');
  233. onReward();
  234. } else {
  235. console.log('[AdManager] 用户未完整观看广告,视为取消,不给予奖励');
  236. // 用户主动关闭或未看完,通知上层进行状态重置
  237. if (onError) {
  238. onError({ errCode: 'USER_CANCELLED', message: '用户取消观看激励视频广告', isEnded: false });
  239. }
  240. }
  241. };
  242. // 绑定事件监听
  243. this.videoAd.onLoad(onLoadHandler);
  244. this.videoAd.onError(onErrorHandler);
  245. this.videoAd.onClose(onCloseHandler);
  246. // 显示广告
  247. this.videoAd.show().then(() => {
  248. console.log('[AdManager] 广告显示成功');
  249. // 追踪广告展示事件
  250. AnalyticsManager.getInstance().trackAdShow({
  251. ad_type: 'rewarded_video',
  252. ad_unit_id: this.AD_UNIT_ID,
  253. show_time: new Date().toISOString()
  254. });
  255. }).catch((showError) => {
  256. console.log('[AdManager] 广告显示失败,尝试重新加载');
  257. // 失败重试,增加延迟确保广告完全加载
  258. this.videoAd.load()
  259. .then(() => {
  260. // 加载成功后稍等片刻再显示,避免渲染问题
  261. setTimeout(() => {
  262. this.videoAd.show().catch(err => {
  263. console.error('[AdManager] 激励视频广告二次显示失败', err);
  264. onErrorHandler(err);
  265. });
  266. }, 300);
  267. })
  268. .catch(err => {
  269. console.error('[AdManager] 激励视频广告加载失败', err);
  270. onErrorHandler(err);
  271. });
  272. });
  273. }
  274. /**
  275. * 等待广告初始化完成
  276. * @param callback 初始化完成后的回调
  277. * @param fallback 超时后的回退处理
  278. */
  279. private waitForAdInitialization(callback: () => void, fallback: () => void): void {
  280. let attempts = 0;
  281. const maxAttempts = 10; // 最多等待5秒
  282. const checkInitialization = () => {
  283. if (this.videoAd) {
  284. callback();
  285. return;
  286. }
  287. attempts++;
  288. if (attempts >= maxAttempts) {
  289. console.log('[AdManager] 广告初始化超时,直接给予奖励');
  290. fallback();
  291. return;
  292. }
  293. setTimeout(checkInitialization, 500);
  294. };
  295. checkInitialization();
  296. }
  297. /**
  298. * 预加载广告
  299. * 可以在游戏启动时调用,提前加载广告
  300. */
  301. public preloadAd(): void {
  302. if (this.videoAd) {
  303. console.log('[AdManager] 开始预加载广告');
  304. this.videoAd.load().then(() => {
  305. console.log('[AdManager] 广告预加载成功');
  306. }).catch(err => {
  307. console.error('[AdManager] 预加载广告失败:', err);
  308. // 预加载失败时,延迟重试
  309. setTimeout(() => {
  310. console.log('[AdManager] 重试预加载广告');
  311. this.preloadAd();
  312. }, 3000);
  313. });
  314. }
  315. }
  316. /**
  317. * 销毁广告管理器
  318. */
  319. public destroy(): void {
  320. if (this.videoAd) {
  321. // 清理所有事件监听,包括全局错误监听
  322. this.videoAd.offLoad();
  323. this.videoAd.offError();
  324. this.videoAd.offClose();
  325. this.videoAd = null;
  326. }
  327. AdManager.instance = null;
  328. }
  329. }