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的一大神器,它能让我们的代码更加干净、模块化。通过这种方法,我们不仅可以节省大量的时间,还可以减少出错的机会。

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

相关推荐
烧仙草奶茶1 个月前
【cocos creator】2.x里,使用3D射线碰撞检测
3d·cocos creator·cocos-creator·2.x
仅此而已7292 个月前
Cocos Creator倒计时
游戏引擎·cocos creator
仅此而已7292 个月前
cocosUI多分辨率适配
游戏引擎·cocos creator·多分辩率适配
SilenceJude2 个月前
cocos creator 3学习记录01——如何替换图片
前端·cocos creator
GrimRaider3 个月前
[Cocos Creator] v3.8开发知识点记录(持续更新)
开发·cocos creator
S_clifftop4 个月前
cocos creator如何使用cryptojs加解密(及引入方法)
cocos creator·加密解密·cryptojs
平淡风云4 个月前
cocosCreator获取手机剪切板内容
java·智能手机·typescript·android studio·cocos creator
zhenmu4 个月前
【cocos creator 3.x】 修改builtin-unlit 加了一个类似流光显示的mask参数
cocos creator·shader·effect
Thomas_YXQ4 个月前
Cocos Creator 编辑器的数据绑定详解
编辑器·游戏引擎·cocos creator·cocos2d·cocos·微信小游戏
Thomas_YXQ5 个月前
Cocos Creator 3D物理引擎的碰撞检测与触发器详解
游戏·3d·cocos creator·cocos2d·cocos