学习Cocos2d-x Lua:面向对象 - 类和继承
这个系列主要聚焦于学习Cocos2d-x Lua,总结Lua开发过程中涉及的知识点,以及探讨在开发过程中如何使用Cocos Code IDE。本篇将着重介绍Lua中的面向对象编程,主要讲解类和继承。
写在前面
终于要探讨Lua中的面向对象编程了。相信目前学习Lua的大部分人都是为了开发手机网游,并且基本都是冲着脚本语言的热更新特性去的,因此全脚本开发变得十分流行。对于普及程度不如C++、Java等主流语言的Lua而言,新手若要在短时间内上手开发游戏并非易事。所以大家更倾向于运用面向对象思想来学习Lua。
1. 类的对象
创建一个类其实很简单,在Lua中,类可以用一个table来表示。那么,如何使用这个类创建多个对象呢?可以借助元表和元方法来实现。
以下是示例代码:
TSprite = {
x = 0,
y = 0
}
function TSprite:setPosition(x, y)
self.x = x
self.y = y
end
function TSprite:new()
local o = {}
setmetatable(o, {__index = self})
return o
end
local who1 = TSprite:new()
local who2 = TSprite:new()
who1:setPosition(1, 2)
who2:setPosition(44, 6)
print("who1坐标(" .. who1.x .. "," .. who1.y .. ")")
print("who2坐标(" .. who2.x .. "," .. who2.y .. ")")
留意TSprite的new函数,该函数创建了一个新的table,并为其设置了一个元表,这个元表的__index元方法指向TSprite本身,最后返回这个新的table。因此,所有通过new函数生成的新table,都能够使用TSprite的函数和各个字段属性(因为__index的值是TSprite)。
通过new函数,我们创建了who1和who2,并调用它们的setPosition函数。最终,who1和who2的x、y值是不同的,这就是类的对象。
2. 类对象的__index都是同一个TSprite,为什么x、y值可以不相同?
或许大家会有这样的疑惑:who1和who2最终调用的都是setPosition函数,调用self.x时最终不也是调用了TSprite的x值吗?为什么它们的x、y值会不一样呢?下面来梳理一下:
- 当
who1中不存在setPosition方法时,Lua会去__index元方法里查找,从而找到TSprite的setPosition函数。 - 在
setPosition函数中,使用了self.x = x,此时的self就是who1。由于who1中原本不存在x字段,所以如果打印self.x的值,实际上打印的是TSprite的x值。 - 但需要注意的是,
__index元方法是用于调用的,而不是用于赋值的。因此,self.x = x这句话,实际上是给who1这个table的x字段赋值。who1本身不存在x字段,赋值后who1就有了x字段,以后who1就不会再去TSprite里查找x字段了。 - 所以,对
who1和who2的x、y字段进行赋值操作时,完全不会影响到TSprite。
3. 节省资源——使用TSprite作为元表
仔细观察new函数,在给新table设置元表时,我们重新创建了一个元表:
setmetatable(o, {__index = self})
这样做的话,每次调用new函数创建一个新对象时,都会产生一个新的元表。虽然这个开销看似可以忽略不计,但对于有强迫症的开发者来说,可能会倾向于下面的代码:
function TSprite:new()
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
在这段新的new函数中,直接使用self作为元表,并将self赋值给__index。下面来详细解释一下:
- 调用
new函数时,self实际上就是TSprite本身。这里使用self是为了后续扩展考虑,当然也可以直接用TSprite代替。 self.__index = self,其实就是设置元表的__index元方法,相当于TSprite.__index = TSprite。TSprite作为一个table,是可以作为元表的,并且元表可以有__index元方法,这并没有问题。- 通过这个小技巧,我们避免了每次调用
new函数时都额外创建一个新的元表,从而节省了资源。
4. 继承
在Lua里实现继承其实很简单,但需要认真思考。以下是示例代码:
TSprite = {
x = 0,
y = 0
}
function TSprite:setPosition(x, y)
self.x = x
self.y = y
end
function TSprite:new()
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
local MoneySprite = TSprite:new()
function MoneySprite:setPosition(x, y)
print("呵呵,我是富二代,根本不需要改变。")
end
local who = MoneySprite:new()
who:setPosition(44, 6)
print("who坐标(" .. who.x .. "," .. who.y .. ")")
TSprite的定义保持不变。按照之前的理解,MoneySprite是TSprite的一个对象,但实际上它仍然是一个table。此时,我们修改了MoneySprite的setPosition函数,因此调用MoneySprite的setPosition函数时,与TSprite无关。
关键在于后续的代码,我们再次调用MoneySprite的new函数创建了一个新对象who。这里的关键在于new函数里的代码,此时new函数里的self是谁呢?由于new函数是由MoneySprite调用的,所以self就是MoneySprite。于是,新对象who的元表就是MoneySprite,元表的__index也是MoneySprite。
因此,调用who的setPosition函数时,实际上调用的是MoneySprite的setPosition函数。所以,who是MoneySprite的对象,而MoneySprite就是TSprite的子类。
以下是输出结果:
[LUA-print] 呵呵,我是富二代,根本不需要改变。
[LUA-print] who坐标(0,0)
怎么样,继承的实现方法是不是很简单?如果对元表、元方法、self的概念比较生疏,可能一时难以理解,没关系,多思考一下,或者隔天再回头思考,或许就会豁然开朗了。