mirror of
https://github.com/cocos/cocos-engine.git
synced 2026-11-15 19:47:03 +08:00
1073 lines
37 KiB
TypeScript
1073 lines
37 KiB
TypeScript
/*
|
||
Copyright (c) 2013-2016 Chukong Technologies Inc.
|
||
Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd.
|
||
|
||
http://www.cocos.com
|
||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
of this software and associated engine source code (the "Software"), a limited,
|
||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||
not use Cocos Creator software for developing other software or tools that's
|
||
used for developing games. You are not granted to publish, distribute,
|
||
sublicense, and/or sell copies of Cocos Creator.
|
||
|
||
The software or tools in this License Agreement are licensed, not sold.
|
||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
THE SOFTWARE.
|
||
*/
|
||
|
||
import { EDITOR, HTML5, JSB, PREVIEW, RUNTIME_BASED, TEST } from 'internal:constants';
|
||
import { systemInfo } from 'pal/system-info';
|
||
import { ConfigOrientation } from 'pal/screen-adapter';
|
||
import { IAssetManagerOptions } from './asset-manager/asset-manager';
|
||
import { EventTarget } from './event';
|
||
import { input } from '../input';
|
||
import * as debug from './platform/debug';
|
||
import { Device, DeviceInfo, Swapchain, SwapchainInfo } from './gfx';
|
||
import { sys } from './platform/sys';
|
||
import { macro } from './platform/macro';
|
||
import type { ICustomJointTextureLayout } from '../3d/skeletal-animation/skeletal-animation-utils';
|
||
import { legacyCC, VERSION } from './global-exports';
|
||
import { IPhysicsConfig } from '../physics/framework/physics-config';
|
||
import { bindingMappingInfo } from './pipeline/define';
|
||
import { SplashScreen } from './splash-screen';
|
||
import { RenderPipeline } from './pipeline';
|
||
import { Node } from './scene-graph/node';
|
||
import { BrowserType } from '../../pal/system-info/enum-type';
|
||
import { Layers } from './scene-graph';
|
||
import { log2 } from './math/bits';
|
||
import { garbageCollectionManager } from './data/garbage-collection';
|
||
import { screen } from './platform/screen';
|
||
|
||
interface ISceneInfo {
|
||
url: string;
|
||
uuid: string;
|
||
}
|
||
|
||
export interface LayerItem {
|
||
name: string;
|
||
value: number;
|
||
}
|
||
|
||
/**
|
||
* @zh
|
||
* 游戏配置。
|
||
* @en
|
||
* Game configuration.
|
||
*/
|
||
export interface IGameConfig {
|
||
/**
|
||
* @zh
|
||
* 设置 debug 模式,在浏览器中这个选项会被忽略。
|
||
* @en
|
||
* Set debug mode, only valid in non-browser environment.
|
||
*/
|
||
debugMode?: debug.DebugMode;
|
||
|
||
/**
|
||
* @zh
|
||
* 当 showFPS 为 true 的时候界面的左下角将显示 fps 的信息,否则被隐藏。
|
||
* @en
|
||
* Left bottom corner fps information will show when "showFPS" equals true, otherwise it will be hide.
|
||
*/
|
||
showFPS?: boolean;
|
||
|
||
/**
|
||
* @zh
|
||
* 暴露类名让 Chrome DevTools 可以识别,如果开启会稍稍降低类的创建过程的性能,但对对象构造没有影响。
|
||
* @en
|
||
* Expose class name to chrome debug tools, the class intantiate performance is a little bit slower when exposed.
|
||
*/
|
||
exposeClassName?: boolean;
|
||
|
||
/**
|
||
* @zh
|
||
* 设置想要的帧率你的游戏,但真正的FPS取决于你的游戏实现和运行环境。
|
||
* @en
|
||
* Set the wanted frame rate for your game, but the real fps depends on your game implementation and the running environment.
|
||
*/
|
||
frameRate?: number;
|
||
|
||
/**
|
||
* @zh
|
||
* Web 页面上的 Canvas Element ID,仅适用于 web 端。
|
||
* @en
|
||
* Sets the id of your canvas element on the web page, it's useful only on web.
|
||
*/
|
||
id?: string | HTMLElement;
|
||
|
||
/**
|
||
* @zh
|
||
* 渲染模式。
|
||
* 设置渲染器类型,仅适用于 web 端:
|
||
* - 0 - 通过引擎自动选择。
|
||
* - 1 - 强制使用 canvas 渲染。
|
||
* - 2 - 强制使用 WebGL 渲染,但是在部分 Android 浏览器中这个选项会被忽略。
|
||
* - 3 - 使用空渲染器,可以用于测试和服务器端环境,目前暂时用于 Cocos 内部测试使用
|
||
* @en
|
||
* Sets the renderer type, only useful on web:
|
||
* - 0 - Automatically chosen by engine.
|
||
* - 1 - Forced to use canvas renderer.
|
||
* - 2 - Forced to use WebGL renderer, but this will be ignored on mobile browsers.
|
||
* - 3 - Use Headless Renderer, which is useful in test or server env, only for internal use by cocos team for now
|
||
*/
|
||
renderMode?: 0 | 1 | 2 | 3;
|
||
|
||
/**
|
||
* @zh
|
||
* 当前包中可用场景。
|
||
* @en
|
||
* Include available scenes in the current bundle.
|
||
*/
|
||
scenes?: ISceneInfo[];
|
||
|
||
/**
|
||
* @internal
|
||
* For internal use.
|
||
*/
|
||
jsList?: string[];
|
||
|
||
/**
|
||
* @en
|
||
* Render pipeline resources
|
||
* @zh
|
||
* Render pipeline 资源
|
||
*/
|
||
renderPipeline?: string;
|
||
|
||
/**
|
||
* @en
|
||
* Asset Manager initialization options
|
||
* @zh
|
||
* 资源管理器初始化设置
|
||
*/
|
||
assetOptions?: IAssetManagerOptions;
|
||
|
||
/**
|
||
* @en
|
||
* GPU instancing options
|
||
* @zh
|
||
* GPU instancing 选项
|
||
*/
|
||
customJointTextureLayouts?: ICustomJointTextureLayout[];
|
||
|
||
/**
|
||
* @en
|
||
* Physics system config
|
||
* @zh
|
||
* 物理系统设置
|
||
*/
|
||
physics?: IPhysicsConfig;
|
||
|
||
/**
|
||
* @en
|
||
* User layers config
|
||
* @zh
|
||
* 用户层级设置
|
||
*/
|
||
layers?: LayerItem[];
|
||
|
||
/**
|
||
* @en
|
||
* The adapter stores various platform-related objects.
|
||
* @zh
|
||
* 平台相关选项
|
||
*/
|
||
adapter?: {
|
||
canvas: HTMLCanvasElement,
|
||
frame: HTMLDivElement,
|
||
container: HTMLDivElement,
|
||
[x: string]: any,
|
||
};
|
||
|
||
/**
|
||
* @en
|
||
* The orientation from the builder configuration.
|
||
* Available value can be 'auto', 'landscape', 'portrait'.
|
||
* @zh
|
||
* 屏幕旋转方向,可选 “自动”,“横屏”,“竖屏”
|
||
*/
|
||
orientation?: ConfigOrientation;
|
||
/**
|
||
* @en
|
||
* Determine whether the game frame exact fits the screen.
|
||
* Now it only works on Web platform.
|
||
* @zh
|
||
* 是否让游戏外框对齐到屏幕上,目前只在 web 平台生效
|
||
*/
|
||
exactFitScreen: boolean,
|
||
}
|
||
|
||
/**
|
||
* @en An object to boot the game.
|
||
* @zh 包含游戏主体信息并负责驱动游戏的游戏对象。
|
||
*/
|
||
export class Game extends EventTarget {
|
||
/**
|
||
* @en Event triggered when game hide to background.<br>
|
||
* Please note that this event is not 100% guaranteed to be fired on Web platform,<br>
|
||
* on native platforms, it corresponds to enter background event, os status bar or notification center may not trigger this event.
|
||
* @zh 游戏进入后台时触发的事件。<br>
|
||
* 请注意,在 WEB 平台,这个事件不一定会 100% 触发,这完全取决于浏览器的回调行为。<br>
|
||
* 在原生平台,它对应的是应用被切换到后台事件,下拉菜单和上拉状态栏等不一定会触发这个事件,这取决于系统行为。
|
||
* @example
|
||
* ```ts
|
||
* import { game, audioEngine } from 'cc';
|
||
* game.on(Game.EVENT_HIDE, function () {
|
||
* audioEngine.pauseMusic();
|
||
* audioEngine.pauseAllEffects();
|
||
* });
|
||
* ```
|
||
*/
|
||
public static readonly EVENT_HIDE = 'game_on_hide';
|
||
|
||
/**
|
||
* @en Event triggered when game back to foreground<br>
|
||
* Please note that this event is not 100% guaranteed to be fired on Web platform,<br>
|
||
* on native platforms, it corresponds to enter foreground event.
|
||
* @zh 游戏进入前台运行时触发的事件。<br>
|
||
* 请注意,在 WEB 平台,这个事件不一定会 100% 触发,这完全取决于浏览器的回调行为。<br>
|
||
* 在原生平台,它对应的是应用被切换到前台事件。
|
||
*/
|
||
public static readonly EVENT_SHOW: string = 'game_on_show';
|
||
|
||
/**
|
||
* @en Event triggered when system in low memory status.<br>
|
||
* This event is only triggered on native iOS/Android platform.
|
||
* @zh 程序在内存不足时触发的事件。<br>
|
||
* 该事件只会在 iOS/Android 平台触发。
|
||
*/
|
||
public static readonly EVENT_LOW_MEMORY: string = 'game_on_low_memory';
|
||
|
||
/**
|
||
* @en Event triggered after game inited, at this point all engine objects and game scripts are loaded
|
||
* @zh 游戏启动后的触发事件,此时加载所有的引擎对象和游戏脚本。
|
||
*/
|
||
public static readonly EVENT_GAME_INITED = 'game_inited';
|
||
|
||
/**
|
||
* @en Event triggered after engine inited, at this point you will be able to use all engine classes.<br>
|
||
* It was defined as EVENT_RENDERER_INITED in cocos creator v1.x and renamed in v2.0.
|
||
* Since Cocos Creator v3.0, EVENT_RENDERER_INITED is a new event, look up define for details.
|
||
* @zh 在引擎初始化之后触发的事件,此时您能够使用引擎所有的类。<br>
|
||
* 它在 Cocos Creator v1.x 版本中名字为 EVENT_RENDERER_INITED,在 v2.0 更名为 EVENT_ENGINE_INITED
|
||
* 并在 Cocos Creator v3.0 中将 EVENT_RENDERER_INITED 用作为渲染器初始化的事件。
|
||
*/
|
||
public static readonly EVENT_ENGINE_INITED = 'engine_inited';
|
||
|
||
/**
|
||
* @en Event triggered after renderer inited, at this point you will be able to use all gfx renderer feature.<br>
|
||
* @zh 在渲染器初始化之后触发的事件,此事件在 EVENT_ENGINE_INITED 之前触发,此时开始可使用 gfx 渲染框架。
|
||
*/
|
||
public static readonly EVENT_RENDERER_INITED: string = 'renderer_inited';
|
||
|
||
/**
|
||
* @en Event triggered when game restart
|
||
* @zh 调用restart后,触发事件
|
||
*/
|
||
public static readonly EVENT_RESTART = 'game_on_restart';
|
||
|
||
/**
|
||
* @en Web Canvas 2d API as renderer backend.
|
||
* @zh 使用 Web Canvas 2d API 作为渲染器后端。
|
||
*/
|
||
public static readonly RENDER_TYPE_CANVAS = 0;
|
||
/**
|
||
* @en WebGL API as renderer backend.
|
||
* @zh 使用 WebGL API 作为渲染器后端。
|
||
*/
|
||
public static readonly RENDER_TYPE_WEBGL = 1;
|
||
/**
|
||
* @en OpenGL API as renderer backend.
|
||
* @zh 使用 OpenGL API 作为渲染器后端。
|
||
*/
|
||
public static readonly RENDER_TYPE_OPENGL = 2;
|
||
|
||
/**
|
||
* @en Headless Renderer, usually used in test or server env
|
||
* @zh 空渲染器,通常用于测试环境或服务器端模式
|
||
*/
|
||
public static readonly RENDER_TYPE_HEADLESS = 3;
|
||
|
||
/**
|
||
* @en If delta time since last frame is more than this threshold in seconds,
|
||
* the game timer will consider user is debugging and adjust the delta time to [[frameTime]].
|
||
* @zh 如果距离上一帧的帧间隔超过了这个阈值(单位是 s),那么就会被认为正在调试,帧间隔会被自动调节为 [[frameTime]].
|
||
*/
|
||
public static DEBUG_DT_THRESHOLD = 1;
|
||
|
||
/**
|
||
* @en The outer frame of the game canvas; parent of game container.
|
||
* @zh 游戏画布的外框,container 的父容器。
|
||
*
|
||
* @deprecated since 3.4.0, frame is a concept on web standard, please manager screens via the `screen` module.
|
||
*/
|
||
public frame: HTMLDivElement | null = null;
|
||
/**
|
||
* @en The container of game canvas.
|
||
* @zh 游戏画布的容器。
|
||
*
|
||
* @deprecated since 3.4.0, container is a concept on web standard, please manager screens via the `screen` module.
|
||
*/
|
||
public container: HTMLDivElement | null = null;
|
||
/**
|
||
* @en The canvas of the game.
|
||
* @zh 游戏的画布。
|
||
*/
|
||
public canvas: HTMLCanvasElement | null = null;
|
||
|
||
/**
|
||
* @en The renderer backend of the game.
|
||
* @zh 游戏的渲染器类型。
|
||
*/
|
||
public renderType = -1;
|
||
|
||
public eventTargetOn = super.on;
|
||
public eventTargetOnce = super.once;
|
||
|
||
/**
|
||
* @en
|
||
* The current game configuration,
|
||
* please be noticed any modification directly on this object after the game initialization won't take effect.
|
||
* @zh
|
||
* 当前的游戏配置
|
||
* 注意:请不要直接修改这个对象,它不会有任何效果。
|
||
*/
|
||
public config: NormalizedGameConfig = {} as NormalizedGameConfig;
|
||
|
||
/**
|
||
* @en Callback when the scripts of engine have been load.
|
||
* @zh 当引擎完成启动后的回调函数。
|
||
* @method onStart
|
||
*/
|
||
public onStart: Game.OnStart | null = null;
|
||
|
||
/**
|
||
* @en Indicates whether the engine and the renderer has been initialized
|
||
* @zh 引擎和渲染器是否以完成初始化
|
||
*/
|
||
public get inited () {
|
||
return this._inited;
|
||
}
|
||
|
||
/**
|
||
* @en Expected frame rate of the game.
|
||
* @zh 游戏的设定帧率。
|
||
*/
|
||
public get frameRate () {
|
||
return this._frameRate;
|
||
}
|
||
public set frameRate (frameRate: number | string) {
|
||
if (typeof frameRate !== 'number') {
|
||
frameRate = parseInt(frameRate, 10);
|
||
if (Number.isNaN(frameRate)) {
|
||
frameRate = 60;
|
||
}
|
||
}
|
||
this._frameRate = frameRate;
|
||
this.frameTime = 1000 / frameRate;
|
||
this._setAnimFrame();
|
||
}
|
||
|
||
/**
|
||
* @en The delta time since last frame, unit: s.
|
||
* @zh 获取上一帧的增量时间,以秒为单位。
|
||
*/
|
||
public get deltaTime () {
|
||
return this._deltaTime;
|
||
}
|
||
|
||
/**
|
||
* @en The total passed time since game start, unit: ms
|
||
* @zh 获取从游戏开始到现在总共经过的时间,以毫秒为单位
|
||
*/
|
||
public get totalTime () {
|
||
return performance.now() - this._initTime;
|
||
}
|
||
|
||
/**
|
||
* @en The start time of the current frame in milliseconds.
|
||
* @zh 获取当前帧开始的时间(以 ms 为单位)。
|
||
*/
|
||
public get frameStartTime () {
|
||
return this._startTime;
|
||
}
|
||
|
||
/**
|
||
* @en The expected delta time of each frame in milliseconds
|
||
* @zh 期望帧率对应的每帧时间(以 ms 为单位)
|
||
*/
|
||
public frameTime = 1000 / 60;
|
||
|
||
public collisionMatrix = [];
|
||
public groupList: any[] = [];
|
||
|
||
/**
|
||
* @deprecated since v3.5.0, this is an engine private interface that will be removed in the future.
|
||
*/
|
||
public _persistRootNodes = {};
|
||
|
||
/**
|
||
* @deprecated since v3.5.0, this is an engine private interface that will be removed in the future.
|
||
*/
|
||
public _gfxDevice: Device | null = null;
|
||
/**
|
||
* @deprecated since v3.5.0, this is an engine private interface that will be removed in the future.
|
||
*/
|
||
public _swapchain: Swapchain | null = null;
|
||
// states
|
||
/**
|
||
* @deprecated since v3.5.0, this is an engine private interface that will be removed in the future.
|
||
*/
|
||
public _configLoaded = false; // whether config loaded
|
||
/**
|
||
* @deprecated since v3.5.0, this is an engine private interface that will be removed in the future.
|
||
*/
|
||
public _isCloning = false; // deserializing or instantiating
|
||
private _inited = false;
|
||
private _engineInited = false; // whether the engine has inited
|
||
private _rendererInitialized = false;
|
||
private _paused = true;
|
||
// frame control
|
||
private _frameRate = 60;
|
||
private _intervalId = 0; // interval target of main
|
||
private _initTime = 0;
|
||
private _startTime = 0;
|
||
private _deltaTime = 0.0;
|
||
private declare _frameCB: (time: number) => void;
|
||
private _onEngineInitedCallback: Array<() => (Promise<void> | void)> = [];
|
||
|
||
// @Methods
|
||
|
||
// @Game play control
|
||
|
||
/**
|
||
* @en Set frame rate of game.
|
||
* @zh 设置游戏帧率。
|
||
* @deprecated since v3.3.0 please use [[game.frameRate]]
|
||
*/
|
||
public setFrameRate (frameRate: number | string) {
|
||
this.frameRate = frameRate;
|
||
}
|
||
|
||
/**
|
||
* @en Get frame rate set for the game, it doesn't represent the real frame rate.
|
||
* @zh 获取设置的游戏帧率(不等同于实际帧率)。
|
||
* @return frame rate
|
||
* @deprecated since v3.3.0 please use [[game.frameRate]]
|
||
*/
|
||
public getFrameRate (): number {
|
||
return this.frameRate as number;
|
||
}
|
||
|
||
/**
|
||
* @en Run the game frame by frame with a fixed delta time correspond to frame rate.
|
||
* @zh 以固定帧间隔执行一帧游戏循环,帧间隔与设定的帧率匹配。
|
||
*/
|
||
public step () {
|
||
legacyCC.director.tick(this.frameTime / 1000);
|
||
}
|
||
|
||
/**
|
||
* @en Pause the game main loop. This will pause:
|
||
* - game logic execution
|
||
* - rendering process
|
||
* - input event dispatching (excluding Web and Minigame platforms)
|
||
*
|
||
* This is different with `director.pause()` which only pause the game logic execution.
|
||
*
|
||
* @zh 暂停游戏主循环。包含:
|
||
* - 游戏逻辑
|
||
* - 渲染
|
||
* - 输入事件派发(Web 和小游戏平台除外)
|
||
*
|
||
* 这点和只暂停游戏逻辑的 `director.pause()` 不同。
|
||
*/
|
||
public pause () {
|
||
if (this._paused) { return; }
|
||
this._paused = true;
|
||
if (this._intervalId) {
|
||
window.cAF(this._intervalId);
|
||
this._intervalId = 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @en Resume the game from pause. This will resume:<br>
|
||
* game logic execution, rendering process, event manager, background music and all audio effects.<br>
|
||
* @zh 恢复游戏主循环。包含:游戏逻辑,渲染,事件处理,背景音乐和所有音效。
|
||
*/
|
||
public resume () {
|
||
if (!this._paused) { return; }
|
||
// @ts-expect-error _clearEvents is a private method.
|
||
input._clearEvents();
|
||
if (this._intervalId) {
|
||
window.cAF(this._intervalId);
|
||
this._intervalId = 0;
|
||
}
|
||
this._paused = false;
|
||
this._updateCallback();
|
||
this._intervalId = window.rAF(this._frameCB);
|
||
}
|
||
|
||
/**
|
||
* @en Check whether the game is paused.
|
||
* @zh 判断游戏是否暂停。
|
||
*/
|
||
public isPaused (): boolean {
|
||
return this._paused;
|
||
}
|
||
|
||
/**
|
||
* @en Restart game.
|
||
* @zh 重新开始游戏
|
||
*/
|
||
public restart (): Promise<void> {
|
||
const endFramePromise = new Promise<void>((resolve) => legacyCC.director.once(legacyCC.Director.EVENT_END_FRAME, () => resolve()) as void);
|
||
return endFramePromise.then(() => {
|
||
for (const id in this._persistRootNodes) {
|
||
this.removePersistRootNode(this._persistRootNodes[id]);
|
||
}
|
||
|
||
// Clear scene
|
||
legacyCC.director.getScene().destroy();
|
||
legacyCC.Object._deferredDestroy();
|
||
|
||
legacyCC.director.reset();
|
||
legacyCC.profiler.reset();
|
||
this.pause();
|
||
return this._setRenderPipelineNShowSplash().then(() => {
|
||
this.resume();
|
||
this._safeEmit(Game.EVENT_RESTART);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @en End game, it will close the game window
|
||
* @zh 退出游戏
|
||
*/
|
||
public end () {
|
||
systemInfo.close();
|
||
}
|
||
|
||
/**
|
||
* @en
|
||
* Register an callback of a specific event type on the game object.<br>
|
||
* This type of event should be triggered via `emit`.<br>
|
||
* @zh
|
||
* 注册 game 的特定事件类型回调。这种类型的事件应该被 `emit` 触发。<br>
|
||
*
|
||
* @param type - A string representing the event type to listen for.
|
||
* @param callback - The callback that will be invoked when the event is dispatched.<br>
|
||
* The callback is ignored if it is a duplicate (the callbacks are unique).
|
||
* @param target - The target (this object) to invoke the callback, can be null
|
||
* @param once - After the first invocation, whether the callback should be unregistered.
|
||
* @return - Just returns the incoming callback so you can save the anonymous function easier.
|
||
*/
|
||
public on (type: string, callback: () => void, target?: any, once?: boolean): any {
|
||
// Make sure EVENT_ENGINE_INITED callbacks to be invoked
|
||
if ((this._engineInited && type === Game.EVENT_ENGINE_INITED)
|
||
|| (this._inited && type === Game.EVENT_GAME_INITED)
|
||
|| (this._rendererInitialized && type === Game.EVENT_RENDERER_INITED)) {
|
||
callback.call(target);
|
||
}
|
||
return this.eventTargetOn(type, callback, target, once);
|
||
}
|
||
|
||
/**
|
||
* @en
|
||
* Register an callback of a specific event type on the game object,<br>
|
||
* the callback will remove itself after the first time it is triggered.<br>
|
||
* @zh
|
||
* 注册 game 的特定事件类型回调,回调会在第一时间被触发后删除自身。
|
||
*
|
||
* @param type - A string representing the event type to listen for.
|
||
* @param callback - The callback that will be invoked when the event is dispatched.<br>
|
||
* The callback is ignored if it is a duplicate (the callbacks are unique).
|
||
* @param target - The target (this object) to invoke the callback, can be null
|
||
*/
|
||
public once (type: string, callback: () => void, target?: any): any {
|
||
// Make sure EVENT_ENGINE_INITED callbacks to be invoked
|
||
if (this._engineInited && type === Game.EVENT_ENGINE_INITED) {
|
||
return callback.call(target);
|
||
}
|
||
return this.eventTargetOnce(type, callback, target);
|
||
}
|
||
|
||
/**
|
||
* @en Init game with configuration object.
|
||
* @zh 使用指定的配置初始化引擎。
|
||
* @param config - Pass configuration object
|
||
*/
|
||
public init (config: IGameConfig) {
|
||
this._initConfig(config);
|
||
// TODO: unify the screen initialization workflow.
|
||
if (HTML5) {
|
||
// @ts-expect-error access private method.
|
||
screen._init({
|
||
configOrientation: config.orientation || 'auto',
|
||
exactFitScreen: config.exactFitScreen,
|
||
});
|
||
}
|
||
// Init assetManager
|
||
if (this.config.assetOptions) {
|
||
legacyCC.assetManager.init(this.config.assetOptions);
|
||
}
|
||
|
||
if (this.config.layers) {
|
||
const userLayers: LayerItem[] = this.config.layers;
|
||
for (let i = 0; i < userLayers.length; i++) {
|
||
const layer = userLayers[i];
|
||
const bitNum = log2(layer.value);
|
||
Layers.addLayer(layer.name, bitNum);
|
||
}
|
||
}
|
||
|
||
return this._initEngine().then(() => {
|
||
if (!EDITOR) {
|
||
this._initEvents();
|
||
}
|
||
|
||
if (legacyCC.director.root && legacyCC.director.root.dataPoolManager) {
|
||
legacyCC.director.root.dataPoolManager.jointTexturePool.registerCustomTextureLayouts(config.customJointTextureLayouts);
|
||
}
|
||
return this._engineInited;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @en Run game with configuration object and onStart function.
|
||
* @zh 运行游戏,并且指定引擎配置和 onStart 的回调。
|
||
* @param onStart - function to be executed after game initialized
|
||
*/
|
||
public run(onStart?: Game.OnStart): Promise<void>;
|
||
|
||
public run (configOrCallback?: Game.OnStart | IGameConfig, onStart?: Game.OnStart) {
|
||
// To compatible with older version,
|
||
// we allow the `run(config, onstart?)` form. But it's deprecated.
|
||
let initPromise: Promise<boolean> | undefined;
|
||
if (typeof configOrCallback !== 'function' && configOrCallback) {
|
||
initPromise = this.init(configOrCallback);
|
||
this.onStart = onStart ?? null;
|
||
} else {
|
||
this.onStart = configOrCallback ?? null;
|
||
}
|
||
garbageCollectionManager.init();
|
||
|
||
return Promise.resolve(initPromise).then(() => this._setRenderPipelineNShowSplash()).then(() => {
|
||
if (!HTML5) {
|
||
// @ts-expect-error access private method.
|
||
screen._init({
|
||
configOrientation: 'auto',
|
||
exactFitScreen: true,
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// @ Persist root node section
|
||
/**
|
||
* @en
|
||
* Add a persistent root node to the game, the persistent node won't be destroyed during scene transition.<br>
|
||
* The target node must be placed in the root level of hierarchy, otherwise this API won't have any effect.
|
||
* @zh
|
||
* 声明常驻根节点,该节点不会在场景切换中被销毁。<br>
|
||
* 目标节点必须位于为层级的根节点,否则无效。
|
||
* @param node - The node to be made persistent
|
||
*/
|
||
public addPersistRootNode (node: Node) {
|
||
if (!legacyCC.Node.isNode(node) || !node.uuid) {
|
||
debug.warnID(3800);
|
||
return;
|
||
}
|
||
const id = node.uuid;
|
||
if (!this._persistRootNodes[id]) {
|
||
const scene = legacyCC.director._scene;
|
||
if (legacyCC.isValid(scene)) {
|
||
if (!node.parent) {
|
||
node.parent = scene;
|
||
} else if (!(node.parent instanceof legacyCC.Scene)) {
|
||
debug.warnID(3801);
|
||
return;
|
||
} else if (node.parent !== scene) {
|
||
debug.warnID(3802);
|
||
return;
|
||
} else {
|
||
node._originalSceneId = scene.uuid;
|
||
}
|
||
}
|
||
this._persistRootNodes[id] = node;
|
||
node._persistNode = true;
|
||
legacyCC.assetManager._releaseManager._addPersistNodeRef(node);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @en Remove a persistent root node.
|
||
* @zh 取消常驻根节点。
|
||
* @param node - The node to be removed from persistent node list
|
||
*/
|
||
public removePersistRootNode (node: Node) {
|
||
const id = node.uuid || '';
|
||
if (node === this._persistRootNodes[id]) {
|
||
delete this._persistRootNodes[id];
|
||
node._persistNode = false;
|
||
node._originalSceneId = '';
|
||
legacyCC.assetManager._releaseManager._removePersistNodeRef(node);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @en Check whether the node is a persistent root node.
|
||
* @zh 检查节点是否是常驻根节点。
|
||
* @param node - The node to be checked
|
||
*/
|
||
public isPersistRootNode (node: { _persistNode: any; }): boolean {
|
||
return !!node._persistNode;
|
||
}
|
||
|
||
/**
|
||
* TODO: Only hack for PhysX initialization, should be removed in future
|
||
* @internal
|
||
*/
|
||
public onEngineInitedAsync (func: () => (Promise<void> | void)) {
|
||
this._onEngineInitedCallback.push(func);
|
||
}
|
||
|
||
// @Engine loading
|
||
|
||
private _initEngine () {
|
||
this._initDevice();
|
||
const director = legacyCC.director;
|
||
return Promise.resolve(director._init()).then(() => {
|
||
legacyCC.view.init();
|
||
// Log engine version
|
||
debug.log(`Cocos Creator v${VERSION}`);
|
||
this.emit(Game.EVENT_ENGINE_INITED);
|
||
this._engineInited = true;
|
||
if (legacyCC.internal.dynamicAtlasManager) { legacyCC.internal.dynamicAtlasManager.enabled = !macro.CLEANUP_IMAGE_CACHE; }
|
||
}).then(() => {
|
||
const initCallbackPromises = this._onEngineInitedCallback.map((func) => func()).filter(Boolean);
|
||
this._onEngineInitedCallback.length = 0;
|
||
return Promise.all(initCallbackPromises);
|
||
});
|
||
}
|
||
|
||
// @Methods
|
||
|
||
// @Time ticker section
|
||
private _setAnimFrame () {
|
||
const frameRate = this._frameRate;
|
||
if (JSB) {
|
||
// @ts-expect-error JSB Call
|
||
jsb.setPreferredFramesPerSecond(frameRate);
|
||
window.rAF = window.requestAnimationFrame;
|
||
window.cAF = window.cancelAnimationFrame;
|
||
} else {
|
||
const rAF = window.requestAnimationFrame = window.requestAnimationFrame
|
||
|| window.webkitRequestAnimationFrame
|
||
|| window.mozRequestAnimationFrame
|
||
|| window.oRequestAnimationFrame
|
||
|| window.msRequestAnimationFrame;
|
||
if (frameRate !== 60 && frameRate !== 30) {
|
||
window.rAF = this._stTime.bind(this);
|
||
window.cAF = this._ctTime;
|
||
} else {
|
||
window.rAF = rAF || this._stTime.bind(this);
|
||
window.cAF = window.cancelAnimationFrame
|
||
|| window.cancelRequestAnimationFrame
|
||
|| window.msCancelRequestAnimationFrame
|
||
|| window.mozCancelRequestAnimationFrame
|
||
|| window.oCancelRequestAnimationFrame
|
||
|| window.webkitCancelRequestAnimationFrame
|
||
|| window.msCancelAnimationFrame
|
||
|| window.mozCancelAnimationFrame
|
||
|| window.webkitCancelAnimationFrame
|
||
|| window.ocancelAnimationFrame
|
||
|| this._ctTime;
|
||
|
||
// update callback function for 30 fps version
|
||
this._updateCallback();
|
||
}
|
||
}
|
||
}
|
||
private _stTime (callback: () => void) {
|
||
const currTime = performance.now();
|
||
const elapseTime = Math.max(0, (currTime - this._startTime));
|
||
const timeToCall = Math.max(0, this.frameTime - elapseTime);
|
||
const id = window.setTimeout(callback, timeToCall);
|
||
return id;
|
||
}
|
||
private _ctTime (id: number | undefined) {
|
||
window.clearTimeout(id);
|
||
}
|
||
private _calculateDT (now?: number) {
|
||
if (!now) now = performance.now();
|
||
this._deltaTime = now > this._startTime ? (now - this._startTime) / 1000 : 0;
|
||
if (this._deltaTime > Game.DEBUG_DT_THRESHOLD) {
|
||
this._deltaTime = this.frameTime / 1000;
|
||
}
|
||
this._startTime = now;
|
||
return this._deltaTime;
|
||
}
|
||
|
||
private _updateCallback () {
|
||
const director = legacyCC.director;
|
||
let callback;
|
||
if (!JSB && !RUNTIME_BASED && this._frameRate === 30) {
|
||
let skip = true;
|
||
callback = (time: number) => {
|
||
this._intervalId = window.rAF(this._frameCB);
|
||
skip = !skip;
|
||
if (skip) {
|
||
return;
|
||
}
|
||
director.tick(this._calculateDT(time));
|
||
};
|
||
} else {
|
||
callback = (time: number) => {
|
||
director.tick(this._calculateDT(time));
|
||
this._intervalId = window.rAF(this._frameCB);
|
||
};
|
||
}
|
||
this._frameCB = callback;
|
||
}
|
||
|
||
// Run game.
|
||
private _runMainLoop () {
|
||
if (!this._inited || (EDITOR && !legacyCC.GAME_VIEW)) {
|
||
return;
|
||
}
|
||
const config = this.config;
|
||
const director = legacyCC.director;
|
||
|
||
debug.setDisplayStats(!!config.showFPS);
|
||
director.startAnimation();
|
||
this.resume();
|
||
}
|
||
|
||
// @Game loading section
|
||
private _initConfig (config: IGameConfig) {
|
||
// Configs adjustment
|
||
if (typeof config.debugMode !== 'number') {
|
||
config.debugMode = debug.DebugMode.NONE;
|
||
}
|
||
config.exposeClassName = !!config.exposeClassName;
|
||
if (typeof config.frameRate !== 'number') {
|
||
config.frameRate = 60;
|
||
}
|
||
const renderMode = config.renderMode;
|
||
if (typeof renderMode !== 'number' || renderMode > 3 || renderMode < 0) {
|
||
config.renderMode = 0;
|
||
}
|
||
config.showFPS = !!config.showFPS;
|
||
|
||
debug._resetDebugSetting(config.debugMode);
|
||
|
||
this.config = config as NormalizedGameConfig;
|
||
this._configLoaded = true;
|
||
|
||
this.frameRate = config.frameRate;
|
||
}
|
||
|
||
private _determineRenderType () {
|
||
const config = this.config;
|
||
const userRenderMode = parseInt(config.renderMode as any, 10);
|
||
|
||
// Determine RenderType
|
||
this.renderType = Game.RENDER_TYPE_CANVAS;
|
||
let supportRender = false;
|
||
|
||
if (userRenderMode === 1) {
|
||
this.renderType = Game.RENDER_TYPE_CANVAS;
|
||
supportRender = true;
|
||
} else if (userRenderMode === 0 || userRenderMode === 2) {
|
||
this.renderType = Game.RENDER_TYPE_WEBGL;
|
||
supportRender = true;
|
||
} else if (userRenderMode === 3) {
|
||
this.renderType = Game.RENDER_TYPE_HEADLESS;
|
||
supportRender = true;
|
||
}
|
||
|
||
if (!supportRender) {
|
||
throw new Error(debug.getError(3820, userRenderMode));
|
||
}
|
||
}
|
||
|
||
private _initDevice () {
|
||
// Avoid setup to be called twice.
|
||
if (this._rendererInitialized) { return; }
|
||
|
||
// Obtain platform-related objects through the adapter
|
||
const adapter = this.config.adapter;
|
||
if (adapter) {
|
||
this.canvas = adapter.canvas;
|
||
this.frame = adapter.frame;
|
||
this.container = adapter.container;
|
||
}
|
||
|
||
this._determineRenderType();
|
||
|
||
// WebGL context created successfully
|
||
if (this.renderType === Game.RENDER_TYPE_WEBGL) {
|
||
const deviceInfo = new DeviceInfo(bindingMappingInfo);
|
||
|
||
if (JSB && window.gfx) {
|
||
this._gfxDevice = gfx.DeviceManager.create(deviceInfo);
|
||
} else {
|
||
let useWebGL2 = (!!window.WebGL2RenderingContext);
|
||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||
if (userAgent.indexOf('safari') !== -1 && userAgent.indexOf('chrome') === -1
|
||
|| sys.browserType === BrowserType.UC // UC browser implementation doesn't conform to WebGL2 standard
|
||
) {
|
||
useWebGL2 = false;
|
||
}
|
||
|
||
const deviceCtors: Constructor<Device>[] = [];
|
||
if (useWebGL2 && legacyCC.WebGL2Device) {
|
||
deviceCtors.push(legacyCC.WebGL2Device);
|
||
}
|
||
if (legacyCC.WebGLDevice) {
|
||
deviceCtors.push(legacyCC.WebGLDevice);
|
||
}
|
||
if (legacyCC.EmptyDevice) {
|
||
deviceCtors.push(legacyCC.EmptyDevice);
|
||
}
|
||
|
||
Device.canvas = this.canvas!;
|
||
for (let i = 0; i < deviceCtors.length; i++) {
|
||
this._gfxDevice = new deviceCtors[i]();
|
||
if (this._gfxDevice.initialize(deviceInfo)) { break; }
|
||
}
|
||
}
|
||
} else if (this.renderType === Game.RENDER_TYPE_HEADLESS && legacyCC.EmptyDevice) {
|
||
this._gfxDevice = new legacyCC.EmptyDevice();
|
||
this._gfxDevice!.initialize(new DeviceInfo(bindingMappingInfo));
|
||
}
|
||
|
||
if (!this._gfxDevice) {
|
||
// todo fix here for wechat game
|
||
debug.error('can not support canvas rendering in 3D');
|
||
this.renderType = Game.RENDER_TYPE_CANVAS;
|
||
return;
|
||
}
|
||
|
||
const swapchainInfo = new SwapchainInfo(this.canvas!);
|
||
const windowSize = screen.windowSize;
|
||
swapchainInfo.width = windowSize.width;
|
||
swapchainInfo.height = windowSize.height;
|
||
this._swapchain = this._gfxDevice.createSwapchain(swapchainInfo);
|
||
|
||
this.canvas!.oncontextmenu = () => false;
|
||
}
|
||
|
||
private _initEvents () {
|
||
systemInfo.on('show', this._onShow, this);
|
||
systemInfo.on('hide', this._onHide, this);
|
||
}
|
||
|
||
private _onHide () {
|
||
this.emit(Game.EVENT_HIDE);
|
||
this.pause();
|
||
}
|
||
|
||
private _onShow () {
|
||
this.emit(Game.EVENT_SHOW);
|
||
this.resume();
|
||
}
|
||
|
||
private _setRenderPipelineNShowSplash () {
|
||
return Promise.resolve(this._setupRenderPipeline()).then(
|
||
() => Promise.resolve(this._showSplashScreen()).then(
|
||
() => {
|
||
this._inited = true;
|
||
this._initTime = performance.now();
|
||
this._runMainLoop();
|
||
this._safeEmit(Game.EVENT_GAME_INITED);
|
||
if (this.onStart) {
|
||
this.onStart();
|
||
}
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
private _setupRenderPipeline () {
|
||
const { renderPipeline } = this.config;
|
||
if (!renderPipeline) {
|
||
return this._setRenderPipeline();
|
||
}
|
||
return new Promise<RenderPipeline>((resolve, reject) => {
|
||
legacyCC.assetManager.loadAny(renderPipeline, (err, asset) => ((err || !(asset instanceof RenderPipeline))
|
||
? reject(err)
|
||
: resolve(asset)));
|
||
}).then((asset) => {
|
||
this._setRenderPipeline(asset);
|
||
}).catch((reason) => {
|
||
debug.warn(reason);
|
||
debug.warn(`Failed load render pipeline: ${renderPipeline}, engine failed to initialize, will fallback to default pipeline`);
|
||
this._setRenderPipeline();
|
||
});
|
||
}
|
||
|
||
private _showSplashScreen () {
|
||
if (!EDITOR && !PREVIEW && legacyCC.internal.SplashScreen) {
|
||
const splashScreen = legacyCC.internal.SplashScreen.instance as SplashScreen;
|
||
splashScreen.main(legacyCC.director.root);
|
||
return new Promise<void>((resolve) => {
|
||
splashScreen.setOnFinish(() => resolve());
|
||
splashScreen.loadFinish = true;
|
||
});
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private _setRenderPipeline (rppl?: RenderPipeline) {
|
||
if (!legacyCC.director.root.setRenderPipeline(rppl)) {
|
||
this._setRenderPipeline();
|
||
}
|
||
|
||
this._rendererInitialized = true;
|
||
this._safeEmit(Game.EVENT_RENDERER_INITED);
|
||
}
|
||
|
||
private _safeEmit (event) {
|
||
if (EDITOR) {
|
||
try {
|
||
this.emit(event);
|
||
} catch (e) {
|
||
debug.warn(e);
|
||
}
|
||
} else {
|
||
this.emit(event);
|
||
}
|
||
}
|
||
}
|
||
|
||
export declare namespace Game {
|
||
export type OnStart = () => void;
|
||
}
|
||
|
||
legacyCC.Game = Game;
|
||
|
||
/**
|
||
* @en
|
||
* This is a Game instance.
|
||
* @zh
|
||
* 这是一个 Game 类的实例,包含游戏主体信息并负责驱动游戏的游戏对象。
|
||
*/
|
||
export const game = legacyCC.game = new Game();
|
||
|
||
type NormalizedGameConfig = IGameConfig & {
|
||
frameRate: NonNullable<IGameConfig['frameRate']>;
|
||
};
|