Cocoscreator: 用装饰器自动绑定节点

🚀有时候,你有没有觉得在Cocos Creator里一遍又一遍地手动绑定节点是一件非常无聊的事?如果你同意,那么告诉你一个秘密:有一种方法!只需一行代码的"魔法",就可以自动绑定节点,省下频繁重复的手动拖拽节点的苦差事!好吧,这并不是真正的魔法,而是装饰器!🌈

装饰器是什么?

简而言之,装饰器在编程中就像是一个魔法药水。它们可以注入到类、属性、方法等中,提供某些额外的功能或修改它们的行为。而在TypeScript中,这种能力变得更加强大,因为装饰器可以和类型系统无缝结合。

进入正题:自动绑定节点的装饰器

🧙‍下面是一个名为bindComp的装饰器函数。这个小家伙是我们今天的主角,它的使命是在onLoad时,自动为所有注入的成员变量设置set&get方法,并在首次访问时为它赋值。它会让你在第一次访问这个属性的时候,自动在节点树上找到它并赋值给这个属性。嗯,就是这么酷!

typescript 复制代码
// 查找指定路径下的节点或组件
function __find<T>(path: string, node: cc.Node, type: FIND_TYPE<T>) {
    let temp = cc.find(path, node);
    if (cc.js.isChildClassOf(type, cc.Component)) {
        let comp = temp?.getComponent(type)
        return comp;
    }
    return temp;
}

const _FIND_OPTIONS_ = "_FIND_OPTIONS_"

// 目标类型,通常用于查找组件
interface FIND_TYPE<T> {
    new(): T;
}

// 查找选项的配置 
interface FindOption<T> {
    path: string; // 查找路径
    type: FIND_TYPE<T>; // 目标类型
    member: string; // 成员变量名称
    root?: string; // 根节点的路径
}

/**
 * @description 当onLoad时,自动对所有注入的成员变量设置set&get方法,当成员变量首次调用时对成员变量赋值
 * @param path 相对于当前脚本this.node的搜索路径,当rootPath非空,则以rootPath为根节点查找
 * @param type 查找组件类型
 * @param rootPath 相对于this.node 的搜索路径,不传入时,以当的this.node为根节点进行查找
 */
export function bindComp<T extends cc.Component | cc.Node>(path: string, type: FIND_TYPE<T>, rootPath?: string) {
    return function (target: any, member: string) {
        if (!(target instanceof cc.Component)) {
            Log.e("无法注入,仅支持 Component 组件")
            return;
        }
        let obj: any = target;
        if (!Reflect.has(target, _FIND_OPTIONS_)) {
            // 重写onLoad,当onLoad时,自动对所有注入的成员变量设置set&get方法,当成员变量首次调用时对成员变量赋值
            let __onLoad = obj.onLoad
            obj.onLoad = function () {
                let self = this;
                let fOption = Reflect.get(self, _FIND_OPTIONS_)
                for (let key in fOption) {
                    let ele: FindOption<T> = Reflect.get(fOption, key)
                    if (!Reflect.get(self, ele.member)) {
                        Reflect.defineProperty(self, ele.member, {
                            enumerable: true,
                            configurable: true,
                            get() {
                                let node = self.node;
                                if (ele.root) {
                                    let rootMemberName = `__${ele.root.replace(/\//g, "_")}`
                                    if (!cc.isValid(self[rootMemberName])) {
                                        self[rootMemberName] = __find(ele.root, node, cc.Node);
                                    }
                                    node = self[rootMemberName];
                                    if (CC_DEBUG && !cc.isValid(node)) {
                                        Log.e(`${cc.js.getClassName(self)}.${ele.root}节点不存在!!!`)
                                    }
                                }

                                if (!cc.isValid(self[key])) {
                                    self[key] = __find(ele.path, node, ele.type);
                                }
                                return self[key];
                            },
                            set(v) {
                                self[key] = v;
                            }
                        })
                    }
                }
                __onLoad && Reflect.apply(__onLoad, this, arguments);
            }


            // 重写onDestroy,当onDestroy时,删除所有注入的成员变量
            let __onDestroy = obj.onDestroy
            obj.onDestroy = function () {
                let self = this;
                let fOption = Reflect.get(self, _FIND_OPTIONS_)
                for (let key in fOption) {
                    let ele: FindOption<T> = Reflect.get(fOption, key)
                    Reflect.deleteProperty(self, ele.member);
                }
                __onDestroy && Reflect.apply(__onDestroy, this, arguments);
            }

            Reflect.defineProperty(target, _FIND_OPTIONS_, { value: {} })
        }

        // 将装饰器的配置信息存储到一个 `_FIND_OPTIONS_` 属性中,以便后续在组件的 `onLoad` 方法中使用
        let option: FindOption<T> = { path: path, type: type, member: member, root: rootPath }
        let attribute = `__${member}`
        let fOption = Reflect.get(target, _FIND_OPTIONS_)
        Reflect.defineProperty(fOption, attribute, { value: option, enumerable: true })
    }
}

这段代码初看起来可能有些吓人,但其实它很简单,让我们逐步深入了解。

🤹一步一步解释代码吧!

1️⃣ 首先,我们的这个bindComp装饰器接受几个参数:pathtyperootPath。前两个必传,最后一个可传可不传。

  • path是相对于当前脚本的this.node的搜索路径。
  • type是要查找的组件类型。
  • rootPath,这个小家伙是个可选项,如果你给它传递了值,它会从这个路径开始作为根节点进行搜索

2️⃣ 在onLoad方法中(也就是组件加载的时候),这个装饰器会走遍所有注入的成员变量,执行查找并绑定的任务。

3️⃣ 这个装饰器还会负责清理工作,在组件销毁的时候,它会在onDestroy方法里删除之前绑定的属性,确保没有留下可能引发内存泄漏的垃圾。

🧐用起来是什么样子呢?

typescript 复制代码
class MyComponent extends cc.Component {
    @bindComp("path/to/node", cc.Node)
    public myNode: cc.Node | null = null;
    
    onLoad() {
        console.log(this.myNode);
    }
}

🚴就这样!就定义了一个myNode,然后就可以直接用了,剩下的一切都不用担心!装饰器@bindComp会负责查找和绑定。你所要做的只是在组件的代码里,愉快地使用这个myNode就行了!

myNode首次被访问时,bindComp会触发getter,这个getter会负责查找节点,并将它绑定到myNode上。

🎉结束语

🚁装饰器真的是TypeScript的一大神器,它能让我们的代码更加干净、模块化。通过这种方法,我们不仅可以节省大量的时间,还可以减少出错的机会。

最后,请记住,任何技术手段都需要根据实际情况进行调整。虽然这种方法看起来很酷,但在实际项目中使用时,要确保它不会引入新的问题。

相关推荐
成长ing121381 个月前
点击音效系统
前端·cocos creator
blakeyi2 个月前
vscode保存自动刷新cocos creator编辑器
ide·vscode·cocos creator·热更新
烧仙草奶茶2 个月前
【cocos creator 3.x】3Dui创建,模型遮挡ui效果
ui·3d·cocos creator·cocos3d
糖墨夕2 个月前
【1】Coco2d creator资源管理注意事项 - meta 文件
前端·cocos creator·cocos2d-x
Setsuna_F_Seiei2 个月前
前端切图仔的一次不务正业游戏开发之旅
前端·游戏·cocos creator
jason_yang2 个月前
转眼间,已是十几年前的游戏代码了
cocos creator·游戏开发·cocos2d-x
成长ing121383 个月前
cocos creator 放大镜效果
前端·cocos creator
烧仙草奶茶4 个月前
【cocos creator】【模拟经营】餐厅经营demo
cocos creator·游戏源码·模拟经营
la_vie_est_belle5 个月前
《Cocos Creator游戏实战》非固定摇杆实现原理
游戏·cocos creator·游戏开发·cocos·非固定摇杆
布鲁克零三四四7 个月前
Cocos Creator导出obj文件用于后端寻路
cocos creator