Files
oops-plugin-framework/assets/core/gui/layer/LayerUI.ts

291 lines
11 KiB
TypeScript
Raw 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.
import { instantiate, Node, Prefab, SafeArea } from 'cc';
import { Collection } from 'db://oops-framework/libs/collection/Collection';
import { resLoader } from '../../common/loader/ResLoader';
import { oops } from '../../Oops';
import type { Uiid } from './LayerEnum';
import { LayerHelper } from './LayerHelper';
import type { UIParam } from './LayerUIElement';
import { LayerUIElement, UIState } from './LayerUIElement';
import type { UIConfig } from './UIConfig';
/** 界面层对象 */
export class LayerUI extends Node {
/** 全局窗口打开失败事件 */
onOpenFailure: Function = null!;
/** 显示界面节点集合 */
protected ui_nodes = new Collection<string, UIState>();
/** 被移除的界面缓存数据 */
protected ui_cache = new Map<string, UIState>();
/** 缓存界面的最大数量限制 */
protected readonly MAX_CACHE_SIZE = 10;
/**
* UI基础层允许添加多个预制件节点
* @param name 该层名
*/
constructor(name: string) {
super(name);
LayerHelper.setFullScreen(this);
this.on(Node.EventType.CHILD_ADDED, this.onChildAdded, this);
this.on(Node.EventType.CHILD_REMOVED, this.onChildRemoved, this);
}
protected onChildAdded(child: Node) {
}
protected onChildRemoved(child: Node) {
const comp = child.getComponent(LayerUIElement);
if (comp) {
this.closeUi(comp.state);
}
}
/**
* 添加一个预制件节点到层容器中,该方法将返回一个唯一`uuid`来标识该操作节点
* @param uiid 窗口唯一标识
* @param config 界面配置数据
* @param params 自定义参数
* @returns ture为成功,false为失败
*/
add(uiid: Uiid, config: UIConfig, params?: UIParam): Promise<Node> {
return new Promise<Node>(async (resolve, reject) => {
if (this.ui_nodes.has(config.prefab)) {
const error = `路径为【${config.prefab}】的预制重复加载`;
console.warn(error);
reject(new Error(error));
return;
}
try {
// 检查缓存中是否存界面
const state = this.initUIConfig(uiid, config, params);
await this.load(state);
if (state.node) {
resolve(state.node);
} else {
reject(new Error(`路径为【${config.prefab}】的预制加载失败,节点为空`));
}
} catch (error) {
console.error(`添加界面【${config.prefab}】时发生错误:`, error);
reject(error);
}
});
}
/** 初始化界面配置初始值 */
protected initUIConfig(uiid: Uiid, config: UIConfig, params?: UIParam) {
let state = this.ui_cache.get(config.prefab);
if (state == null) {
if (config.bundle == null) config.bundle = resLoader.defaultBundleName;
if (config.destroy == null) config.destroy = true;
if (config.vacancy == null) config.vacancy = false;
if (config.mask == null) config.mask = false;
if (config.safeArea == null) config.safeArea = false;
state = new UIState();
state.uiid = uiid.toString();
state.config = config;
}
state.params = params ?? {};
state.valid = true;
this.ui_nodes.set(config.prefab, state);
return state;
}
/**
* 加载界面资源
* @param state 显示参数
* @param bundle 远程资源包名,如果为空就是默认本地资源包
*/
protected async load(state: UIState): Promise<Node> {
return new Promise<Node>(async (resolve, reject) => {
// 加载界面资源超时提示
if (state.node == null) {
let timerId: any = null;
try {
timerId = setTimeout(this.onLoadingTimeoutGui, oops.config.game.loadingTimeoutGui);
// 优先加载配置的指定资源包中资源,如果没配置则加载默认资源包资源
const res = await resLoader.load(state.config.bundle!, state.config.prefab, Prefab);
// 检查加载完成后 state 是否已被标记为移除,避免创建僵尸节点
if (!state.valid) {
console.log(`界面【${state.config.prefab}】在加载过程中已被移除,取消实例化`);
resolve(null!);
return;
}
if (res) {
state.node = instantiate(res);
// 是否启动真机安全区域显示
if (state.config.safeArea) state.node.addComponent(SafeArea);
// 窗口事件委托
const comp = state.node.addComponent(LayerUIElement);
comp.state = state;
}
else {
console.warn(`路径为【${state.config.prefab}】的预制加载失败`);
this.failure(state);
}
} finally {
// 确保在所有情况下都清理定时器和关闭等待提示
if (timerId !== null) {
clearTimeout(timerId);
}
oops.gui.waitClose();
}
}
await this.uiInit(state);
resolve(state.node);
});
}
/**
* 创建界面节点
* @param state 视图参数
*/
protected uiInit(state: UIState): Promise<boolean> {
return new Promise<boolean>(async (resolve, reject) => {
// 如果节点为空或已被标记为无效,直接返回失败
if (!state.node || !state.valid) {
resolve(false);
return;
}
const comp = state.node.getComponent(LayerUIElement)!;
const r: boolean = await comp.add();
if (r) {
state.valid = true; // 标记界面为使用状态
if (!state.params.preload) {
state.params.preload = false;
state.node.parent = this;
}
}
else {
console.warn(`路径为【${state.config.prefab}】的自定义预处理逻辑异常.检查预制上绑定的组件中 onAdded 方法,返回true才能正确完成窗口显示流程`);
this.failure(state);
}
resolve(r);
});
}
/** 加载超时事件*/
private onLoadingTimeoutGui() {
oops.gui.waitOpen();
}
/** 窗口关闭事件 */
protected closeUi(state: UIState) {
this.ui_nodes.delete(state.config.prefab);
}
/** 打开窗口失败逻辑 */
protected failure(state: UIState) {
this.closeUi(state);
this.onOpenFailure && this.onOpenFailure();
}
/**
* 根据预制件路径删除,预制件如在队列中也会被删除,如果该预制件存在多个也会一起删除
* @param prefabPath 预制路径
*/
remove(prefabPath: string): void {
const state = this.ui_nodes.get(prefabPath);
if (state) {
// 标记为无效,防止异步加载完成后创建僵尸节点
state.valid = false;
const release: boolean = state.config.destroy!;
// 不释放界面,缓存起来待下次使用
if (release === false) {
this.addToCache(state.config.prefab, state);
}
// 界面移出舞台(增加 node 判空保护,避免异步加载未完成时崩溃)
if (state.node) {
const comp = state.node.getComponent(LayerUIElement);
comp && comp.remove(release);
}
}
}
/** 添加界面到缓存,实现 LRU 策略 */
private addToCache(prefabPath: string, state: UIState) {
// 如果缓存已满,移除最早的缓存项
if (this.ui_cache.size >= this.MAX_CACHE_SIZE) {
const firstKey = this.ui_cache.keys().next().value;
if (firstKey) {
const oldState = this.ui_cache.get(firstKey);
if (oldState) {
this.ui_cache.delete(firstKey);
const comp = oldState.node.getComponent(LayerUIElement);
comp && comp.remove(true);
}
}
}
this.ui_cache.set(prefabPath, state);
}
/** 删除缓存的界面,当调用 remove 移除舞台时,可通过此方法删除缓存界面 */
removeCache(prefabPath: string) {
const state = this.ui_cache.get(prefabPath);
if (state) {
this.ui_cache.delete(prefabPath);
const comp = state.node.getComponent(LayerUIElement);
comp && comp.remove(true);
}
}
/** 显示界面 */
show(prefabPath: string) {
const state = this.ui_nodes.get(prefabPath);
if (state) state.node.parent = this;
}
/**
* 根据预制路径获取已打开界面的节点对象
* @param prefabPath 预制路径
*/
get(prefabPath: string): Node {
const state = this.ui_nodes.get(prefabPath);
if (state) return state.node;
return null!;
}
/**
* 判断当前层是否包含 uuid或预制件路径对应的Node节点
* @param prefabPath 预制件路径或者UUID
*/
has(prefabPath: string): boolean {
return this.ui_nodes.has(prefabPath);
}
/**
* 清除所有节点,队列当中的也删除
* @param isDestroy 移除后是否释放
*/
clear(isDestroy: boolean): void {
// 清除所有显示的界面
const length = this.ui_nodes.array.length - 1;
for (let i = length; i >= 0; i--) {
const uip = this.ui_nodes.array[i];
this.remove(uip.config.prefab);
}
this.ui_nodes.clear();
// 清除缓存中的界面
if (isDestroy) {
this.ui_cache.forEach((value: UIState, prefabPath: string) => {
this.removeCache(prefabPath);
});
}
}
}