前言
做了三年的游戏开发,其中有两年都在使用Lua这一脚本语言,想着是时候写点什么东西输出一下,算是给个交代。本文既不是讲Lua入门,也不会讲到Lua虚拟机那么深,读者尽可放大心随意看。
类的实现
原生Lua是不支持面向对象编程的。那怎么办呢?使用prototype模式即可。
首先读者需要知道Lua的原方法和原表这一知识点。如果接触过c++或者c#的同学应该知道我们可以对一些类的操作做重载(overload),改变诸如加减乘除,甚至一些更高级的操作。而在像是Lua这种脚本语言来说是没有重载这一操作的。取而代之的,我们可以通过重写元表来改变table原有的操作逻辑。
1 | local theMetaTable = {} |
在这么多元方法里面有一个最值得关注的,称为__index
。顾名思义,传入一个key,返回一个value。接下来就来讲解如何使用这个元方法实现面向对象。首先我们假设有一个方法叫做class(string name)
,会生成一个带有构造方法和其他各种方法的类原型(类原型是啥意思?类似于Java里的Class,c#里的Type),我们使用这个类原型就可以实例化出我们的实例来。使用方法大概是这样子的:
1 | Person = class('class') |
明眼的读者可以发现了:从class函数返回的Person
一定是一个带着new方法的table。但是为什么这个new方法会生成新的实例呢?我改变oldWong实例的sex
属性之后,再用Person去实例化一个实例,会是什么性别?有兴趣的读者可自行试试。
接下来笔者将为你揭秘__index
方法在class(string name)
函数中的应用。
1 | function class(clzName) |
看一个函数我习惯先看最后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 | oldWong.introduce = function(self) |
老王他重写了自己的introduce方法。现在再从老王这个婊表中再读出introdce这个成员变量的时候,由于老王本身就已经拥有了自我介绍这个方法,于是就直接返回这个方法,不需要再从类里面去寻找原始的introduce方法了。于是老王的自我介绍就自成一派了。
在老王叛逆的故事中,我们可以学到一种debug的方法。在我两年的lua使用时间中,其中一年半是在使用cocos2dx的,以这里为例子解释一下如何使用这个原理来快速debug。
cocos2dx的节点Node类有一个addChild(self, childNode)
方法,用来添加一个子节点。现在我发现游戏中有一个节点oldWongNode,莫名其妙地添加了一个叫做runNode的节点,但是由于前人写的代码太冗长太垃圾,找了半天都找不到究竟这个子节点是在哪里被添加的。这个时候我想起了老王的故事,这么写了一段代码,然后发现了究竟是哪个凶手同事调用了调用了这个方法添加了runNode:
1 | local rawAddChild = oldWongNode.addChild |
未完待续。。。