AdManager.ts 12 KB

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