feat: 接入激励视频

This commit is contained in:
richarjiang
2026-06-29 19:55:40 +08:00
parent 411ca8c772
commit e2fd4eeda3
16 changed files with 9215 additions and 73 deletions

View File

@@ -0,0 +1,660 @@
# 梗中作乐 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?.();
}
});
}
}
}
```