Laya:基于Prefab的简单UI框架。

本文介绍了一种UI框架的设计思路及其实现细节,包括UI界面的管理、UI元素的快速定位与操作等核心功能。该框架利用两个主要脚本UIMgr和UIBase实现了界面的高效管理和代码复用。

UI框架的功能:
方便快捷的对UI界面进行管理:如快速打开与关闭一个界面,防止相同界面多开等等。

核心代码有2个脚本,一个UIMgr,一个UIBase。
UIMgr的功能是管理UI界面。
UIBase的功能是:所有界面都要继承自UIBase,这样界面就可以直接方便地调用父类的方法,更好地复用代码。管理特殊前缀开头的节点名称,只有使用了特定字符串开头的节点才会被添加到UI管理的数据结构里。

UIMgr里面用到了一个SceneMgr,可以在我的上一篇文章里找到有它的介绍,UIMgr的场景方法和SceneMgr是一样的,同样是一个单例。

import SceneMgr from "./SceneMgr";

/**
 * UI界面管理器
 */
export default class UIMgr extends Laya.Script
{
    private uiPrefabMap=new Map<string,Laya.Prefab>();  //UI预制体字典:Map<string,Laya.Prefab>
    public static Instance: UIMgr;

    constructor()
    {
        super();
        UIMgr.Instance=this;
    }

    /**
     * 获取UI预制体的完整路径
     * @param uiPrefabName UI预制体名称
     */
    public GetPrefabPath(uiPrefabName:string):string
    {
        return "prefab/"+uiPrefabName+".prefab";
    }

    //=====================================< UIPanel >类型UI======================================= 

    /**
     * 找到名字对应的UI物体
     */
    public FindTargetUI<T extends Laya.UIComponent>(targetName:string,parent:Laya.Node):T
    {
        if(!parent){
            return null;
        }
        return parent.getChildByName(targetName)as T;
    }

    /**
     * 在场景中找到UI并销毁
     */
    public DestroyUI(uiName:string)
    {
        let sc2d=SceneMgr.Instance.GetCurSc2D();
        if(sc2d){
            let ui=sc2d.getChildByName(uiName)as Laya.UIComponent;            
            if(ui){
                ui.destroy(true);
            }
        }
    }

    /**
     * 设置UI visible
     */
    public SetUIVisible(uiName:string,visible:boolean)
    {
        let sc2d=SceneMgr.Instance.GetCurSc2D();
        if(sc2d){
            let ui=sc2d.getChildByName(uiName)as Laya.UIComponent;
            if(ui){
                ui.visible=visible;
            }
        }
    }

    /**
     * 加载并打开UI面板(面板类型UI,不能打开多个)
     * @param uiCtlerScript  控制此界面的脚本类
     * @param callback 加载完回调(Laya.UIComponent)=>{}
     * @param parent 父级节点
     * @param isOnlyOne 是否只能存在一个
     */
    public OpenUI(uiCtlerScript:any,callback:Function=(param:any)=>{},parent?:Laya.Node,isOnlyOne=true)
    {
        let uiName=uiCtlerScript.name;
        if(!parent){
            parent=SceneMgr.Instance.GetCurSc2D();
        }
        if(!parent){
            return;
        }
        if(parent.getChildByName(uiName)){
            if(isOnlyOne==true){
                return;
            }
        }
        if(this.uiPrefabMap.has(uiName))
        {
            let uiPrefab=this.uiPrefabMap.get(uiName) as Laya.Prefab;
            this.OpenUICommonOp(uiPrefab,uiCtlerScript,parent,callback);
        }
        else
        {
            this.LoadUIPrefab(uiName,(uiPrefab:Laya.Prefab)=>
            {
                this.OpenUICommonOp(uiPrefab,uiCtlerScript,parent,callback);
            });
        }
    }

    /**
     * 从字典中移除UI预制体并清理单个资源(最好在切换场景时使用,清理掉下个场景不再使用的UI预制体)
     * @param uiCtlerScript 控制此界面的脚本类(ui索引,预制体的名称)
     */
    public ClearRes(uiCtlerScript:any)
    {
        let k=uiCtlerScript.name;
        if(!this.uiPrefabMap.has(k))
        return;
        this.uiPrefabMap.delete(k);
        Laya.LoaderManager.prototype.clearRes(this.GetPrefabPath(k));
        Laya.Resource.destroyUnusedResources();
    }

    /**
     * 加载UI预制体
     * @param uiName 预制体UI名称
     * @param callback 加载完回调
     */
    private LoadUIPrefab(uiName:string,callback:Function=(uiPrefab:any)=>{})
    {
        let uiPath=this.GetPrefabPath(uiName);
        Laya.loader.load(uiPath,Laya.Handler.create(this,(uiPrefab:Laya.Prefab)=>
        {    
            if(!uiPrefab){
                console.error("不存在目标预制体",uiName);
                return;
            }
            this.uiPrefabMap.set(uiName,uiPrefab);
            callback(uiPrefab);
        }));
    }

    /**
     * 加载面板的共有操作
     */
    private OpenUICommonOp(uiPrefab:Laya.Prefab,uiCtlerScript:any,parent:Laya.Node,callback:Function=(ui:any)=>{})
    {
        let uiName=uiCtlerScript.name;
        let ui=uiPrefab.create()as Laya.UIComponent;
        parent.addChild(ui);
        ui.name=uiName;
        this.AddSrcToNode(uiCtlerScript,ui);
        callback(ui);
    }
	
	private AddSrcToNode(src:any,targetNode:Laya.Node)
    {
        if(targetNode.getComponent(src))
        return;
        targetNode.addComponent(src);
    }
 
     /**
      * 打印字典
      */
    public LogUIMapInfo()
    {
        if(this.uiPrefabMap.size==0||!this.uiPrefabMap==null)
        {
            console.log("UI预制体资源Map为空");
            return;            
        }
        for (let [key, value] of this.uiPrefabMap) 
        {
            console.log("key:",key,"===","value:",value);
        }
    }
}

UIBase脚本:
当UI界面里的节点使用了prefixArray里的前缀命名时(如:imgTest,txtPlayerName等等),此节点会被添加到allUIDic里,在脚本里可以通过名称获取目标节点。prefixArray里可以自定义添加或删除你的节点前缀。

export default class UIBase extends Laya.Script {

    public allUIDic: Map<string, Laya.Node> = new Map<string, Laya.Node>();
    private prefixArray: string[] = ["img", "btn", "txt", "list", "box","hsld","ti"]; //需要加入UI字典的UI前缀

    constructor() {
        super();
    }

    onAwake() {
        this.SetAllUINodesDic();
    }

    /**
     * 销毁当前界面UI
     * @param cbOnClose 关闭时的回调
     */
    public CloseUI() {
        this.owner.destroy(true);
    }

    private GetNodeByMap<T extends Laya.Node>(nodeName:string,map:Map<string,Laya.Node>):T
    {
        if(!map.has(nodeName)){
            return null;
        }
        return map.get(nodeName) as T;
    }

    /**
     * 添加点击事件带有声音
     * @param btName 按钮名称
     * @param callback 回调
     * @param needPlayClickSound 是否播放点击音效
     * @param clickSoundPath 音效地址
     */
    public AddBtnEvent(btName: string, callback: Function,needPlayClickSound=true,clickSoundPath?:string) {
        let bt: Laya.Button = this.GetBtn(btName);
        if (!bt) return;
        bt.on(Laya.Event.CLICK,this,callback)
    }

    /**
     * 通过文本名称获取Tetx组件
     * @param txtName 文本组件名称
     * @returns Laya.Text
     */
    public GetTxt(txtName: string): Laya.Text {
        return this.GetNodeByMap<Laya.Text>(txtName, this.allUIDic);
    }

    /**
     * 通过文本名称获取Image组件
     * @param imgName Image组件名称
     * @returns Laya.Image
     */
    public GetImg(imgName: string): Laya.Image {
        return this.GetNodeByMap<Laya.Image>(imgName, this.allUIDic);
    }

    /**
     * 通过文本名称获取Image组件
     * @param btnName Image组件名称
     * @returns Laya.Image
     */
    public GetBtn(btnName: string): Laya.Button {
        return this.GetNodeByMap<Laya.Button>(btnName, this.allUIDic);
    }

    /**
     * 通过文本名称获取List组件
     * @param listName List组件名称
     * @returns Laya.List
     */
    public GetList(listName: string): Laya.List {
        return this.GetNodeByMap<Laya.List>(listName, this.allUIDic);
    }

    /**
     * 通过文本名称获取Box组件
     * @param boxName Box组件名称
     * @returns Laya.Box
     */
    public GetBox(boxName: string): Laya.Box {
        return this.GetNodeByMap<Laya.Box>(boxName, this.allUIDic);
    }

    /**
     * 通过文本名称获取HSlider组件
     */
    public GetHSlider(name: string): Laya.HSlider {
        return this.GetNodeByMap<Laya.HSlider>(name, this.allUIDic);
    }

    /**
     * 通过文本名称获取TextInput组件
     */
    public GetTextInput(name: string): Laya.TextInput {
        return this.GetNodeByMap<Laya.TextInput>(name, this.allUIDic);
    }

    /**
     * 通过泛型名称获取UI组件
     * @param uiName UI组件名称
     * @returns Laya.UIComponent
     */
    public GetUIByT<T extends Laya.UIComponent>(uiName: string): T {
        return this.GetNodeByMap<T>(uiName, this.allUIDic);
    }

    /**
     * 设置UIvisible
     */
    public SetVisible<T extends Laya.UIComponent>(uiName: string,visible:boolean)
    {
        this.GetUIByT<T>(uiName).visible=visible;
    }

    /**
     * 设置文本内容
     */
    public SetText(uiName: string,content:string)
    {
        this.GetTxt(uiName).text=content;
    }

    /**
     * 设置图片的skin
     */
    public SetImgSkin(imgName:string,skin:string)
    {
        this.GetImg(imgName).skin=skin;
    }

    /**
     * 设置按钮的skin
     */
    public SetBtnSkin(btnName:string,skin:string)
    {
        this.GetImg(btnName).skin=skin;
    }

    //--------------------------------------------添加UI到字典-------------------------------------------------

    /**
     * 检查目标ui是否需要加入字典
     */
    private CheckNeedAddToDic(uiName: string) {
        for (let i = 0; i < this.prefixArray.length; i++) {
            if (uiName.startsWith(this.prefixArray[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * 将所有的UI节点装入字典(每个ui节点不能重名)
     */
    public SetAllUINodesDic() {
        this.allUIDic = this.GetAllChildrenMap(this.owner);
    }

    //获取目标节点的所有子节点,将所有子节点放入数组并返回
    private GetChildNodesArray(target: Laya.Node): Laya.Node[] {
        let nodeArray: Laya.Node[] = [];
        for (let i = 0; i < target.numChildren; i++) {
            let node = target.getChildAt(i);
            if (node) {
                nodeArray.push(node);
            }
        }
        return nodeArray;
    }

    //递归获取目标节点的所有子孙节点,并将他们全部放入数组并返回
    private FindAndGetAllChildren(parentNode: Laya.Node, outNodesArray: Laya.Node[]): Laya.Node[] {
        if (parentNode.numChildren > 0) {
            let nodeArray = this.GetChildNodesArray(parentNode);
            nodeArray.forEach(node => {
                if (this.CheckNeedAddToDic(node.name) == true) {
                    outNodesArray.push(node);
                }
                if (this.GetChildNodesArray(node).length > 0) {
                    this.FindAndGetAllChildren(node, outNodesArray);
                }
                else {
                    return outNodesArray;
                }
            });
        }
        return null;
    }

    //构建一个数组来存放获取的所有节点并返回此数组
    private GetAllChildrenArray(parentNode: Laya.Node): Laya.Node[] {
        let allChildrenArray: Laya.Node[] = [];
        this.FindAndGetAllChildren(parentNode, allChildrenArray);
        return allChildrenArray;
    }

    //将所有节点封装到字典里,方便获取
    private GetAllChildrenMap(parentNode: Laya.Node): Map<string, Laya.Node> {
        let allChildrenArray = this.GetAllChildrenArray(parentNode);
        let map = new Map();
        for (let i = 0; i < allChildrenArray.length; i++) {
            if (!map.has(allChildrenArray[i].name)) {
                map.set(allChildrenArray[i].name, (allChildrenArray[i]));
            }
        }
        return map;
    }

    /**
      * 打印UI字典
      */
    public LogUIMap() {
        if (this.allUIDic.size== 0||!this.allUIDic){
            console.log("UI预制体资源Map为空");
            return;
        }
        console.log("UI节点个数:", this.allUIDic.size);
        for (let [key, value] of this.allUIDic) {
            console.log("key:", key, "===", "value:", value);
        }
    }
}

UI框架使用方法:

在创建UI界面前先要说一下场景的创建,我创建场景使用的是View而不是Scene,因为Scene不好做分辨率适配。
在这里插入图片描述
场景创建完成后要把View场景的设计分辨率和四个边距点设置为0。我使用的是1136x640的分辨率。
在这里插入图片描述
UI界面制作的流程:

比如现在要添加一个名称为TestUIPanel的界面。
我一般创建一个新UI界面是这样做的:
首先创建一个Image名称叫做:TestUIPanel
在这里插入图片描述
在这里插入图片描述
把Image的skin设置为空,四个边距点设置为0,因为这个Image会作为UI预制体的底板。
如果你的UI需要有黑色半透明背景,可以添加一个Box组件,然后设置它的颜色和透明度,但是4个边距点也要设置为0。
在这里插入图片描述
在这里插入图片描述
然后在下面添加了一个按钮,点击可以关闭界面。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后就是保存此UI界面为预制体既可。
在这里插入图片描述
Laya默认的UI预制体是在这个目录下的在这里插入图片描述
我加载时使用的也是默认目录。
双击打开UI预制体:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以点击设置页面调整UI预制体的默认场景大小。
目前UI界面就创建完成了。

接下来是编写界面的控制和打开的代码:
创建一个和界面名称一样的脚本,这里必须要保证界面名称和脚本名称一致,因为我是通过把脚本转为字符串去读取的UI预制体。
在这里插入图片描述
在这里插入图片描述
接着把创建好的脚本继承自UIBase,如果要使用onAwake方法必须调用一下基类的super().onAwake或是执行一下this.SetAllUINodesDic();方法。

接下来就是UI的逻辑代码:
在这里插入图片描述
打开此界面:
在这里插入图片描述
打开界面的方法有四个参数:
@param uiCtlerScript — 控制此界面的脚本类
@param callback — 加载完回调(Laya.UIComponent)=>{}
@param parent — 父级节点,不传则使用当前2d场景作为父节点
@param isOnlyOne — 是否只能存在一个,false时可以打开多个相同界面

运行运行:
在这里插入图片描述
点击关闭按钮:
在这里插入图片描述
以上就是此框架的大概内容,代码也比较简单,可以自己具体看一下。

<think>我们之前讨论的是如何加载预制体并添加到场景。现在用户有一个具体的需求:使用代码创建Mahjong预制体并添加到Laya场景,并且已经使用了`@property`装饰器声明了一个预制体属性。 用户代码片段:`@property({ type: Laya.Prefab }) private Mahjong: Laya.Prefab;` 这个代码通常在LayaAir的Script脚本中,用于在IDE中关联预制体资源。这样,我们可以在代码中直接使用这个Mahjong属性来实例化预制体。 步骤: 1. 在IDE中,将Mahjong预制体文件(比如mahjong.prefab)拖到该脚本组件的对应属性上(在IDE的属性面板中会显示Mahjong属性,然后拖入预制体)。 2. 在代码中,我们可以通过这个Mahjong属性来创建实例。 注意:这种方式不需要动态加载,因为预制体已经通过IDE关联,会在场景加载时自动加载。 具体实现: ```typescript const mahjongInstance: Laya.Sprite = this.Mahjong.create(); // 创建预制体实例 // 或者使用 new this.Mahjong() 也可以,但根据Laya的版本,推荐使用create方法 // 设置位置等属性 mahjongInstance.pos(100, 100); // 添加到当前节点(如果是脚本组件,则this.owner是组件所属节点) this.owner.addChild(mahjongInstance); // 或者直接添加到舞台 // Laya.stage.addChild(mahjongInstance); ``` 完整示例: 假设我们有一个脚本组件MahjongGame,绑定在场景中的一个节点上。 ```typescript export default class MahjongGame extends Laya.Script { // 声明一个预制体属性,IDE中可以拖入 @property({ type: Laya.Prefab }) public Mahjong: Laya.Prefab; constructor() { super(); } onEnable(): void { // 确保预制体资源已经关联 if (this.Mahjong) { this.createMahjong(); } } private createMahjong(): void { // 创建预制体实例 let instance = this.Mahjong.create(); // 设置位置(示例位置,根据需求调整) instance.pos(200, 300); // 将实例添加到当前脚本所挂载的节点下 (this.owner as Laya.Sprite).addChild(instance); } } ``` 注意事项: 1. 在IDE中,需要将mahjong.prefab文件拖到该脚本组件的Mahjong属性上(在IDE的属性面板中)。 2. 这样,当场景加载时,预制体资源就已经准备好,可以直接创建实例。 3. 使用`create()`方法创建实例,也可以使用`new`,但`create`是预制体实例化的标准方式。 如果用户没有在IDE中关联,而是想通过代码动态加载,那么就需要使用`Laya.loader.load`,但用户已经使用了`@property`方式,所以这里采用IDE关联的方式。 相关问题: 1. 如何在LayaAir IDE中将预制体关联到脚本属性? 2. 预制体实例化后如何动态修改组件属性? 3. 如何批量创建多个麻将预制体实例? 4. 如何销毁预制体实例并管理内存? </think>### 在 Laya 中创建麻将预制体并添加到场景的完整指南 #### 步骤说明(使用 `@property` 绑定预制体) 1. **IDE 预制体准备** - 在 Laya IDE 中设计麻将预制体(Mahjong.prefab) - 设置好麻将的纹理、碰撞体和脚本组件 - 导出到 `bin/prefab/` 目录 2. **脚本声明预制体引用** ```typescript export default class GameScene extends Laya.Script { // IDE 中拖入麻将预制体 @property({ type: Laya.Prefab }) private Mahjong: Laya.Prefab; // 其他代码... } ``` 3. **场景初始化时创建麻将** ```typescript onEnable(): void { this.createMahjong(); } private createMahjong(): void { // 创建预制体实例 const mahjong = this.Mahjong.create() as Laya.Sprite; // 设置麻将位置(示例坐标) mahjong.pos(300, 200); // 获取麻将数值组件(假设有MahjongValue脚本) const valueComp = mahjong.getComponent(MahjongValue); if(valueComp) valueComp.setValue(5); // 设置麻将牌值 // 添加到当前节点(脚本挂载的节点) this.owner.addChild(mahjong); } ``` #### 关键参数说明 | 参数 | 类型 | 说明 | |------|------|------| | `@property` | 装饰器 | 声明IDE可配置属性 | | `type: Laya.Prefab` | 类型约束 | 指定绑定资源类型为预制体 | | `create()` | 方法 | 预制体实例化核心方法 | | `this.owner` | 节点引用 | 当前脚本挂载的父节点 | #### 最佳实践 1. **批量创建**(麻将桌场景) ```typescript // 创建麻将阵列 const positions = [ [300, 200], [400, 200], [500, 200], [300, 300], [400, 300], [500, 300] ]; positions.forEach(pos => { const tile = this.Mahjong.create() as Laya.Sprite; tile.pos(pos[0], pos[1]); this.owner.addChild(tile); }); ``` 2. **动态加载替代方案**(不依赖 IDE 拖拽) ```typescript private loadMahjongPrefab(): void { Laya.loader.load("prefab/Mahjong.json", Laya.Handler.create(this, res => { const prefab = new Laya.Prefab(); prefab.json = res; const instance = prefab.create(); Laya.stage.addChild(instance); })); } ``` 3. **组件交互** ```typescript // 麻将点击事件处理 mahjong.on(Laya.Event.CLICK, this, () => { const anim = mahjong.getComponent(Laya.Animation); anim.play("select"); // 获取麻将数值 const value = mahjong.getComponent(MahjongValue).value; console.log(`选中麻将: ${value}`); }); ``` #### 常见问题解决 1. **预制体未绑定错误** - 确保在 IDE 中将 Mahjong.prefab 拖到脚本的 Mahjong 属性栏 - 检查预制体路径是否正确(应位于 `bin/prefab/`) 2. **组件获取失败** - 确认预制体是否包含所需脚本组件 - 使用安全获取方式: ```typescript const comp = mahjong.getComponent(MahjongValue) || mahjong.addComponent(MahjongValue); ``` 3. **内存优化** - 使用对象池管理频繁创建的麻将: ```typescript // 初始化对象池 Laya.Pool.createClass("MahjongPool", MahjongSprite); // 获取实例 const tile = Laya.Pool.getItem("MahjongPool"); ``` --- ### 相关问题 1. 如何给 Laya 预制体中的子组件动态绑定事件?[^1] 2. 麻将预制体的碰撞检测最佳实现方式是什么? 3. Laya 中如何实现预制体的动态换肤功能?(如不同麻将样式) 4. 如何优化大量麻将预制体同时渲染的性能?[^3] 5. Laya 预制体与原生 JavaScript 类如何实现数据交互?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨幕枫陵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值