前言:
每当自己想要放弃的时候,可以告诉自己再多撑一天、一个星期、一个月,再多撑一年吧。你会发现拒绝退场的结果令人惊讶!
--摘自短篇原创文学
----------------------------------------------------------分 割 线-----------------------------------------------------------------
说到脚本语言,我们脑海之中跳出像Python,LUA,Ruby这样的语言!可以这么说,任何大型游戏都有自己的脚本系统,所有如果我们想要做出一款比较好一点的游戏,那么脚本语言也是我们所必须要掌握的。Lua语言也是由于他的很多特点,比如高效,轻量,接口干净,可以与C语言进行较好的交互等,也正因为这些特点,学起来也比较快!红遍全球的wow就是采用的Lua脚本语言(虽然我没玩过这游戏,不过看别人玩过,貌似现在玩的人少了)。虽然它已经没有当初的辉煌了,不过经典还是经典!
为什么要使用Lua脚本语言
刚刚简单介绍了一下Lua脚本语言,接下来我们要说一说我们为什么要用它,换句话说我们为什么在游戏之中要使用脚本语言。众所周知,Lua脚本是一个很轻量级的脚本,也是号称性能最高的脚本,我们都很清楚,性能对于一个游戏是多么重要!国产单机游戏的招牌大作《仙剑奇侠传六》剧情,人物都还不错,可是性能方面,优化方面总是提不上去,从问世就被玩家一直诟病(不知道现在的情况怎么样了)!前不久听说仙剑奇侠传七已经立项了,而且很有可能采用的是大名鼎鼎的虚幻引擎,所以在画面的表现上可能更真实了,可能会更加接近国外的那么次时代的大作(之前的仙剑好像用的是RenderWare引擎,其实个人感觉其实之前的几代画面也不错),我个人觉得引擎固然重要,但是能让引擎服务于游戏,而不是游戏服务于引擎(现在的市面上就不少打着采用什么什么很有名的游戏引擎,但是实际的游戏性缺很差,完全是为了贴合这些著名的引擎而设计出来的游戏,自然没有什么游戏性可言了)!作为仙剑的忠实粉丝,希望这次不要大家失望哦!
扯了一些其他的内容,回归正题吧!游戏之中为什么要用脚本语言!原因可能有以下三点吧!
首先学习门槛低,哪怕是新人也可以快速上手!
其次就是开发成本低,可维护性比较强!
最后就是它是一种动态语言,灵活性很高!
相比较C/C++这样的高复杂性、高风险的编译型语言来说,Lua作为一种轻量级的动态语言,简单的语言特性,精简的基础以及基础库,使得语言学习的门槛大大降低。C++在开发过程之中的要求特别高,它的高效性也造成了它的高复杂性,极易产生Bug,对于开发人员的要求就很高了!从语言的抽象层次来说C/C++抽象低,所以C/C++更加适合底层逻辑的支持,而Lua脚本抽象层次高,更加适合游戏逻辑的实现。脚本语言运行在虚拟机之上,而虚拟机又运行在游戏逻辑之上,Lua作为一种解释型语言,我们可以随时修改并把它体现在游戏之中,以便于快速完成开发。很可惜C/C++做不到,如果说有一个大型的游戏工程,每次修改都需要重新编译,这样下来的成本会很高。所以说如果一个游戏之中所有的功能都使用C/C++来实现,那么对于游戏开发人员来说是一个噩梦!
关于运用的场景来说,举个例子就是当你在希望在游戏开始的时候读取一些信息,以配置你的游戏。通常来说,这些内容都是放到一个文本文件之中,当你的游戏启动的时候,你需要打开这个文件,然后解析字符串,找到所需要的信息。
就目前来说Lua语言在嵌入式开发和游戏开发过程之中运用的最多了!
Lua中的一些知识点
主要讲一下Lua语言之中的一些语法上的小知识,也算是一些比较基础的知识吧。
变 量
关于变量---Lua的数字只有Double类型的,64bits,即使是这样,Lua处理数据的速度并不慢,而且语法也很简单,假设我们想要有一个变量var,那么可以这样写:
var = 1024
var = 3.0
var = 3.1416
var = 314.16e-2
var = 0.31416E1
var = 0xff
var = 0x56
Lua是一种
动态类型的语言。在语言之中没有类型定义的语法,每个值都“携带”了它自身的类型信息。在Lua之中基础类型有八种:nil(空)、boolean(布尔)、number(数字)、string(字符串)、userdata(自定义类型)、function(函数)、thread(线程)和表(table)。可以用
type函数来判断类型。
在Lua之中变量没有预定义的类型,任何变量都可以包含任何类型的值,其他类型大家都可以很容易理解,需要注意一下的就是nil类型了,它的功能主要就是区别于其他任何值,一个全局变量在第一次赋值之前的默认值就是nil,将nil赋予一个全局变量就等同与删除它,所以这点需要说明一下。
字符串的话,在Lua之中你可以用单引号,也可以用双引号,如下面的方式(这几种方式是完全相同的)
a = 'alo\n123"'
a = "alo\n123\""
a = '\97lo\10\04923"'
a = [[alo
123"]]
还有一点需要关注的就是全局变量与局部变量,
lua中的变量如果没有特殊说明,全是全局变量,那怕是语句块或是函数里。变量前加local关键字的是局部变量。
在Lua之中,有一种类型是不得不说的---就是Table(表),它实现了"关联数组",所谓的“关联数组”是一种具有特殊索引方式的数组。不仅可以通过整数来索引它,还可以使用字符串或者其他类型的值(出了nil)来索引它。table是Lua中主要的同事也是仅有的数据结构的机制,具有强大的功能,它没有固定的大小,你可以动态地添加任意元素到一个table之中。通俗点说的话,所谓Table其实就是一个Key Value的数据结构,它很像Javascript中的Object,或是PHP中的数组,在别的语言里叫Dict或Map,或者你可以想一下STL之中的Map(不过还是有很大差别的,STL之中Map是基于红黑树来实现的)
创建一个简单的Table如下:
person = {name="Yasuo", age=20, handsome=True}
虽然上面看上去像C/C++中的结构体,但是name,age, handsome都是key。你还可以像下面这样写义Table:(这样会显得更像key-value的结构)
t = {[20]=100, ['name']="Yasuo", [sex]="男"}
再如像以下的定义:
arr1 = {[1]=10, [2]=20, [3]=30, [4]=40, [5]=50}
arr2 = {"hello", 10, "Yasuo", function() print("www.csdn.con") end}
没错,函数也可以放在table之中,如果需要调用的话,可以这样写arr2[4](),而且这里需要说明一下,在Lua之中,table的下标是从1开始的,而不是从0开始的,这与我们熟悉的C/C++语言不同,所以这个需要特别注意一下。所以在遍历的时候我们可以这样写:
for i=1, #arr1 do
print(arr1[i])
end
上面的程序之中#arr的意思就是arr的长度,还有一点就是Lua也是用Table来管理全局变量的,Lua把这些全局变量放在了一个叫“_G”的Table里。关于变量的先说这么多!
语 句
---if-else分支语句:
if age == 40 and sex =="Male" then
print("Man")
elseif age > 60 and sex ~="Female" then
print("old man without country!")
elseif age < 20 then
io.write("too young, too naive!\n")
else
local age = io.read()
print("Your age is "..age)
end
简单说明一下,1.-- 在Lua之中不等于是~=而不是我们在C/C++之中用的!=。
2.-- 我们在C/C++之中做输入输出的时候经常是采用scanf(cin),printf(cout)函数从标准输入、标准输出来进行读写。但是在Lua之中使用read、write函数,不过挺类似的io库的分别从stdin和stdout读写的read和write函数。
3.-- 我们在Lua之中使用..作为拼接操作符。
4.-- 在条件表达式之中我们使用and,or,not作为关键字,不过也有些差别,有兴趣可以去了解一下。
----for循环语句:
for语句有两种形式,一种是数字型for和泛型for
先来看看数字型for,来个典型的示例:
for i=1,10 do --顺序打印1到10
print(i)
end
for i=1,10, 1 do --顺序打印1到10
print(i)
end
for i=10,1,-1 do --逆序打印1到10
print(i)
end
再来看一看泛型for,泛型for循环通过一个迭代器(iterator)函数来遍历所有值,来看一个例子吧!
a = {1, 2, 3, 4}
for i,v in ipairs(a) do --结果输出1 2 3 4
print(v)
end
Lua基础库提供了一个ipairs,这是一个用于
遍历数组的迭代器函数。每次循环过程之中,i会被赋予一个索引值,同事v被赋予一个对应于该
索引的数组元素元素值。
在Lua之中还提供了一个pairs,虽然与ipairs类似,不过通过下面的例子就可以看出不同。
a = {1, 2, 3, 4}
b = {1, 2, 3, 4, ["5"] = 5}
for i,v in ipairs(a) do --结果输出1 2 3 4
print(v)
end
print(" ")
for i,v in ipairs(b) do --结果还是1 2 3 4
print(v)
end
print(" ")
for k,v in pairs(b) do --结果输出1 2 3 4 5
print(v)
end
ipairs只会索引出key值为数字的内容,如果不是数字的,for循环就结束了,但是pairs则会一直向后索引,一直到key值为nil。
--- until循环:
sum = 2
repeat
sum = sum ^ 2 --幂操作
print(sum)
until sum >1000
---
函数的定义:
function fib(n)
if n < 2 then return 1 end
return fib(n - 2) + fib(n - 1)
end
这里有必要说一下,Lua之中函数是可以返回多个值的,如下:
function getUserInfo(id)
print(id)
return "Yasuo", 20, "163.com", "http://www.baidu.com"
end
name, age, email, website, n= getUserInfo()
另外地,由于返回值只有4个,因此n会被赋值为nil,这里没有传id,所有打印出的id为nil。
Lua之中面向对象
关于这部分内容,我之前看到一个总结地比较好的人的讲解,在这里我直接先拿过来用了!由于原文被多次转载了,所以就不知道地址了,所有有兴趣的可以去网上搜一下!
要理解Lua之中的面向对象,首先我们要来了解一下Lua之中的MetaTable和MetaMethod,它们是Lua中的重要的语法,MetaTable主要是用来做一些类似于C++重载操作符式的功能。
比如,我们有两个分数:
fraction_a = {numerator=2, denominator=3}
fraction_b = {numerator=4, denominator=7}
我们想实现分数间的相加:2/3 + 4/7,我们如果要执行: fraction_a + fraction_b,会报错的。所以,我们可以动用MetaTable,如下所示:
fraction_op={}
function fraction_op.__add(f1, f2)
ret = {}
ret.numerator = f1.numerator * f2.denominator + f2.numerator * f1.denominator
ret.denominator = f1.denominator * f2.denominator
return ret
end
为之前定义的两个table设置MetaTable:(其中的setmetatble是库函数)
setmetatable(fraction_a, fraction_op)
setmetatable(fraction_b, fraction_op)
于是你就可以这样干了:(调用的是fraction_op.__add()函数)
fraction_s = fraction_a + fraction_b
至于__add这是MetaMethod,这是Lua内建约定的,其它的还有如下的MetaMethod:
__add(a, b) 对应表达式 a + b
__sub(a, b) 对应表达式 a - b
__mul(a, b) 对应表达式 a * b
__div(a, b) 对应表达式 a / b
__mod(a, b) 对应表达式 a % b
__pow(a, b) 对应表达式 a ^ b
__unm(a) 对应表达式 -a
__concat(a, b) 对应表达式 a .. b
__len(a) 对应表达式 #a
__eq(a, b) 对应表达式 a == b
__lt(a, b) 对应表达式 a < b
__le(a, b) 对应表达式 a <= b
__index(a, b) 对应表达式 a.b
__newindex(a, b, c) 对应表达式 a.b = c
__call(a, ...) 对应表达式 a(...)
我们可以看到上面的表之中有个__index的东西,
这个东西主要是重载了find key的操作,所以才使得变得有点面向对象的感觉了,
有点像Javascript的prototype。
所谓__index,说得明确一点,如果我们有两个对象a和b,我们想让b作为a的prototype只需要:
setmetatable(a, {__index = b})
例如下面的示例:你可以用一个Window_Prototype的模板加上__index的MetaMethod来创建另一个实例:
Window_Prototype = {x=0, y=0, width=100, height=100}
MyWin = {title="Hello"}
setmetatable(MyWin, {__index = Window_Prototype})
于是:MyWin中就可以访问x, y, width, height的内容了。(注:当表要索引一个值时如table[key], Lua会首先在table本身中查找key的值, 如果没有并且这个table存在一个带有__index属性的Metatable, 则Lua会按照__index所定义的函数逻辑查找)
有了以上的基础,我们可以来说说所谓的Lua的面向对象。
Person={}
function Person:new(p)
local obj = p
if (obj == nil) then
obj = {name="Yasuo", age=20, handsome=true}
end
self.__index = self
return setmetatable(obj, self)
end
function Person:toString()
return self.name .." : ".. self.age .." : ".. (self.handsome and "handsome" or "ugly")
end
上面我们可以看到有一个new方法和一个toString的方法。其中:
1)self 就是 Person,Person:new(p),相当于Person.new(self, p)
2)new方法的self.__index = self 的意图是怕self被扩展后改写,所以,让其保持原样
3)setmetatable这个函数返回的是第一个参数的值。
于是:我们可以这样调用:
me = Person:new()
print(me:toString())
kf = Person:new{name="King's fucking", age=70, handsome=false}
print(kf:toString())
继承如下,我就不多说了,Lua和Javascript很相似,都是在Prototype的实例上改过来改过去的。
Student = Person:new()
function Student:new()
newObj = {year = 2013}
self.__index = self
return setmetatable(newObj, self)
end
function Student:toString()
return "Student : ".. self.year.." : " .. self.name
end
关于其他的一些内容,比如像模块之类的我就不写了!有兴趣的可以去看一看《Lua程序设计(第二版)》这本书,里面关于Lua的内容讲的很详细!
如何灵活运用Lua
上面说了那么多,不过最终还是要归结到一个用字上面!由于在“配置”方面,Lua可以给你更加灵活的表达方式,这也是Lua受青睐的原因之一吧!你可以像以下这样配置:
if player:is_dead() then
do_something()
else
do_else()
end
而且写完之后,如果你发现之前写的不太好,想要修改一下,那么你可以尽情修改,在你修改完了之后,你并不需要重新编译你的游戏代码!而且通常你并不希望在游戏之中还有一个单独的解释器,你需要在
游戏之中运行解释器。那么如何在代码之中运行呢?
所以接下来就要说一说有关于Lua与C/C++的交互了,关于这方面的内容,就得先从最基本的Lua解释器的工作机制来说起,简单的说就是Lua解释器自身维护一个运行时栈,通过这个运行时栈,Lua解释器向主机程序传递参数,所以很多时候我们都是对这个栈来进行操作。
所以提供了一系列的C API来进行相关的操作,下面给出常用的API
luaL_newstate函数用于初始化一个lua_State实例
luaL_openlibs函数用于打开Lua中的所有标准库,如io库、string库等。
luaL_loadbuffer编译了buff中的Lua代码,如果没有错误,则返回0,同时将编译后的程序块压入虚拟栈中。
lua_pcall函数会将程序块从栈中弹出,并在保护模式下运行该程序块。执行成功返回0,否则将错误信息压入栈中。
lua_tostring函数中的-1,表示栈顶的索引值,栈底的索引值为1,以此类推。该函数将返回栈顶的错误信息,但是不会将其从栈中弹出。
lua_pop是一个宏,用于从虚拟栈中弹出指定数量的元素,这里的1表示仅弹出栈顶的元素。
lua_close用于释放状态指针所引用的资源。
入栈操作:
Lua针对每种C类型,都有一个C API函数与之对应,如:
void lua_pushnil(lua_State* L); --nil值
void lua_pushboolean(lua_State* L, int b); --布尔值
void lua_pushnumber(lua_State* L, lua_Number n); --浮点数
void lua_pushinteger(lua_State* L, lua_Integer n); --整型
void lua_pushlstring(lua_State* L, const char* s, size_t len); --指定长度的内存数据
void lua_pushstring(lua_State* L, const char* s); --以零结尾的字符串,其长度可由strlen得出。
出栈操作:
API使用“索引”来引用栈中的元素,第一个压入栈的为1,第二个为2,依此类推。我们也可以使用负数作为索引值,其中-1表示为栈顶元素,-2为栈顶下面的元素,同样依此类推。
Lua提供了一组特定的函数用于检查返回元素的类型,如:
int lua_isboolean (lua_State *L, int index);
int lua_iscfunction (lua_State *L, int index);
int lua_isfunction (lua_State *L, int index);
int lua_isnil (lua_State *L, int index);
int lua_islightuserdata (lua_State *L, int index);
int lua_isnumber (lua_State *L, int index);
int lua_isstring (lua_State *L, int index);
int lua_istable (lua_State *L, int index);
int lua_isuserdata (lua_State *L, int index);
以上函数,成功返回1,否则返回0。需要特别指出的是,对于lua_isnumber而言,不会检查值是否为数字类型,而是检查值是否能转换为数字类型。
如何才能得到一个脚本变量的值呢?
lua_pushstring(L, "var"); //将变量的名字放入栈
lua_gettatbl(L, LUA_GLOBALSINDEX);//变量的值现在栈顶
假设你在脚本中有一个变量 var = 100,而且刚刚入了栈,如何得到这个值呢?我们可以这样
int var = lua_tonumber(L, -1);
Lua定义了一个宏让你简单的取得一个变量的值:
lua_getglobal(L, name)
所以可以这样说,我们取得一个变量的值变得更加容易了。
lua_getglobal(L, "var"); //变量的值现在栈顶
int var = lua_tonumber(L, -1);
来看一个完整的例子:
#include <iostream>
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
int main()
{
//lua_State *L = lua_open();
//1.创建一个state
lua_State * L = luaL_newstate();
//打开标准库
luaL_openlibs(L);
//2.入栈操作
lua_pushstring(L, "You are so cool!");
lua_pushnumber(L, 20);
//3.取值操作
if(lua_isstring(L, 1))
{
std::cout<<lua_tostring(L, 1)<<std::endl;
}
if(lua_isnumber(L, 2))
{
std::cout<<lua_tonumber(L, 2)<<std::endl;
}
//4.关闭state
lua_close(L);
return 0;
}
输出的结果为:
You are so cool!
20
如何调用函数
关于这个问题,先来说一说如何在VS之中调用Lua之中定义的函数!我们用例子来讲比较好!
假设在Lua文件之中定义了一个函数Inc(),该Lua文件名字为“MyWork.lua”。
function Inc(number)
number = number + 1
return number
end
而在VS中我们可以这么写:
#include <iostream>
#include <io.h>
#include <string>
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
int main()
{
lua_State * L = luaL_newstate();
if (luaL_dofile(L,"MyWork.lua"))//返回0,读取没有错误,返回1,读取发生错误,(同一目录下)路径为相对路径
{
printf("error\n");
}
lua_getglobal(L, "Inc"); //函数现在栈顶
lua_pushnumber(L, 100); //压入参数
lua_pcall(L, 1, 1, 0); //调用函数,有一个参数,一个返回值
int result = (int)lua_tonumber(L, -1); //获取栈顶元素的值
printf("%d\n", result);
return 0;
}
如此我们就完成了,在VS之中调用Lua函数!
最后我们来看一看如何在Lua之中调用VS之中定义的函数,假设我们在VS之中定义的函数如下:
static int dir(lua_State * L) //该函数是获取当前路径下的所有文件名称
{
std::cout << "[C++] dir C++ dir" << std::endl;
std::string to_search = luaL_checkstring(L, 1); //获取路径
long hfilehandle = 0; //用于查找文件的句柄
struct _finddata_t fileinfo;
std::string pathName, ret;
hfilehandle = _findfirst(pathName.assign(to_search).append("\\*").c_str(), &fileinfo); //获取句柄
if(-1 == hfilehandle)
return -1;
do
{
ret.append(fileinfo.name);
ret.append("\n");
} while (!_findnext(hfilehandle, &fileinfo));
_findclose(hfilehandle); //关闭
lua_pushstring(L, ret.c_str()); //将结果压回去
return 1; //返回值为1个
}
在Lua文件之中,我们只写这么一句内容,主要是调用这个在VS之中定义的函数dir()
print(dir("."))
最后我们来编写VS之中的main()函数!
int main()
{
lua_State * L = luaL_newstate(); //创建state环境
luaL_openlibs(L); //打开标准库
//注册函数方法一
std::cout << "[C++] Pushing the C++ function" << std::endl;
lua_pushcfunction(L, dir); //进行函数的注册
lua_setglobal(L, "dir");
//注册函数方法二
//lua_register(L, "dir", dir);
if (luaL_dofile(L,"MyWork.lua")) //读取Lua文件
{
printf("error\n");
}
lua_close(L);
return 0;
}
编写完毕之后,接着运行!打印出结果如下:
注:我的Lua文件与VS文件都放在同一目录之下!
好了,这次关于Lua的内容就说这么多了!如果还想深入了解,可以仔细看看《Lua程序设计》这本书,或者读一读更加深层次的有关于Lua的用书!