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

661 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 梗中作乐 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<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)
```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<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?.();
}
});
}
}
}
```