# 梗中作乐 V1.0 - 源代码文档(第 1 部分) 软件名称:梗中作乐 版本号:V1.0 --- ## 第 1 部分:程序入口与视图管理 本部分包含 3 个源文件,是整个小游戏的启动入口、加载页与全局视图调度器。 ### 1.1 程序主入口 (assets/main.ts) ```typescript import { _decorator, Component, Prefab, AudioClip } from 'cc'; import { ViewManager } from './scripts/core/ViewManager'; import { ToastManager } from './scripts/utils/ToastManager'; import { AudioManager } from './scripts/utils/AudioManager'; import { ShareLaunchHandler } from './scripts/utils/ShareLaunchHandler'; const { ccclass, property } = _decorator; /** * 主入口脚本 * 负责初始化 ViewManager 并注册页面 */ @ccclass('main') export class main extends Component { @property({ type: Prefab, tooltip: '首页预制体' }) pageHomePrefab: Prefab | null = null; @property({ type: Prefab, tooltip: '关卡页面预制体' }) pageLevelPrefab: Prefab | null = null; @property({ type: Prefab, tooltip: '写关卡页面预制体' }) pageWriteLevelsPrefab: Prefab | null = null; @property({ type: Prefab, tooltip: '预览试卷页面预制体' }) pagePreviewLevelsPrefab: Prefab | null = null; @property({ type: Prefab, tooltip: '挑战数据页面预制体' }) pagePKDataPrefab: Prefab | null = null; @property({ type: Prefab, tooltip: '挑战详情页面预制体' }) pagePKDetailPrefab: Prefab | null = null; @property({ type: Prefab, tooltip: '挑战结算页面预制体' }) pagePKEndPrefab: Prefab | null = null; @property({ type: Prefab, tooltip: 'Toast 预制体' }) toastPrefab: Prefab | null = null; @property({ type: AudioClip, tooltip: '通用按钮点击音效' }) buttonClickAudio: AudioClip | null = null; /** * onLoad 比 start 更早执行 * 确保 ViewManager 在 PageLoading.start() 之前初始化 */ onLoad() { this._initViewManager(); } /** * 初始化页面管理器 */ private _initViewManager(): void { // 初始化 ViewManager,绑定 Canvas 作为页面容器 ViewManager.instance.init(this.node); // 注册页面配置(通过编辑器属性引用预制体) if (this.pageHomePrefab) { ViewManager.instance.register('PageHome', { prefab: this.pageHomePrefab, cache: true, zIndex: 0 }); } if (this.pageLevelPrefab) { ViewManager.instance.register('PageLevel', { prefab: this.pageLevelPrefab, cache: true, zIndex: 1 }); } if (this.pageWriteLevelsPrefab) { ViewManager.instance.register('PageWriteLevels', { prefab: this.pageWriteLevelsPrefab, cache: true, zIndex: 2 }); } if (this.pagePreviewLevelsPrefab) { ViewManager.instance.register('PagePreviewLevels', { prefab: this.pagePreviewLevelsPrefab, cache: true, zIndex: 3 }); } if (this.pagePKDataPrefab) { ViewManager.instance.register('PagePKData', { prefab: this.pagePKDataPrefab, cache: true, zIndex: 3 }); } if (this.pagePKDetailPrefab) { ViewManager.instance.register('PagePKDetail', { prefab: this.pagePKDetailPrefab, cache: true, zIndex: 4 }); } if (this.pagePKEndPrefab) { ViewManager.instance.register('PagePKEnd', { prefab: this.pagePKEndPrefab, cache: true, zIndex: 3 }); } // 初始化 Toast 管理器 if (this.toastPrefab) { ToastManager.instance.init(this.toastPrefab, this.node); } AudioManager.instance.init(this.buttonClickAudio, this.node); // 注册 wx.onShow / wx.onHide: // 用户把小游戏退到后台后再点击好友分享卡片,能拿到最新的 shareCode 并直达分享挑战关卡。 // 必须在 PageLoading 跑之前注册,这样初始 launch 中的 shareCode 也会被作为种子记下。 ShareLaunchHandler.instance.init(); } } ``` ### 1.2 加载页 (assets/PageLoading.ts) ```typescript import { _decorator, Component, ProgressBar, Label, assetManager } from 'cc'; import type { AssetManager } from 'cc'; import { ViewManager } from './scripts/core/ViewManager'; import { LevelDataManager } from './scripts/utils/LevelDataManager'; import { AuthManager } from './scripts/utils/AuthManager'; import { ShareManager } from './scripts/utils/ShareManager'; import { ShareLaunchHandler } from './scripts/utils/ShareLaunchHandler'; import { WxSDK } from './scripts/utils/WxSDK'; const { ccclass, property } = _decorator; /** * 页面加载组件 * 负责用户登录、预加载资源并显示加载进度 * 流程:登录 + game-data → 拿到 nextLevel → 加载首关图片 → 进入首页 */ @ccclass('PageLoading') export class PageLoading extends Component { private static readonly FONT_BUNDLE_NAME = 'fonts'; @property(ProgressBar) progressBar: ProgressBar | null = null; @property(Label) statusLabel: Label | null = null; start() { this._startPreload(); } private async _startPreload(): Promise { if (this.progressBar) { this.progressBar.progress = 0; } this._updateStatusLabel('正在加载...'); // 阶段0: 主动触发微信隐私授权。分享挑战直达关卡时也会先经过这里。 this._updateStatusLabel('正在确认隐私授权...'); const privacyAuthorized = await WxSDK.ensurePrivacyAuthorized(); if (!privacyAuthorized) { console.warn('[PageLoading] 用户未同意隐私授权,继续加载基础流程'); } // 阶段1: 登录 + 获取 game-data(含 nextLevel) this._updateProgress(0); this._updateStatusLabel('正在连接服务器...'); const loginSuccess = await AuthManager.instance.initialize(); if (loginSuccess) { console.log('[PageLoading] 用户登录成功'); } else { console.warn('[PageLoading] 登录失败,继续离线模式'); } this._updateProgress(0.2); // 阶段2: 加载首关图片(如果有 nextLevel) const nextLevel = AuthManager.instance.nextLevel; let levelSuccess = false; if (nextLevel) { levelSuccess = await LevelDataManager.instance.initialize(nextLevel, (progress, message) => { // 关卡图片加载占 20%-80% 进度 this._updateProgress(0.2 + progress * 0.6); this._updateStatusLabel(message); }); if (!levelSuccess) { this._updateStatusLabel('资源加载失败,请重新打开游戏'); return; } } else if (loginSuccess) { // nextLevel 为 null → 全部通关(或服务端无关卡) console.log('[PageLoading] 全部通关或无可用关卡'); this._updateProgress(0.8); } else { // 登录失败且没有 nextLevel this._updateStatusLabel('加载失败,请重新打开游戏'); return; } // 阶段3: 加载字体分包 const fontSuccess = await this._loadFontBundle(); if (!fontSuccess) { this._updateStatusLabel('字体资源加载失败,请重新打开游戏'); return; } // 检测分享码:从微信启动参数中获取 const shareCode = WxSDK.getShareCodeFromLaunch(); if (shareCode && loginSuccess) { this._updateStatusLabel('正在加载挑战关卡...'); const joinSuccess = await ShareManager.instance.joinShare(shareCode); if (joinSuccess) { // 把启动 shareCode 同步给 ShareLaunchHandler, // 避免 wx.onShow 在初始展示时拿到同一个 code 又走一遍 join。 ShareLaunchHandler.instance.markActiveShareCode(shareCode); this._updateProgress(1); this._updateStatusLabel('加载完成'); // 跳过首页,直接进入分享挑战关卡 ViewManager.instance.open('PageLevel', { params: { shareMode: true }, onComplete: () => { this.node.destroy(); }, }); return; } console.warn('[PageLoading] 加入分享失败,进入正常模式'); } // 正常流程:预加载 PageHome (82-100%) ViewManager.instance.preload('PageHome', (progress) => { this._updateProgress(0.82 + progress * 0.18); this._updateStatusLabel('正在加载界面资源...'); }, () => { this._onPreloadComplete(); } ); } private _updateProgress(progress: number): void { if (this.progressBar) { this.progressBar.progress = progress; } } private _updateStatusLabel(message: string): void { if (this.statusLabel) { this.statusLabel.string = message; } } private _onPreloadComplete(): void { this._updateProgress(1); this._updateStatusLabel('加载完成'); // 兜底:如果在 PageLoading 还在 preload 期间,wx.onShow 已经把游戏切到了分享态 // (ShareLaunchHandler 已经 joinShare 成功),这里就不应再覆盖回首页, // 否则用户点了好友分享卡片却看到 PageHome。 if (ShareManager.instance.isShareMode) { console.log('[PageLoading] 检测到分享态已激活,跳过首页直达 PageLevel'); ViewManager.instance.open('PageLevel', { params: { shareMode: true }, onComplete: () => { this.node.destroy(); }, }); return; } ViewManager.instance.open('PageHome', { onComplete: () => { this.node.destroy(); } }); } /** * 加载字体分包,避免字体资源进入小游戏主包 */ private _loadFontBundle(): Promise { const bundleName = PageLoading.FONT_BUNDLE_NAME; const cachedBundle = assetManager.getBundle(bundleName); if (cachedBundle) { console.log(`[PageLoading] 字体分包已加载: ${bundleName}`); this._updateProgress(0.82); return Promise.resolve(true); } this._updateStatusLabel('正在加载字体资源...'); this._updateProgress(0.8); return new Promise((resolve) => { assetManager.loadBundle(bundleName, (err: Error | null, bundle: AssetManager.Bundle | null) => { if (err || !bundle) { console.error(`[PageLoading] 字体分包加载失败: ${bundleName}`, err); resolve(false); return; } console.log(`[PageLoading] 字体分包加载完成: ${bundleName}`); this._updateProgress(0.82); resolve(true); }); }); } } ``` ### 1.3 视图管理器 (assets/scripts/core/ViewManager.ts) ```typescript import { _decorator, Node, Prefab, instantiate, error } from 'cc'; import { BaseView, ViewConfig, ViewOptions } from './BaseView'; const { ccclass } = _decorator; /** * 已注册的页面配置映射 */ interface RegisteredView { config: ViewConfig; } /** * 页面管理器 * 单例模式,统一管理页面的注册、打开、关闭、返回等操作 */ @ccclass('ViewManager') export class ViewManager { /** 单例实例 */ private static _instance: ViewManager | null = null; /** 获取单例 */ public static get instance(): ViewManager { if (!ViewManager._instance) { ViewManager._instance = new ViewManager(); } return ViewManager._instance; } /** 页面容器节点 */ private _container: Node | null = null; /** 已注册的页面配置 */ private _registeredViews: Map = new Map(); /** 页面栈 */ private _viewStack: BaseView[] = []; /** 页面实例缓存(用于缓存模式的页面) */ private _viewCache: Map = new Map(); /** * 初始化管理器 * @param container 页面容器节点(通常是 Canvas) */ init(container: Node): void { this._container = container; } /** * 获取当前容器 */ getContainer(): Node | null { return this._container; } /** * 注册页面 * @param viewId 页面唯一标识 * @param config 页面配置 */ register(viewId: string, config: ViewConfig): void { if (this._registeredViews.has(viewId)) { error(`ViewManager: 页面 "${viewId}" 已注册`); return; } this._registeredViews.set(viewId, { config: { cache: true, // 默认缓存 zIndex: 0, // 默认层级 ...config } }); } /** * 批量注册页面 * @param views 页面配置映射 */ registerAll(views: Record): void { for (const [viewId, config] of Object.entries(views)) { this.register(viewId, config); } } /** * 打开页面 * @param viewId 页面唯一标识 * @param options 打开选项 */ open(viewId: string, options?: ViewOptions): void { if (!this._container) { const err = new Error('ViewManager: 未初始化,请先调用 init()'); options?.onError?.(err); return; } const registered = this._registeredViews.get(viewId); if (!registered) { const err = new Error(`ViewManager: 页面 "${viewId}" 未注册`); options?.onError?.(err); return; } // 检查是否有缓存的实例 const cachedView = this._viewCache.get(viewId); if (cachedView && cachedView.node.isValid) { this._showView(cachedView, options); return; } // 直接使用预制体引用实例化 this._instantiateView(viewId, registered.config.prefab, options); } /** * 实例化视图 */ private _instantiateView(viewId: string, prefab: Prefab, options?: ViewOptions): void { if (!this._container) return; const registered = this._registeredViews.get(viewId); if (!registered) return; const node = instantiate(prefab); const view = node.getComponent(BaseView); if (!view) { error(`ViewManager: 预制体 "${viewId}" 缺少 BaseView 组件`); node.destroy(); options?.onError?.(new Error('缺少 BaseView 组件')); return; } // 设置视图属性 view.viewId = viewId; view.config = registered.config; view.setParams(options?.params); // 设置层级 node.setSiblingIndex(registered.config.zIndex || 0); // 添加到容器 this._container.addChild(node); // 调用加载回调 view.onViewLoad(); // 缓存视图实例 if (registered.config.cache) { this._viewCache.set(viewId, view); } // 显示视图 this._showView(view, options); } /** * 显示视图 */ private _showView(view: BaseView, options?: ViewOptions): void { // 隐藏当前页面 const currentView = this.getCurrentView(); if (currentView && currentView !== view) { currentView._doHide(); } // 设置参数 if (options?.params !== undefined) { view.setParams(options?.params); } // 入栈 if (!this._viewStack.includes(view)) { this._viewStack.push(view); } // 显示 view._doShow(); // 回调 options?.onComplete?.(view); } /** * 关闭当前页面 * @param options 关闭选项 */ close(options?: { destroy?: boolean }): void { const currentView = this._viewStack.pop(); if (!currentView) return; const shouldDestroy = options?.destroy ?? !currentView.config?.cache; this._hideAndDestroyView(currentView, shouldDestroy); // 显示上一页 const prevView = this.getCurrentView(); if (prevView) { prevView._doShow(); } } /** * 返回上一页(close 的别名) */ back(): void { this.close(); } /** * 替换当前页面 * @param viewId 新页面标识 * @param options 打开选项 */ replace(viewId: string, options?: ViewOptions): void { const currentView = this.getCurrentView(); if (currentView) { this._viewStack.pop(); const shouldDestroy = !currentView.config?.cache; this._hideAndDestroyView(currentView, shouldDestroy); } this.open(viewId, options); } /** * 隐藏并销毁视图(内部方法) */ private _hideAndDestroyView(view: BaseView, shouldDestroy: boolean): void { view._doHide(); if (shouldDestroy) { this._viewCache.delete(view.viewId); view._doDestroy(); } } /** * 获取当前页面 */ getCurrentView(): BaseView | null { return this._viewStack.length > 0 ? this._viewStack[this._viewStack.length - 1] : null; } /** * 获取页面栈 */ getViewStack(): BaseView[] { return [...this._viewStack]; } /** * 清空所有页面 */ clearAll(): void { // 从栈顶开始销毁 while (this._viewStack.length > 0) { const view = this._viewStack.pop()!; const shouldDestroy = !view.config?.cache; this._hideAndDestroyView(view, shouldDestroy); } // 销毁缓存的页面 for (const view of this._viewCache.values()) { if (view.node.isValid) { view._doDestroy(); } } this._viewCache.clear(); } /** * 预加载页面预制体(主包资源已随游戏加载,此方法仅为兼容性保留) * @param viewId 页面标识 * @param onProgress 进度回调 * @param onComplete 完成回调 */ preload(viewId: string, onProgress?: (progress: number) => void, onComplete?: () => void): void { const registered = this._registeredViews.get(viewId); if (!registered) { error(`ViewManager: 页面 "${viewId}" 未注册`); onComplete?.(); return; } // 主包资源已加载,直接回调 onProgress?.(1); onComplete?.(); } /** * 批量预加载页面 * @param viewIds 页面标识数组 * @param onProgress 总进度回调 (0-1) * @param onComplete 完成回调 */ preloadAll(viewIds: string[], onProgress?: (progress: number) => void, onComplete?: () => void): void { if (viewIds.length === 0) { onProgress?.(1); onComplete?.(); return; } let completed = 0; const total = viewIds.length; for (const viewId of viewIds) { this.preload(viewId, () => { completed++; onProgress?.(completed / total); if (completed === total) { onComplete?.(); } }); } } } ```