661 lines
20 KiB
Markdown
661 lines
20 KiB
Markdown
# 梗中作乐 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?.();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
```
|