游戏之中的粘合剂---Lua脚本语言

前言:

   每当自己想要放弃的时候,可以告诉自己再多撑一天一个星期一个月,再多撑一年吧。你会发现拒绝退场的结果令人惊讶!

                                                          --摘自短篇原创文学     

----------------------------------------------------------分 割 线----------------------------------------------------------------- 

        说到脚本语言,我们脑海之中跳出像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的用书!


  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值