使用过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使用的不多了,而且官方也停止维护了,但里面的一些代码实现技巧还是值得我们学习的。