LUA如何实现组件开发模式

        使用过quick-cocos2dx开发过游戏的朋友都使用过addComponent这个方法,相当于给当前类添加一个组件,可以让当前类方便的复用组件提供的功能。比如下面的代码示例就是通过给Dispatcher类添加事件监听分发的组件,让该类拥有了事件监听分发的功能,这样我们可以自由的让一个类拥有事件监听分发的功能,而不必让所有的事件都堆在一个事件分发器中导致事件增多时影响事件分发的效率。

-- 在构造函数中添加事件分发的组件,这样Dispatcher类就可以实现注册监听功能了
function Dispatcher:ctor()
    cc(self):addComponent("components.behavior.EventProtocol"):exportMethods()
end

-- 获取一个当前类的单例
function Dispatcher:getInstance()
	if Dispatcher.instance == nil then
		Dispatcher.instance = NetDispatcher:new()
	end
	return Dispatcher.instance
end

-- 监听一个事件
Dispatcher:getInstance():addEventListener(EVENT_ID,handler(self, self.eventCb))

-- 分发一个事件
Dispatcher:getInstance():dispatchEvent(EVENT_ID)

        那么quick框架层是如何实现这个神奇的功能呢。

        首先我们看一个cc(self)的魔法,在framework/cc/init.lua文件中有这样一段代码。

local GameObject = cc.GameObject
local ccmt = {}
ccmt.__call = function(self, target)
    if target then
        return GameObject.extend(target)
    end
    printError("cc() - invalid target")
end
setmetatable(cc, ccmt)

        lua中当table名字做为函数名字的形式被调用的时候,会调用__call函数。setmetatable(cc, ccmt)设置ccmt为cc的元表,所以当我们这样调用时cc(self) 会调用到ccmt的__call元方法。我们看到ccmt的__call方法最后会调用return GameObject.extend(target),这又是个什么东西呢,我们去framework/cc/GameObject.lua中看一看:


local Registry = import(".Registry")

local GameObject = {}

function GameObject.extend(target)
    target.components_ = {}

    function target:checkComponent(name)
        return self.components_[name] ~= nil
    end

    function target:addComponent(name)
        local component = Registry.newObject(name)
        self.components_[name] = component
        component:bind_(self)
        return component
    end

    function target:removeComponent(name)
        local component = self.components_[name]
        if component then component:unbind_() end
        self.components_[name] = nil
    end

    function target:getComponent(name)
        return self.components_[name]
    end

    return target
end

return GameObject

        代码一目了然。我们调用GameObject.extend(target)时,相当于给target定义一个components_容易用来存放添加到target上的组件实例,同时给target扩展了组件添加,删除,获取的方法来管理组件。在addComponent中有这样一段代码Registry.newObject(name),Registry类主要是用来管理组件的,我们在ramework/cc/init.lua文件中可以看到下面代码,对于我们代码库中实现的组件类,都会注册到Registry实例中,保证一个组件路径对应包中一个唯一的组件实现路径。

-- init components
local components = {
    "components.behavior.StateMachine",
    "components.behavior.EventProtocol",
    "components.ui.BasicLayoutProtocol",
    "components.ui.LayoutProtocol",
    "components.ui.DraggableProtocol",
}
for _, packageName in ipairs(components) do
    cc.Registry.add(import("." .. packageName, CURRENT_MODULE_NAME), packageName)
end

Registry的实现如下:


local Registry = class("Registry")

Registry.classes_ = {}
Registry.objects_ = {}

function Registry.add(cls, name)
    assert(type(cls) == "table" and cls.__cname ~= nil, "Registry.add() - invalid class")
    if not name then name = cls.__cname end
    assert(Registry.classes_[name] == nil, string.format("Registry.add() - class \"%s\" already exists", tostring(name)))
    Registry.classes_[name] = cls
end

function Registry.remove(name)
    assert(Registry.classes_[name] ~= nil, string.format("Registry.remove() - class \"%s\" not found", name))
    Registry.classes_[name] = nil
end

function Registry.exists(name)
    return Registry.classes_[name] ~= nil
end

function Registry.newObject(name, ...)
    local cls = Registry.classes_[name]
    if not cls then
        -- auto load
        pcall(function()
            cls = require(name)
            Registry.add(cls, name)
        end)
    end
    assert(cls ~= nil, string.format("Registry.newObject() - invalid class \"%s\"", tostring(name)))
    return cls.new(...)
end

function Registry.setObject(object, name)
    assert(Registry.objects_[name] == nil, string.format("Registry.setObject() - object \"%s\" already exists", tostring(name)))
    assert(object ~= nil, "Registry.setObject() - object \"%s\" is nil", tostring(name))
    Registry.objects_[name] = object
end

function Registry.getObject(name)
    assert(Registry.objects_[name] ~= nil, string.format("Registry.getObject() - object \"%s\" not exists", tostring(name)))
    return Registry.objects_[name]
end

function Registry.removeObject(name)
    assert(Registry.objects_[name] ~= nil, string.format("Registry.removeObject() - object \"%s\" not exists", tostring(name)))
    Registry.objects_[name] = nil
end

function Registry.isObjectExists(name)
    return Registry.objects_[name] ~= nil
end

return Registry

        上面的代码重点可以看一下Registry.add和Registry.newObject的实现。初始化时把组件都注册到Registry中,我们给一个target添加一个组件实例时都通过Registry.newObject,这样可以保证组件重名时添加到错误的组件实例。

        到这里cc(self):addComponent("components.behavior.EventProtocol")的魔法我们已经清楚了,那么exportMethods()又是做什么的呢?框架层给我们实现了一个Component类,我们的组件都要继承这个类,那么我们去Component类中看一看。

        它在framework/cc/components/Component.lua文件中


local Component = class("Component")

function Component:ctor(name, depends)
    self.name_ = name
    self.depends_ = checktable(depends)
end

function Component:getName()
    return self.name_
end

function Component:getDepends()
    return self.depends_
end

function Component:getTarget()
    return self.target_
end

function Component:exportMethods_(methods)
    self.exportedMethods_ = methods
    local target = self.target_
    local com = self
    for _, key in ipairs(methods) do
        if not target[key] then
            local m = com[key]
            target[key] = function(__, ...)
                return m(com, ...)
            end
        end
    end
    return self
end

function Component:bind_(target)
    self.target_ = target
    for _, name in ipairs(self.depends_) do
        if not target:checkComponent(name) then
            target:addComponent(name)
        end
    end
    self:onBind_(target)
end

function Component:unbind_()
    if self.exportedMethods_ then
        local target = self.target_
        for _, key in ipairs(self.exportedMethods_) do
            target[key] = nil
        end
    end
    self:onUnbind_()
end

function Component:onBind_()
end

function Component:onUnbind_()
end

return Component

        Component的实现中我们重点看一下Component:exportMethods_的实现,它有一个methods参数,这是一个组件提供的方法列表,通过遍历这个列表,给target添加组件里面提供的方法属性,然后把这个方法属性指向一个匿名函数,在匿名函数中调用组件的对应方法。比如我们文字开头使用的EventProtocol组件中的实现如下:

function EventProtocol:exportMethods()
    self:exportMethods_({
        "addEventListener",
        "dispatchEvent",
        "removeEventListener",
        "removeEventListenersByTag",
        "removeEventListenersByEvent",
        "removeAllEventListenersForEvent",
        "removeAllEventListeners",
        "hasEventListener",
        "dumpAllEventListeners",
    })
    return self.target_
end

它最后调用的还是Component:exportMethods_方法。

        quick-cocos2dx的框架中提供下面这些组件,其中事件监听和状态机是很常用的,你可以研究一下他们的具体实现,在开发中也可以尝试实现自己的组件库而不是通过繁琐的继承关系来达到代码复用的目的。

         这篇文章粘贴了太多的源码,主要是因为我觉得它的实现很巧妙,所以就忍不住多贴了一些代码。随着cocoscreator的流行quick-cocos2dx使用的不多了,而且官方也停止维护了,但里面的一些代码实现技巧还是值得我们学习的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值