You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
12 KiB
362 lines
12 KiB
|
3 weeks ago
|
import 'db://assets/mx/tools/MiniGameTextureBoot';
|
||
|
|
import 'db://assets/mx/tools/CacheManagerPatch';
|
||
|
|
import 'db://assets/mx/tools/SilenceConsoleInRelease';
|
||
|
|
import 'db://assets/script/battle/debug/DesignDebug';
|
||
|
|
import { _decorator, assetManager, Camera, clamp01, Component, director, Label, Node, Sprite, sys } from 'cc';
|
||
|
|
|
||
|
|
import { GBundle, UIConfigs, GAudioPath } from './game/ConfigRes';
|
||
|
|
import { GameController } from './game/GameController';
|
||
|
|
import { GM } from './game/GM';
|
||
|
|
import { DEBUG, WECHAT } from 'cc/env';
|
||
|
|
import { sdkConfig } from './sdk/sdkConfig';
|
||
|
|
import { GEvent } from '../mx/module/event/GEvent';
|
||
|
|
import { BattleUiPauseGate } from './battle/BattleUiPauseGate';
|
||
|
|
import { BootPipeline } from '../mx/module/boot/BootPipeline';
|
||
|
|
import { RuntimeLoadingGate } from '../mx/module/ui/RuntimeLoadingGate';
|
||
|
|
import { ClientErrorReporter } from '../mx/tools/ClientErrorReporter';
|
||
|
|
|
||
|
|
const { ccclass, property } = _decorator;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 游戏入口组件:冷启动 Boot、双门闩进游戏、Boot/运行时 Loading 与网络等待 UI。
|
||
|
|
*/
|
||
|
|
@ccclass('GameLaunch')
|
||
|
|
export class GameLaunch extends Component {
|
||
|
|
|
||
|
|
/** 当前场景挂载的唯一实例 */
|
||
|
|
static instantiate: GameLaunch = null;
|
||
|
|
|
||
|
|
@property(Node)
|
||
|
|
Loading: Node;
|
||
|
|
|
||
|
|
@property(Label)
|
||
|
|
title: Label;
|
||
|
|
|
||
|
|
@property(Node)
|
||
|
|
netUI: Node;
|
||
|
|
|
||
|
|
@property(Node)
|
||
|
|
loadingUI: Node;
|
||
|
|
|
||
|
|
private _progress = 0;
|
||
|
|
/** 首包 Boot 资源就绪 */
|
||
|
|
private _firstResReady = false;
|
||
|
|
/** 配表与 gg.data.init 完成 */
|
||
|
|
private _sdkAndDataReady = false;
|
||
|
|
/** 已进入游戏,停止 Boot 进度刷新 */
|
||
|
|
private _enteredGame = false;
|
||
|
|
private _deferredStarted = false;
|
||
|
|
|
||
|
|
private _loadingProgressSprite: Sprite | null = null;
|
||
|
|
private _loadingPercentLabel: Label | null = null;
|
||
|
|
private _loadingBarSprite: Sprite | null = null;
|
||
|
|
private _bootPipeline = new BootPipeline();
|
||
|
|
private _runtimeLoadingGate: RuntimeLoadingGate | null = null;
|
||
|
|
|
||
|
|
protected onLoad(): void {
|
||
|
|
gg.sdk.capturePfShow();
|
||
|
|
if (WECHAT) {
|
||
|
|
// 编译期仅微信包保留;initSdk30r 内再 require 30r,抖音/快手包不含 @dn-sdk
|
||
|
|
gg.sdk.initSdk30r();
|
||
|
|
}
|
||
|
|
GameLaunch.instantiate = this;
|
||
|
|
if (assetManager.cacheManager) {
|
||
|
|
assetManager.cacheManager.autoClear = true;
|
||
|
|
}
|
||
|
|
this.bindGlobalEvents();
|
||
|
|
this.resolveLoadingRefs();
|
||
|
|
this._runtimeLoadingGate = new RuntimeLoadingGate(
|
||
|
|
this.getLoadingUI.bind(this),
|
||
|
|
this.getBootLoading.bind(this),
|
||
|
|
this.isRuntimePhase.bind(this),
|
||
|
|
this.onLoadingWatchdog.bind(this),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected update(dt: number): void {
|
||
|
|
if (this._enteredGame) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (this._loadingProgressSprite) {
|
||
|
|
this._loadingProgressSprite.fillRange = clamp01(this._progress);
|
||
|
|
}
|
||
|
|
if (!gg.game) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const progress = gg.game.CurentLoadingProgress;
|
||
|
|
if (this._loadingPercentLabel) {
|
||
|
|
this._loadingPercentLabel.string = `${Math.floor(progress * 100)}%`;
|
||
|
|
}
|
||
|
|
if (this._loadingBarSprite) {
|
||
|
|
this._loadingBarSprite.fillRange = progress;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
start(): void {
|
||
|
|
if (DEBUG) {
|
||
|
|
window['gg'] = gg;
|
||
|
|
window['gm'] = GM;
|
||
|
|
}
|
||
|
|
if (!this.initCameraAndShell()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
gg.game = this.addComponent(GameController);
|
||
|
|
Promise.all([
|
||
|
|
gg.lang.init(sdkConfig.defaultLang),
|
||
|
|
this._bootPipeline.start({
|
||
|
|
onTitle: this.onBootTitle.bind(this),
|
||
|
|
onProgress: this.onBootProgress.bind(this),
|
||
|
|
onBootAssetsReady: this.onBootAssetsReady.bind(this),
|
||
|
|
onSdkAndDataReady: this.onSdkAndDataReady.bind(this),
|
||
|
|
}),
|
||
|
|
]).catch(this.onBootFailed.bind(this));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 双门闩:首包资源与 SDK/存档均就绪后进游戏。
|
||
|
|
* 门闩字段为 _firstResReady / _sdkAndDataReady;不用 Loading.active 作就绪条件。
|
||
|
|
*/
|
||
|
|
private tryEnterGame(): void {
|
||
|
|
if (this._enteredGame) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!this._firstResReady || !this._sdkAndDataReady) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (this._progress < 1) {
|
||
|
|
this._progress = 1;
|
||
|
|
}
|
||
|
|
this._enteredGame = true;
|
||
|
|
if (!this._deferredStarted) {
|
||
|
|
this._deferredStarted = true;
|
||
|
|
this._bootPipeline.startDeferredPhase();
|
||
|
|
}
|
||
|
|
gg.game.init();
|
||
|
|
if (this.Loading && this.Loading.active) {
|
||
|
|
this.Loading.active = false;
|
||
|
|
}
|
||
|
|
if (this._loadingProgressSprite) {
|
||
|
|
this._loadingProgressSprite.fillRange = 1;
|
||
|
|
}
|
||
|
|
this.enabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 显示网络等待 UI */
|
||
|
|
showNetUI(): void {
|
||
|
|
if (this.netUI) {
|
||
|
|
this.netUI.active = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 关闭网络等待 UI */
|
||
|
|
closeNetUI(): void {
|
||
|
|
if (this.netUI) {
|
||
|
|
this.netUI.active = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 请求显示运行时 Loading */
|
||
|
|
showLoading(index: number = 2, immediate = false): void {
|
||
|
|
this._runtimeLoadingGate?.requestShow(index, immediate);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 请求隐藏运行时 Loading */
|
||
|
|
hideLoading(): void {
|
||
|
|
this._runtimeLoadingGate?.requestHide();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 强制关闭运行时 Loading */
|
||
|
|
forceHideLoading(): void {
|
||
|
|
this._runtimeLoadingGate?.forceHide();
|
||
|
|
}
|
||
|
|
|
||
|
|
protected onDestroy(): void {
|
||
|
|
this.unbindGlobalEvents();
|
||
|
|
this._runtimeLoadingGate?.dispose();
|
||
|
|
this._runtimeLoadingGate = null;
|
||
|
|
if (GameLaunch.instantiate === this) {
|
||
|
|
GameLaunch.instantiate = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private bindGlobalEvents(): void {
|
||
|
|
GEvent.Ins.on(GEvent.SHOW_NET_UI, this.showNetUI, this);
|
||
|
|
GEvent.Ins.on(GEvent.HIDE_NET_UI, this.closeNetUI, this);
|
||
|
|
GEvent.Ins.on(GEvent.SHOW_LOADING, this.showLoading, this);
|
||
|
|
GEvent.Ins.on(GEvent.HIDE_LOADING, this.hideLoading, this);
|
||
|
|
GEvent.Ins.on(GEvent.FORCE_HIDE_LOADING, this.forceHideLoading, this);
|
||
|
|
this._bindJsErrorSink();
|
||
|
|
this._bindBrowserErrorSink();
|
||
|
|
}
|
||
|
|
|
||
|
|
private unbindGlobalEvents(): void {
|
||
|
|
GEvent.Ins.off(GEvent.SHOW_NET_UI, this.showNetUI, this);
|
||
|
|
GEvent.Ins.off(GEvent.HIDE_NET_UI, this.closeNetUI, this);
|
||
|
|
GEvent.Ins.off(GEvent.SHOW_LOADING, this.showLoading, this);
|
||
|
|
GEvent.Ins.off(GEvent.HIDE_LOADING, this.hideLoading, this);
|
||
|
|
GEvent.Ins.off(GEvent.FORCE_HIDE_LOADING, this.forceHideLoading, this);
|
||
|
|
if (sys.isBrowser && typeof window !== 'undefined') {
|
||
|
|
window.removeEventListener('error', this._onBrowserError);
|
||
|
|
window.removeEventListener('unhandledrejection', this._onBrowserRejection);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 小游戏未捕获异常:打日志并上报平台 */
|
||
|
|
private _bindJsErrorSink(): void {
|
||
|
|
const names = ['wx', 'tt', 'ks', 'qg'];
|
||
|
|
for (let i = 0; i < names.length; i++) {
|
||
|
|
const api = window[names[i]] as {
|
||
|
|
onError?: (cb: (msg: unknown) => void) => void;
|
||
|
|
onUnhandledRejection?: (cb: (res: unknown) => void) => void;
|
||
|
|
} | undefined;
|
||
|
|
if (!api) continue;
|
||
|
|
try {
|
||
|
|
api.onError?.(this._onMiniGameJsError);
|
||
|
|
api.onUnhandledRejection?.(this._onMiniGameRejection);
|
||
|
|
} catch (e) {
|
||
|
|
ClientErrorReporter.report('GameLaunch_bindJsErrorSink', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 浏览器预览未捕获异常 */
|
||
|
|
private _bindBrowserErrorSink(): void {
|
||
|
|
if (!sys.isBrowser || typeof window === 'undefined') return;
|
||
|
|
window.addEventListener('error', this._onBrowserError);
|
||
|
|
window.addEventListener('unhandledrejection', this._onBrowserRejection);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _onMiniGameJsError = (msg: unknown): void => {
|
||
|
|
ClientErrorReporter.report('MiniGame_onError', msg);
|
||
|
|
};
|
||
|
|
|
||
|
|
private _onMiniGameRejection = (res: unknown): void => {
|
||
|
|
const reason = res && typeof res === 'object' && 'reason' in res
|
||
|
|
? (res as { reason?: unknown }).reason
|
||
|
|
: res;
|
||
|
|
ClientErrorReporter.report('MiniGame_unhandledRejection', reason);
|
||
|
|
};
|
||
|
|
|
||
|
|
private _onBrowserError = (ev: ErrorEvent): void => {
|
||
|
|
const msg = ev.message || String(ev.error ?? 'error');
|
||
|
|
ClientErrorReporter.report('Browser_onError', msg);
|
||
|
|
};
|
||
|
|
|
||
|
|
private _onBrowserRejection = (ev: PromiseRejectionEvent): void => {
|
||
|
|
ClientErrorReporter.report('Browser_unhandledRejection', ev.reason);
|
||
|
|
};
|
||
|
|
|
||
|
|
private resolveLoadingRefs(): void {
|
||
|
|
if (!this.Loading) {
|
||
|
|
console.log('[GameLaunch] Loading 节点未绑定');
|
||
|
|
} else {
|
||
|
|
const progressNode = this.Loading.getChildByName('progress');
|
||
|
|
if (!progressNode) {
|
||
|
|
console.log('[GameLaunch] Loading/progress 子节点缺失');
|
||
|
|
} else {
|
||
|
|
this._loadingProgressSprite = progressNode.getComponent(Sprite);
|
||
|
|
if (!this._loadingProgressSprite) {
|
||
|
|
console.log('[GameLaunch] progress 节点缺少 Sprite 组件');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!this.loadingUI) {
|
||
|
|
console.log('[GameLaunch] loadingUI 节点未绑定');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const labelNode = this.loadingUI.find('1/Label');
|
||
|
|
if (!labelNode) {
|
||
|
|
console.log('[GameLaunch] loadingUI/1/Label 缺失');
|
||
|
|
} else {
|
||
|
|
this._loadingPercentLabel = labelNode.getComponent(Label);
|
||
|
|
if (!this._loadingPercentLabel) {
|
||
|
|
console.log('[GameLaunch] loadingUI Label 缺少 Label 组件');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const barNode = this.loadingUI.find('1/progress');
|
||
|
|
if (!barNode) {
|
||
|
|
console.log('[GameLaunch] loadingUI/1/progress 缺失');
|
||
|
|
} else {
|
||
|
|
this._loadingBarSprite = barNode.getComponent(Sprite);
|
||
|
|
if (!this._loadingBarSprite) {
|
||
|
|
console.log('[GameLaunch] loadingUI progress 缺少 Sprite 组件');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**绑定场景相机并初始化 UI、音频 */
|
||
|
|
private initCameraAndShell(): boolean {
|
||
|
|
const scene = director.getScene();
|
||
|
|
if (!scene) {
|
||
|
|
console.error('[GameLaunch] director.getScene() 为空,无法初始化 UI');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const uiCameraNode = scene.find('UICamera');
|
||
|
|
if (!uiCameraNode) {
|
||
|
|
console.error('[GameLaunch] 场景缺少 UICamera 节点');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const uiCamera = uiCameraNode.getComponent(Camera);
|
||
|
|
if (!uiCamera) {
|
||
|
|
console.error('[GameLaunch] UICamera 缺少 Camera 组件');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
gg.ui.init(UIConfigs, uiCamera, false);
|
||
|
|
gg.ui.setBattlePauseHook(this.onBattleUiPause.bind(this));
|
||
|
|
gg.audio.init({ bundle: GBundle.Audio, path: GAudioPath.SfxUi, clickEffname: '点击按钮' });
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private onBootTitle(text: string): void {
|
||
|
|
if (!this.title) {
|
||
|
|
console.log('[GameLaunch] title Label 未绑定,无法更新 Boot 标题');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.title.string = text;
|
||
|
|
}
|
||
|
|
|
||
|
|
private onBootProgress(delta: number): void {
|
||
|
|
this._progress += delta;
|
||
|
|
}
|
||
|
|
|
||
|
|
private onBootAssetsReady(): void {
|
||
|
|
this.onBootTitle('资源加载完成');
|
||
|
|
this._firstResReady = true;
|
||
|
|
this.tryEnterGame();
|
||
|
|
}
|
||
|
|
|
||
|
|
private onSdkAndDataReady(): void {
|
||
|
|
this._sdkAndDataReady = true;
|
||
|
|
this.tryEnterGame();
|
||
|
|
}
|
||
|
|
|
||
|
|
private onBootFailed(err: unknown): void {
|
||
|
|
console.error('[GameLaunch] boot', String(err));
|
||
|
|
}
|
||
|
|
|
||
|
|
private onBattleUiPause(uiId: number, open: boolean): void {
|
||
|
|
const token = `ui:${uiId}`;
|
||
|
|
if (open) {
|
||
|
|
BattleUiPauseGate.acquire(token);
|
||
|
|
} else {
|
||
|
|
BattleUiPauseGate.release(token);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private getLoadingUI(): Node | null {
|
||
|
|
return this.loadingUI;
|
||
|
|
}
|
||
|
|
|
||
|
|
private getBootLoading(): Node | null {
|
||
|
|
return this.Loading;
|
||
|
|
}
|
||
|
|
|
||
|
|
private isRuntimePhase(): boolean {
|
||
|
|
return this._enteredGame;
|
||
|
|
}
|
||
|
|
|
||
|
|
private onLoadingWatchdog(): void {
|
||
|
|
gg.game?.onLoadingWatchdogTimeout?.();
|
||
|
|
}
|
||
|
|
}
|