前言

做了三年的游戏开发,其中有两年都在使用Lua这一脚本语言,想着是时候写点什么东西输出一下,算是给个交代。本文既不是讲Lua入门,也不会讲到Lua虚拟机那么深,读者尽可放大心随意看。

类的实现

原生Lua是不支持面向对象编程的。那怎么办呢?使用prototype模式即可。

首先读者需要知道Lua的原方法和原表这一知识点。如果接触过c++或者c#的同学应该知道我们可以对一些类的操作做重载(overload),改变诸如加减乘除,甚至一些更高级的操作。而在像是Lua这种脚本语言来说是没有重载这一操作的。取而代之的,我们可以通过重写元表来改变table原有的操作逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local theMetaTable = {}

theMetaTable.__add = function(leftVal, rightVal)
local ret = {}
ret.age = leftVal.age + rightVal.age
return ret
end

local tA = {age = 1}
local tB = {age = 99}
setmetatable(tA, theMetaTable)
setmetatable(tB, theMetaTable)
local tC = tA + tB
-- tC = {age = 100}
print(tc.age)

在这么多元方法里面有一个最值得关注的,称为__index。顾名思义,传入一个key,返回一个value。接下来就来讲解如何使用这个元方法实现面向对象。首先我们假设有一个方法叫做class(string name),会生成一个带有构造方法和其他各种方法的类原型(类原型是啥意思?类似于Java里的Class,c#里的Type),我们使用这个类原型就可以实例化出我们的实例来。使用方法大概是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Person = class('class')
Person.name = 'DefaultName'
Person.age = -1;
Person.sex = 'male'
Person.create = function(name, age)
local ret = Person.new()
ret.name = name
ret.age = age
return ret
end
Person.introduce = function(self)
print(string.format('i am %s, and i am %d', self.name, self.age))
end

local oldWong = Person.create('oldWong', 45)
oldWong.sex = 'female'
oldWong:introduce()
-- console print: I am oldWong, and i am 45.

明眼的读者可以发现了:从class函数返回的Person一定是一个带着new方法的table。但是为什么这个new方法会生成新的实例呢?我改变oldWong实例的sex属性之后,再用Person去实例化一个实例,会是什么性别?有兴趣的读者可自行试试。

接下来笔者将为你揭秘__index方法在class(string name)函数中的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function (clzName)
local ret = {}
ret.__class = {}
ret.__class.__name = clzName
ret.new = function()
local instance
-- metatable
local mt = {}
mt.__index = function(t, key)
return rawget(instance, key) or ret[key]
end
setmetatable(instance, mt)
return instance
end
return ret
end

看一个函数我习惯先看最后return了什么东西,再看这个返回值是怎么被创建的,内容是啥。在这个class方法中,返回值是一个被命名为ret的表,这个表被填进了两个东西:

  • 类的信息,暂时只有类名
  • 一个叫做new的方法

看起来这个new方法就是关键了。

new方法最后返回了一个叫instance的table,这个看似什么内容都没有。等等…并不是什么东西都没有,这个table被塞进了一个元表(metatable),而这个元表被重写了__index方法。

__index方法实际上是为了定义一种行为:传进一个key,返回一个value。也就是说根据上面的代码,当mt这个表被设置为instance的元表之后,当以后外界要从instance取东西出去的时候,会先在表本身里面找(rawget),如果找不到了,再继续在找ret表里面找。

看完上面的解释,我相信还是有一部分读者会睁着大眼睛问我,你说了这么多,那和面向对象有鸡毛关系?没事,笔者这篇文章本来就是想写给对Lua了解不深的朋友看的,所以一定会解释清楚。

我们要知道,在Lua中,方法其实是一个内置类型,成为function。当一个表想调用自己拥有的一个方法的时候有两种形式:

  • t.function(t)
  • t:function()

上面两种形式是等效的,使用冒号连接的时候,会把冒号前面的变量作为后面方法的第一个变量传入。

在上面关于Person的代码例子中,有一句代码我们来看看是怎么最后调用成功的。

1
oldWong:introduce()

在调用introduce()之前,Lua需要先从oldWong这个表中拿出这个属性。这是一种以key换value的操作,于是自然会调用到__index元方法。首先Lua会在oldWong本身中找这个方法,找不到,然后就会在__index那段代码里的ret表里面找。还记得ret最后被返回,被我们持有为叫做Person的表了吗?

所以’oldWong:introduce()’的实质调用,可以看做是

1
Person.introduce(oldWong)

明白了吧。

老王的叛逆

接下来,我们假设老王是一个很叛逆的人,他不想按照我们给他的方法进行自我介绍,想要用自己的方式来展示足够骚的自己,他怎么做呢:

1
2
3
4
5
oldWong.introduce = function(self)
print('i am Xiao Wang, bu yiyang de Xiao Wang')
end

oldWong.introduce()

老王他重写了自己的introduce方法。现在再从老王这个表中再读出introdce这个成员变量的时候,由于老王本身就已经拥有了自我介绍这个方法,于是就直接返回这个方法,不需要再从类里面去寻找原始的introduce方法了。于是老王的自我介绍就自成一派了。

在老王叛逆的故事中,我们可以学到一种debug的方法。在我两年的lua使用时间中,其中一年半是在使用cocos2dx的,以这里为例子解释一下如何使用这个原理来快速debug。

cocos2dx的节点Node类有一个addChild(self, childNode)方法,用来添加一个子节点。现在我发现游戏中有一个节点oldWongNode,莫名其妙地添加了一个叫做runNode的节点,但是由于前人写的代码太冗长太垃圾,找了半天都找不到究竟这个子节点是在哪里被添加的。这个时候我想起了老王的故事,这么写了一段代码,然后发现了究竟是哪个凶手同事调用了调用了这个方法添加了runNode:

1
2
3
4
5
6
7
local rawAddChild = oldWongNode.addChild
oldWongNode.addChild = function(self, child)
if child.name == 'runNode':
printTraceback()
end
rawAddChild(self, child)
end

未完待续。。。