游戏开发实现简易实用的ui框架
游戏开发实现简易实用的ui框架
本文使用
cocos
引擎实现,框架代码本质上不依赖某一个引擎,稍作修改也能作为其他引擎的实现
1.1 UI管理框架的核心需求剖析
- 分层与类型管理
- 对不同类型UI需要进行分层管理。
- 不同层级的UI需要有不同的父节点,保证渲染顺序和交互优先级。
- 资源管理与预加载
- 支持动态加载UI资源,并缓存已加载的资源。
- 提供预加载功能,提前加载高频使用的UI。
- 实例化与复用
- 管理UI实例的创建和销毁,避免重复创建。
- 通过缓存机制复用UI实例,减少性能开销。
- 显示与隐藏管理
- 提供便捷的API控制UI的显示与隐藏。
- 能自动控制UI层级。
- 事件与参数传递
- 支持通过参数初始化UI,方便UI组件根据业务逻辑动态变化。
1.2 探讨UI层级的问题
在做UI管理的时候层级关系是一个常见的问题,特别是在动态加载时由于设计不当或忽视一些细节,从而导致层级混乱、显示错误或者UI交互问题。
毛毛虫认为解决这个问题的一个方式就是进行UI分类管理,只要根据UI的类型对其层级加以区分,不同类型的UI展示具有不同的特性。
在本次的设计中将UI区分为四个UI层(全屏视图、弹框、常驻UI、提示视图)。
全屏视图是优先级最低的UI,显示在所有视图之下,同一时间只会存在一个全屏视图。
常驻UI通常是底部或顶部的UI多用于全屏视图的切换,优先级只比全屏视图高。
弹框是游戏中最多的UI,具有较高的优先级,同时可以出现多个,越晚出现的弹框层级越高。
提示视图是优先级最高的UI,提示视图不是指提示框,提示框应该被归类为弹框,常见的提示视图有加载界面、过场动画、网络故障提示等。多数情况提示视图不会同时出现多个,如果存在多个越晚出现的层级应当越高。
1.3 UI基类的设计和自定义生命周期的封装
基类模板:
export default abstract class BaseView extends cc.Component implements IBaseView{
UIType: UI_TYPE;
private _params: any;
get params(){
return this._params;
}
set params(v) {
this._params = v;
}
onLoad(){
//#region 公共事件
//#endregion
this.OnMountEnd();
}
/**挂载后事件,作OnLoad使用 */
protected abstract OnMountEnd();
protected onEnable(): void {
this.ShowUI(this.params);
}
abstract ShowUI(params: any);
HideUI() {
//#region 公共事件
//#endregion
this.node.active = false;
}
}
export abstract class FullView extends BaseView{
UIType: UI_TYPE = UI_TYPE.FULL;
}
export abstract class PopView extends BaseView{
UIType: UI_TYPE = UI_TYPE.POP;
}
export abstract class TipView extends BaseView{
UIType: UI_TYPE = UI_TYPE.FULL;
}
export abstract class ResidentView extends BaseView{
UIType: UI_TYPE = UI_TYPE.RESIDENT;
}
export enum UI_TYPE{
/**全屏视图 */
FULL,
/**弹出视图 */
POP,
/**提示视图 */
TIP,
/**常驻视图 */
RESIDENT
}
interface IBaseView{
UIType: UI_TYPE;
/**显示UI事件 */
ShowUI(params: any);
}
基类设计的特点与目的:
- 通用性
BaseView
提供了一个统一的结构,所有具体的 UI 类都继承自此类,IBaseView
给予 UI 类一些行为约束。- 生命周期的封装
- 使用
OnMountEnd
替代了onLoad
,将公共逻辑与具体实现分离。确保了在onLoad
执行一些通用逻辑(如资源加载、事件注册)后,具体的子类可以通过OnMountEnd
来完成特定的初始化工作。- ShowUI:UI 的初始化和展示逻辑,在 UI 被展示时调用,可以根据传入的参数动态初始化视图内容提升灵活性,可以理解为可以传参的激活事件。
- HideUI:为视图的销毁提供一个统一的入口,避免直接调用
node.active = false
导致遗漏清理逻辑,为后续的扩展(如内存回收、动画关闭等)提供了统一的触发点;- UI 类型的标记
- 基于
UI_TYPE
枚举定义了四种 UI 类型,并通过抽象类(FullView
、PopView
等)进行进一步的封装。- 子类通过继承对应的抽象类,自动绑定 UI 类型,减少重复代码。
1.4 UI管理器的封装
UI管理器模板:
import BaseView, { UI_TYPE } from "./BaseView";
const UIPath: string = "prefab/UI/";
export default class UIManager {
private isInited: boolean = false;
private static instance: UIManager = null;
private FullParentNode: cc.Node = null;
private PopParentNode: cc.Node = null;
private TipParentNode: cc.Node = null;
private ResidentParentNode: cc.Node = null;
public static get inst() {
if (this.instance == null) {
this.instance = new UIManager();
}
return this.instance;
}
public initUIManager(
fullParentNode: cc.Node,
popParentNode: cc.Node,
tipParentNode: cc.Node = null,
residentParentNode: cc.Node = null) {
this.FullParentNode = fullParentNode;
this.PopParentNode = popParentNode;
this.TipParentNode = tipParentNode;
this.ResidentParentNode = residentParentNode;
}
private UIcache: { [key: string]: BaseView } = {};
private onView: BaseView = null;
public ShowUI(ViewClass: string, params?) {
let view: BaseView = this.UIcache[ViewClass];
if (view == null) {
this.instacePrefab(ViewClass, true, params)
return;
}
if (view.node.active) {
console.error("请勿重复加载");
return;
}
view.params = params;
this.Mount(view);
view.node.active = true;
}
public HideUI(ViewClass: string) {
let view: BaseView = this.UIcache[ViewClass];
view && view.HideUI();
}
public preloadUI(uiViews: string[]) {
if (this.isInited) {
return;
}
this.isInited = true;
uiViews.forEach(uiView => this.instacePrefab(uiView));
}
private instacePrefab(UIName: string, isShow: boolean = false, params?: any) {
if (this.UIcache[UIName]) {
return;
}
cc.resources.load(UIPath + UIName, cc.Prefab, (err, assert) => {
if (err) {
console.error(`没有${UIName}页面,路径信息:${UIPath + UIName}`);
return;
}
let uiNode: cc.Node = cc.instantiate(assert as cc.Prefab);
let uiView: BaseView = uiNode.getComponent(BaseView);
if (uiView == null) {
console.error(`${UIName}没有挂载脚本,路径信息:${UIPath + UIName}`);
return;
}
this.UIcache[assert.name] = uiView;
uiView.params = params;
this.Mount(uiView);
uiNode.active = isShow;
})
}
private Mount(view: BaseView) {
switch (view.UIType) {
case UI_TYPE.FULL:
this.onView && this.onView.HideUI();
this.onView = view;
view.node.parent = this.FullParentNode;
break;
case UI_TYPE.POP:
view.node.setSiblingIndex(999);
view.node.parent = this.PopParentNode;
break;
case UI_TYPE.TIP:
view.node.setSiblingIndex(999);
view.node.parent = this.TipParentNode;
break;
case UI_TYPE.RESIDENT:
view.node.parent = this.ResidentParentNode;
break;
}
}
public hideResidentNode() {
this.ResidentParentNode.children.forEach(nod => nod.active = false);
}
public HideAll() {
Object.keys(this.UIcache).forEach(key => {
let view: BaseView = this.UIcache[key];
if (view.node.active) {
view.HideUI();
}
})
}
public getView(ViewClass: new () => BaseView): BaseView {
let view: BaseView = this.UIcache[ViewClass.name];
if (view == null) {
console.error(`页面${ViewClass.name}不存在`);
return null;
}
return view;
}
}
UIManager 是整个项目的 UI 管理框架核心模块,主要用于加载、显示、隐藏以及管理 UI 层级,通过分层管理和缓存机制使 UI 管理更为结构化、高效化;
单例模式
get inst
通过单例模式实现全局唯一实例,避免多个实例造成管理混乱。但大型项目最好是实现一个 IOC 容器管理,避免实例滥用;UI显示管理
ShowUI
方法用于控制 UI 的激活,记录 UI 视图的初始化参数,- 节点生命周期
onLoad
的运行时机在第一次挂载时,而节点实例如果默认是激活状态会出现自动执行onEnable
,从而导致参数未记录就执行 UI 视图的ShowUI
事件,故参数记录应当在挂载之前,当然也可以通过手动将节点设置为非激活状态;- 如果有需求在节点挂载前执行一些逻辑也可以添加在下面代码
挂载前事件
标记的位置;//#region 挂载前事件 //#endregion view.params = params; this.Mount(view); view.node.active = true;
UI挂载管理
Mount
方法控制节点的挂载,使不同类型的 UI 视图判断自己的逻辑,如全屏视图需要关闭上一个全屏视图并记录,弹出视图需要修改节点索引;预加载
preloadUI
方法实现 UI 的预加载,可以将常使用的视图提前加载优化 UI 速度;其他
- 通常情况下不建议使用
getView
获取并持有视图脚本,一切行为应该趋向于中心管理;- UI 的隐藏事件尽可能的使用 UI 类的
HideUI
方法,而非 UIManager 的HideUI
方法;- UI 视图名称可以使用枚举记录而并未字符串,根据不同的语言特性部分语言可以直接使用
class类
,因为 cocos 构建后类名会被编译成简单的字母不能和预制体统一名称,故该案例使用的字符串;
1.5 使用示例
- 实现示例
@ccclass
export default class AMoudleView extends PopView {
protected OnMountEnd() {
//进行节点获取、适配、事件注册等操作
}
ShowUI(params: {`具体属性`}) {
//UI初始化逻辑
}
HideUI(): void {
//在此实现私有隐藏事件,非特殊需求尽量不要写在 super.HideUI() 之后
super.HideUI();
}
}
- UI 调用逻辑
//通过 UI 的中心管理器 UIManager 显示视图
UIManager.inst.ShowUI(ViewName.AModuleView, { `具体参数` });
2.1 结语
对于微型项目毛毛虫不建议专门搭建一个框架,一方面框架的搭建无可避免的容易增加代码量,另一方面框架的设计需要对生命周期有较深理解否则容易出现时序性的问题。