学习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
的概念比较生疏,可能一时难以理解,没关系,多思考一下,或者隔天再回头思考,或许就会豁然开朗了。