Files
mp-xieyingeng/软著源代码/01-程序入口与视图管理.md
2026-06-29 19:55:40 +08:00

20 KiB
Raw Blame History

梗中作乐 V1.0 - 源代码文档(第 1 部分)

软件名称:梗中作乐 版本号V1.0


第 1 部分:程序入口与视图管理

本部分包含 3 个源文件,是整个小游戏的启动入口、加载页与全局视图调度器。

1.1 程序主入口 (assets/main.ts)

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)

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<void> {
        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<boolean> {
        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)

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<string, RegisteredView> = new Map();

    /** 页面栈 */
    private _viewStack: BaseView[] = [];

    /** 页面实例缓存(用于缓存模式的页面) */
    private _viewCache: Map<string, BaseView> = 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<string, ViewConfig>): 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?.();
                }
            });
        }
    }
}