|
Sam Lantinga, 首席程序员, Loki Entertainment Software
1999 年 9 月 01 日
Sam
Lantinga 是 Simple DirectMedia Layer (SDL) 库的作者和 Loki Entertainment
的首席开发人员,他将向您介绍一种将游戏移植到 Linux上的优秀工具。SDL
是一个跨平台代码移植的理想工具,它支持许多平台,如Linux、Solaris、IRIX、FreeBSD 和 MacOS,这对于那些认为可以在
Linux上开发商业软件的 Linux 开发者来说是一大进步。他向社区的前辈之一讨教SDL 如何使 Linux
用户享受任何平台上最好的游戏,SDL如何帮助开发者跟上下一代计算机游戏迷的要求。
Sam
Lantinga 是 Simple DirectMedia Layer (SDL) 库的作者和 Loki Entertainment
的首席开发人员,他将向您介绍一种将游戏移植到 Linux 上的优秀工具。SDL 是一个跨平台代码移植的理想工具,它支持许多平台,如
Linux、Solaris、IRIX、FreeBSD 和 MacOS,这对于那些认为可以在 Linux 上开发商业软件的 Linux
开发者来说是一大进步。他向社区的前辈之一讨教 SDL 如何使 Linux 用户享受任何平台上最好的游戏,SDL
如何帮助开发者跟上下一代计算机游戏迷的要求。
自从 Linus
首先开发出 Linux 时开始,到现在 Linux 成为所有黑客的梦想并且遍及全世界,Linux 开发最重要的元素之一就是 OS
上游戏的质量和可用性。游戏是我们用来娱乐和休闲的。它们可以提高创造力并拓展思路。游戏还可以用来测量操作系统的性能。由于游戏越来越复杂,它们迫使每
个子系统逼近极限。每当我装配一个系统时,首先要做的就是装入一个游戏并试玩,以“测试”每一项的性能。
Linux 上的游戏已经存在了很长时间。从早期的 NetTrek,到受高度赞扬的
DOOM!和
雷神 (Quake),人们已经可以在 Linux 上玩游戏了。但问题是没有足够的游戏。没有哪家大公司为 Linux 创作能产生轰动效应的游戏。但是,由于该操作系统变得日益流行,这种情况正开始改善。
Linux 上最早期的游戏使用 X11 协议。但是对于游戏来说,X11 实在太慢了,因为它是针对在网络上透明运行的基于菜单的应用而设计的协议。使用它的游戏通常没有绚丽的画面,而且运行得相当慢。
DOOM!是
一个值得注意的例外,虽然它使用 X11,但是它通过使用 MIT 共享内存扩展可以使动画更流畅,并提供了逼真的三维效果。还有一些游戏使用
SVGA 图形库,SVGAlib。我最喜欢的一个老游戏是重力战争 (Gravity Wars),它对其模拟的老 Amiga 游戏
Gravity Force 做了重大改动。但使用 SVGAlib 的程序只能适用于少数受支持的显卡。
早期 X11 游戏,
争霸 (Craft)的图片。
|
神话 2 (Myth 2)的图片,Loki 出品
|
|
|
今天,游戏开发者有了更多的选择。仍然可以使用
X 工具箱或全屏 API,如 SVGAlib 或 fbcon,来编写游戏,但他们现在还有许多游戏库可以使用。Simple
DirectMedia Layer 库是 Linux 上最好的低层游戏开发 API 之一。
SDL 是什么?
Simple DirectMedia Layer 库,简称
SDL,是为数不多的商业游戏开发公司使用的免费软件库之一。它提供跨平台的二维帧缓冲区图形和音频服务,它支持 Linux、Win32 和
BeOS。也不同程度地支持其它平台,包括 Solaris、IRIX、FreeBSD 和
MacOS。除了大量的服务,包括线程、独立于字节存储次序的宏和 CD 音频,SDL 还提供了一个简单的
API,它允许您尽可能接近本机硬件。使用 SDL 有三重优点:稳定、简单和灵活。
-
稳定。如果 SDL 不向 API
提供可靠的支持,那么那些爱好者和商业公司就不能使用它。因为使用了 SDL,就添加了错误修正并增强了性能,也就加强了 API
的强健性。就像内核开发是分步进行的,SDL 的开发也是分步进行的,其中一部分是可靠稳定的 API,其它部分是新功能和构思的沙箱。
-
简单。SDL 被设计成一个简单的 API,以最少的代码实现您的构思。比如,我最近从 Linux 演示组
Optimum中移植了一些演示程序,我将它们的 X11 代码替换成 SDL 代码(请参见下面的列表)。您可以看到,SDL 代码非常易于编写和理解。
X11 代码
int init_x (int X, int Y, int W, int H, int bpp, const char *Name) { XPixmapFormatValues *formatList; int formatCount; int i; int formatOk; int scanlineLength; XGCValues gcVal; unsigned long gcMask; dis = XOpenDisplay ( NULL ); if ( dis == NULL) { fprintf ( stderr , "Error :\n" ); fprintf ( stderr , " Cannot connect to Display.\n"); exit (1); } screen = DefaultScreen ( dis ); depth = DefaultDepth ( dis , screen ); width = DisplayWidth ( dis , screen ); height = DisplayHeight ( dis , screen );
winRoot = DefaultRootWindow ( dis ); winAttr.border_pixel = BlackPixel ( dis , screen ); winAttr.background_pixel = BlackPixel ( dis , screen ); winMask = CWBackPixel | CWBorderPixel;
formatList = XListPixmapFormats( dis, &formatCount); if (formatList == NULL){ fprintf ( stderr , " Cannot get pixmap list\n"); exit (1); } formatOk=-1; for (i=0; ibytes_per_line*xim->height, IPC_CREAT|0777); xim->data = SHMInfo.shmaddr = (char *)shmat(SHMInfo.shmid, 0, 0); SHMInfo.readOnly = False; XShmAttach(dis, &SHMInfo); XSync(dis, False); buffer=(unsigned char *)xim->data; #else buffer = (unsigned char *)calloc(W*H, pixmapFormat.bits_per_pixel/8); xim = XCreateImage ( dis , CopyFromParent , depth , ZPixmap , 0 , (char *) buffer , W , H , pixmapFormat.scanline_pad, scanlineLength); if (xim == NULL){ fprintf(stderr, " Couldnt create Ximage..\n"); exit(-1); } #endif gcVal.foreground = 0; gcVal.background = 0; gcMask = GCForeground | GCBackground; gc = XCreateGC ( dis , win , gcMask , &gcVal ); if (depth==24) depth = pixmapFormat.bits_per_pixel; return (depth); }
|
SDL 代码
int init_x (int X, int Y, int W, int H, int bpp, const char *Name) { int i; if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) { fprintf ( stderr , "Erreur :\n" ); fprintf ( stderr , " Impossible de se connecter au Display\n"); exit (1); } screen = SDL_SetVideoMode(W, H, bpp, SDL_SWSURFACE|SDL_HWPALETTE); if ( screen == NULL ) { fprintf ( stderr , "Erreur :\n" ); fprintf ( stderr , " Impossible de se connecter au Display\n"); exit (1); } SDL_WM_SetCaption ( Name, Name ); for ( i=SDL_NOEVENT; iformat->BitsPerPixel; width = screen->w; height = screen->h; buffer = (unsigned char *)screen->pixels; return (depth); }
|
-
灵活。
返回到上面的 Optimum 演示代码示例,只要移植到 SDL,并确定一些数据假设,那么根本不必改动代码,演示就可以在 Win32、BeOS
和 Linux 控制台上运行了。灵活性的另一方面体现在尽管代码完全是跨平台的,但不会把您和底层实现隔开。SDL 提供了函数
SDL_GetWMInfo(),该函数可以让您访问底层驱动程序的专用窗口信息。Loki Entertainment Software
广泛使用这一技术为它们的游戏智能窗口管理器交互。
这种坚如磐石般的稳定、简单和强大功能的组合已经给 Linux 带来了一些极其引人入胜的游戏,包括
Hopkins F.B.I.、
文明:力量的呼唤 (Civilization: Call To Power)、
神话 2:Soulblighter (MythII: Soulblighter)、
铁路大亨 2 (Railroad Tycoon II)等等。编程爱好者和商业公司使用这个库的事实表示它正在日益提高其功能和稳定性。这符合实际游戏的实际需要。
文明:力量的呼唤 (Civilization: Call To Power)的图片
|
天旋地转 2 (Descent 2)的图片
|
|
|
|
7.Iterators and the Generic for 在这一章我们讨论为范性for写迭代器, 我们从一个简单的迭代器开始,然后我们学习如何通过利用范性for的强大之处写出更高效的迭代器. 7.1 迭代器与闭包 迭代器是一种支持指针类型的结构,它可以使遍历集合的每一个元素.在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素.
迭代器需要保留上一次成功调用的状态和下一次成功调用的状态,也就是他知道来自于哪里和将要前往哪里.闭包提供的机制可以很容易实现这个任务.记住:闭包
是一个内部函数,它可以访问一个或者多个外部函数的外部局部变量.每次闭包的成功调用后这些外部局部变量都保存他们的值(状态).当然如果要创建一个闭包
必须要创建其外部局部变量.所以一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数). 举一个简单的例子,我们为一个list写一个简单的迭代器,与ipairs()不同的是我们实现的这个迭代器返回元素的值而不是索引下标: function list_iter (t) local i = 0 local n = table.getn(t) return function () i = i + 1 if i <= n then return t[i] end end end 这个例子中list_iter 是一个工厂,每次调用他都会创建一个新的闭包(迭代器本身).闭包包村内部局部变量(t,i,n),因此每次调用他返回list中的下一个元素值,当list中没有值时,返回nil.我们可以在while语句中使用这个迭代器: t = {10, 20, 30} iter = list_iter(t) -- creates the iterator while true do local element = iter() -- calls the iterator if element == nil then break end print(element) end 我们设计的这个迭代器也很容易用于范性for语句 t = {10, 20, 30} for element in list_iter(t) do print(element) end 范性for为迭代循环处理所有的薄记(bookkeeping):首先调用迭代工厂;内部保留迭代函数,因此我们不需要iter变量;然后在每一个新的迭代处调用迭代器函数;当迭代器返回nil时循环结束(后面我们将看到范性for能胜任更多的任务). 下面看一个稍微高级一点的例子:我们写一个迭代器遍历一个文件内的所有匹配的单词.为了实现目的,我们需要保留两个值:当前行和在当前行的偏移量,我们使用两个外部局部变量line,pos保存这两个值. function allwords () local line = io.read() -- current line local pos = 1 -- current position in the line return function () -- iterator function while line do -- repeat while there are lines local s, e = string.find(line, "%w+", pos) if s then -- found a word? pos = e + 1 -- next position is after this word return string.sub(line, s, e) -- return the word else line = io.read() -- word not found; try next line pos = 1 -- restart from first position end end return nil -- no more lines: end of traversal end end
迭代函数的主体部分调用了string.find函数,string.find在当前行从当前位置开始查找匹配的单词,例子中匹配的单词使用模式'%w+
'描述的;如果查找到一个单词,迭代函数更新当前位置pos为单词后的第一个位置,并且返回这个单词(string.sub函数从line中提取两个位置
参数之间的子串).否则迭代函数读取新的一行并重新搜索.如果没有line可读返回nil结束. 尽管迭代函数有些复杂,但使用起来是很直观的: for word in allwords() do print(word) end 通常情况下,迭代函数都难写易用.这不是一个大问题:一般Lua编程不需要自己定义迭代函数,而是使用语言提供的,除非确实需要自己定义. 7.2 范性for的语义
前面我们看到的迭代器有一个缺点:每次调用都需要创建一个闭包,大多数情况下这种做法都没什么问题,例如在allwords迭代器中创建一个闭包的代价比
起读整个文件来说微不足道,然后在有些情况下创建闭包的代价是不能忍受的.在这些情况下我们可以使用范性for本身来保存迭代的状态. 前面我们看到在循环过程中范性for在自己内部保存迭代函数,实际上它保存三个值:迭代函数,状态常量和控制变量.下面详细说明. 范性for的文法如下: for <var-list> in <exp-list> do <body> end <var-list>是一个或多个以逗号分割变量名的列表,<exp-list>是一个或多个以逗号分割的表达式列表,通常情况下exp-list只有一个值:迭代工厂的调用. for k, v in pairs(t) do print(k, v) end 变量列表k,v;表达式列表pair(t),在很多情况下变量列表也只有一个变量,比如: for line in io.lines() do io.write(line, '\n') end 我们称变量列表中第一个变量为控制变量,其值为nil时循环结束. 下面我们看看范性for的执行过程: 首先,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数,状态常量和控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略. 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数). 第三,将迭代函数返回的值赋给变量列表. 第四,如果返回的第一个值为nil循环结束,否则执行循环体. 第五,回到第二步再次调用迭代函数. 更精确的来说: for var_1, ..., var_n in explist do block end 等价于 do local _f, _s, _var = explist while true do local var_1, ... , var_n = _f(_s, _var) _var = var_1 if _var == nil then break end block end end 如果我们的迭代函数是f,状态常量是s,控制变量的初始值是a0,那么控制变量将循环:a1=f(s,a0);a2=f(s,a1);...直到ai=nil 7.3 无状态的迭代器 无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价. 每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素.这种无状态迭代器的典型的简单的例子是ipairs,他遍历数组的每一个元素. a = {"one", "two", "three"} for i, v in ipairs(a) do print(i, v) end 迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs和迭代函数都很简单,我们在Lua中可以这样实现: function iter (a, i) i = i + 1 local v = a[i] if v then return i, v end end function ipairs (a) return iter, a, 0 end
当Lua调用ipairs(a)开始循环时,他获取三个值:迭代函数iter,状态常量a和控制变量初始值0;然后Lua调用iter(a,0)返回1,
a[1](除非a[1]=nil);第二次迭代调用iter(a,1)返回2,a[2]...直到第一个非nil元素. Lua库中实现的pairs是一个用next实现的原始方法: function pairs (t) return next, t, nil end 还可以不使用ipairs直接使用next for k, v in next, t do ... end 记住:exp-list返回结果会被调整为三个,所以Lua获取next,t,nil;确切地说当他调用pairs时获取. 7.4 多状态的迭代器
很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到table
内,将table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在table内,所以迭代函数通常不需要第二个参数. 下面我们重写allwords迭代器,这一次我们不是使用闭包而是使用带有两个域(line,pos)的table. 开始迭代的函数是很简单的,他必须返回迭代函数和初始状态: local iterator -- to be defined later function allwords () local state = {line = io.read(), pos = 1} return iterator, state end
真正的处理工作是在迭代函数内完成: function iterator (state) while state.line do -- repeat while there are lines -- search for next word local s, e = string.find(state.line, "%w+", state.pos) if s then -- found a word? -- update next position (after this word) state.pos = e + 1 return string.sub(state.line, s, e) else -- word not found state.line = io.read() -- try next line... state.pos = 1 -- ... from first position end end return nil -- no more lines: end loop end
我们应该尽可能的写无状态的迭代器,因为这样循环的时候由for来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭
包;尽可能不要使用table这种方式,因为创建闭包的代价要比创建table小,另外Lua处理闭包要比处理table速度快些.后面我们还将看到另一
种使用协同来创建迭代器的方式,这种方式功能更强但更复杂. 7.4 真正的迭代器 迭代器的名字有一些误导,因为它并没有迭代,完成迭代功能的是for语句,也许更好的叫法应该是'生成器';但是在其他语言比如java,C++迭代器的说法已经很普遍了,我们也将沿用这种术语. 有一种方式创建一个在内部完成迭代的迭代器.这样当我们使用迭代器的时候就不需要使用循环了;我们仅仅使用每一次迭代需要处理的任务作为参数调用迭代器即可,具体地说,迭代器接受一个函数作为参数,并且这个函数在迭代器内部被调用. 作为一个具体的例子,我们使用上述方式重写allwords迭代器: function allwords (f) -- repeat for each line in the file for l in io.lines() do -- repeat for each word in the line for w in string.gfind(l, "%w+") do -- call the function f(w) end end end 如果我们想要打印出单词,只需要 allwords(print) 更一般的做法是我们使用匿名函数作为作为参数,下面的例子打印出单词'hello'出现的次数: local count = 0 allwords(function (w) if w == "hello" then count = count + 1 end end) print(count) 用for结构完成同样的任务: local count = 0 for w in allwords() do if w == "hello" then count = count + 1 end end print(count) 真正的迭代器风格的写法在Lua老版本中很流行,那时还没有for循环. 两种风格的写法相差不大,但也有区别:一方面,第二种风格更容易书写和理解;另一方面,for结构更灵活,可以使用break和continue语句 ;在真正的迭代器风格写法中return语句只是从匿名函数中返回而不是退出循环.
http://www.swarmagents.com/thesis/index.asp
http://ly4cn.teeta.com/blog/directory/3608/
http://www.ferzkopp.net/joomla/content/view/19/14/
www.devmaster.net
并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制。大致上有这么两种多任务技术,一种是抢占式多任务
(preemptive multitasking),它让操作系统来决定何时执行哪个任务。另外一种就是协作式多任务(cooperative
multitasking),它把决定权交给任务,让它们在自己认为合适的时候自愿放弃执行。这两种多任务方式各有优缺点,前者固有的同步问题使得程序经
常有不可预知的行为,而后者则要求任务具备相当的自律精神。 协程(coroutine)技术是一种程序控制机制,早在上世纪60年代就已
提出,用它可以很方便地实现协作式多任务。在主流的程序语言(如C++、Java、Pascal等)里我们很少能看到协程的身影,但是现在不少动态脚本语
言(Python、Perl)却都提供了协程或与之相似的机制,其中最突出的便是Lua。 Lua语言实现的协程是一种非对称
式(asymmetric)协程,或称半对称式(semi-asymmetric)协程,又或干脆就叫半协程(semi-coroutine)。这种协程
机制之所以被称为非对称的,是因为它提供了两种传递程序控制权的操作:一种是(重)调用协程(通过coroutine.resume);另一种是挂起协程
并将程序控制权返回给协程的调用者(通过coroutine.yield)。一个非对称协程可以看做是从属于它的调用者的,二者的关系非常类似于例程
(routine)与其调用者之间的关系。既然有非对称式协程,当然也就有对称式(symmetric)协程了,它的特点是只有一种传递程序控制权的操
作,即将控制权直接传递给指定的协程。曾经有这么一种说法,对称式和非对称式协程机制的能力并不等价,但事实上很容易根据前者来实现后者。接下来我们就用
代码来证明这个事实。 --对称式协程库coro.lua coro = {} --coro.main用来标识程序的主函数 coro.main = function() end -- coro.current变量用来标识拥有控制权的协程, -- 也即正在运行的当前协程 coro.current = coro.main -- 创建一个新的协程 function coro.create(f) return coroutine.wrap(function(val) return nil,f(val) end) end -- 把控制权及指定的数据val传给协程k function coro.transfer(k,val) if coro.current ~= coro.main then return coroutine.yield(k,val) else -- 控制权分派循环 while k do coro.current = k if k == coro.main then return val end k,val = k(val) end error("coroutine ended without transfering control...") end end 如果暂时还弄不懂上面的程序,没关系,看看如何使用这个库后再回头分析。下面是使用示例: require("coro.lua") function foo1(n) print("1: foo1 received value "..n) n = coro.transfer(foo2,n + 10) print("2: foo1 received value "..n) n = coro.transfer(coro.main,n + 10) print("3: foo1 received value "..n) coro.transfer(coro.main,n + 10) end function foo2(n) print("1: foo2 received value "..n) n = coro.transfer(coro.main,n + 10) print("2: foo2 received value "..n) coro.transfer(foo1,n + 10) end function main() foo1 = coro.create(foo1) foo2 = coro.create(foo2) local n = coro.transfer(foo1,0) print("1: main received value "..n) n = coro.transfer(foo2,n + 10) print("2: main received value "..n) n = coro.transfer(foo1,n + 10) print("3: main received value "..n) end --把main设为主函数(协程) coro.main = main --将coro.main设为当前协程 coro.current = coro.main --开始执行主函数(协程) coro.main()
上面的示例定义了一个名为main的主函数,整个程序由它而始,也因它而终。为什么需要一个这样的主函数呢?上面说了,程序控制权可以在对称式协程之间
自由地直接传递,它们之间无所谓谁从属于谁的问题,都处于同一个层级,但是应用程序必须有一个开始点,所以我们定义一个主函数,让它点燃程序运行的导火
线。虽说各个协程都是平等的,但做为程序运行原动力的主函数仍然享有特殊的地位(这个世上哪有绝对的平等!),为此我们的库专门用了一个
coro.main变量来保存主函数,并且在它执行之前要将它设为当前协程(虽然上面的main实际只是一个普通函数而非一个真正的协程,但这并无太大的
关系,以后主函数也被称为主协程)。示例运行的结果是: 1: foo1 received value 0 1: foo2 received value 10 1: main received value 20 2: foo2 received value 30 2: foo1 received value 40 2: main received value 50 3: foo1 received value 60 3: main received value 70 协程的执行序列是:main->foo1->foo2->main->foo2->foo1->main->foo1->main。
coro.transfer(k,val)函数中k是将要接收程序控制权的协程,而val是传递给k的数据。如果当前协程不是主协程,
tansfer(k,val)就简单地利用coroutine.yield(k,val)将当前协程挂起并传回两项数据,即程序控制权的下一站和传递给它
的数据;否则进入一个控制权分派(dispatch)循环,该循环(重)启动(resume)k协程,等待它执行到挂起(suspend),并根据此时协
程传回的数据来决定下一个要(重)启动的协程。从应用示例来看,协程与协程之间似乎是用transfer直接传递控制权的,但实际上这个传递还是通过了主
协程。每一个在主协程里被调用(比较coro.current和coro.main是否相同即可判断出)的transfer都相当于一个协程管理器,它不
断地(重)启动一个协程,将控制权交出去,然后等那个协程挂起时又将控制权收回,然后再(重)启动下一个协程...,这个动作不会停止,除非<
1>将(重)启动的协程是主协程;<2>某个协程没有提供控制权的下一个目的地。很显然,每一轮分派循环开始时都由主协程把握控制权,
在循环过程中如果控制权的下一站又是主协程的话就意味着这个当初把控制权交出去的主协程transfer操作应该结束了,所以函数直接返回val从而结束
这轮循环。对于情况<2>,因为coro.create(f)创建的协程的体函数(body
function)实际是function(val) return nil,f(val)
end,所以当函数f的最后一条指令不是transfer时,这个协程终将执行完毕并把nil和函数f的返回值一起返回。如果k是这样的协程,
transfer执行完k,val =
k(val)语句后k值就成了nil,这被视为一个错误,因为程序此时没法确定下一个应该(重)启动的协程到底是谁。所以在对称式模型下,每一个协程(当
然主协程出外)最后都必须显式地将控制权传递给其它的协程。根据以上分析,应用示例的控制权的分派应为: 第一轮分派: main->foo1->main->foo2->main->main(结束) 第二轮分派: main->foo2->main->foo1->main->main(结束) 第三轮分派: main->foo1->main->main(结束)
由于可以直接指定控制权传递的目标,对称式协程机制拥有极大的自由,但得到这种自由的代价却是牺牲程序结构。如果程序稍微复杂一点,那么即使是非常
有经验的程序员也很难对程序流程有全面而清晰的把握。这非常类似goto语句,它能让程序跳转到任何想去的地方,但人们却很难理解充斥着goto的程序。
非对称式协程具有良好的层次化结构关系,(重)启动这些协程与调用一个函数非常类似:被(重)启动的协程得到控制权开始执行,然后挂起(或结束)并将控制
权返回给协程调用者,这与计算机先哲们倡导的结构化编程风格完全一致。 综上所述,Lua提供的非对称式协程不但具有与对称式协程一样强大的能力,而且还能避免程序员滥用机制写出结构混乱的程序。
http://www.luachina.net/bbs/125/ShowPost.aspx
http://www.personal.kent.edu/~rmuhamma/Compilers/compiler.html
http://www.luachina.net/bbs/125/ShowPost.aspx
http://lua-users.org/wiki/WikiHelp
http://luaplus.org/tiki-index.php
在你的游戏中应用Lua(1):在你的游戏代码中运行解释器
通常,你希望在你的游戏开始的时候读取一些信息,以配置你的游戏,这些信息通常都是放到一个文本文件中,在你的游戏启动的时候,你需要打开这个文件,然后解析字符串,找到所需要的信息。
是的,或许你认为这样就足够了,为什么还要使用Lua呢?
应用于“配置”这个目的,Lua提供给你更为强大,也更为灵活的表达方式,在上一种方式中,你无法根据某些条件来配置你的游戏,Lua提供给你灵活的表达方式,你可以类似于这样来配置你的游戏:
if player:is_dead() then do_something() else do_else() end
更为重要的是,在你做了一些修改之后,完全不需要重新编译你的游戏代码。
通常,在游戏中你并不需要一个单独的解释器,你需要在游戏来运行解释器,下面,让我们来看看,如何在你的代码中运行解释器:
//这是lua所需的三个头文件 //当然,你需要链接到正确的lib #include "lua.h" #include "lauxlib.h" #include "lualib.h"
int main(int argc, char *argv[]) { lua_State *L = lua_open(); luaopen_base(L); luaopen_io(L);
const char *buf = "print('hello, world!')";
lua_dostring(buf);
lua_close(L);
return 0; }
程序输出:hello, world!
有时你需要执行一段字符串,有时你可能需要执行一个文件,当你需要执行一个文件时,你可以这么做: lua_dofile(L, "test.lua");
看,非常简单吧。
在你的游戏中应用Lua(1):Getting Value
在上一篇文章我们能够在我们的游戏代码中执行Lua解释器,下面让我们来看看如何从脚本中取得我们所需要的信息。
首先,让我来简单的解释一下Lua解释器的工作机制,Lua解释器自身维护一个运行时栈,通过这个运行时栈,Lua解释器向主机程序传递参数,所以我们可以这样来得到一个脚本变量的值:
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 "lua.h" #inculde "lauxlib.h" #include "lualib.h"
int main(int argc, char *argv[]) { lua_State *L = lua_open(); luaopen_base(L); luaopen_io(L);
const char *buf = "var = 100";
lua_dostring(L, buf);
lua_getglobal(L, "var"); int var = lua_tonumber(L, -1);
assert(var == 100);
lua_close(L);
return 0; }
在你的游戏中应用Lua(1):调用函数
假设你在脚本中定义了一个函数:
function main(number) number = number + 1 return number end
在你的游戏代码中,你希望在某个时刻调用这个函数取得它的返回值。
在Lua中,函数等同于变量,所以你可以这样来取得这个函数:
lua_getglobal(L, "main");//函数现在栈顶
现在,我们可以调用这个函数,并传递给它正确的参数:
lua_pushnumber(L, 100); //将参数压栈 lua_pcall(L, 1, 1, 0); //调用函数,有一个参数,一个返回值 //返回值现在栈顶 int result = lua_tonumber(L, -1);
result 就是函数的返回值
完整的测试代码如下:
#include "lua.h" #include "lauxlib.h" #include "lualib.h"
int main(int argc, char *argv[]) { lua_State *L = lua_open(); luaopen_base(L);
const char *buf = "function main(number) number = number + 1 return number end";
lua_dostring(buf);
lua_getglobal(L, "main"); lua_pushnumber(L, 100); lua_pcall(L, 1, 1, 0);
int result = lua_tonumber(L, -1);
assert(result == 101);
lua_close(L);
return 0; }
在你的游戏中应用Lua(2):扩展Lua
Lua本身定位在一种轻量级的,灵活的,可扩充的脚本语言,这意味着你可以自由的扩充Lua,为你自己的游戏量身定做一个脚本语言。
你可以在主机程序中向脚本提供你自定的api,供脚本调用。
Lua定义了一种类型:lua_CFunction,这是一个函数指针,它的原型是: typedef int (*lua_CFunction) (lua_State *L);
这意味着只有这种类型的函数才能向Lua注册。
首先,我们定义一个函数
int foo(lua_State *L) { //首先取出脚本执行这个函数时压入栈的参数 //假设这个函数提供一个参数,有两个返回值
//get the first parameter const char *par = lua_tostring(L, -1);
printf("%s\n", par);
//push the first result lua_pushnumber(L, 100);
//push the second result lua_pushnumber(L, 200);
//return 2 result return 2; }
我们可以在脚本中这样调用这个函数
r1, r2 = foo("hello")
print(r1..r2)
完整的测试代码如下:
#include "lua.h" #include "lauxlib.h" #include "lualib.h"
int foo(lua_State *L) { //首先取出脚本执行这个函数时压入栈的参数 //假设这个函数提供一个参数,有两个返回值
//get the first parameter const char *par = lua_tostring(L, -1);
printf("%s\n", par);
//push the first result lua_pushnumber(L, 100);
//push the second result lua_pushnumber(L, 200);
//return 2 result return 2; }
int main(int argc, char *argv[]) { lua_State *L = lua_open(); luaopen_base(L); luaopen_io(L);
const char *buf = "r1, r2 = foo("hello") print(r1..r2)";
lua_dostring(L, buf);
lua_close(L);
return 0; }
程序输出: hello 100200
在你的游戏中应用Lua(3):using lua in cpp
lua和主机程序交换参数是通过一个运行时栈来进行的,运行时栈的信息放在一个lua_State的结构中,lua提供的api都需要一个lua_State*的指针,除了一个:
lua_open();
这个函数将返回一个lua_State*型的指针,在你的游戏代码中,你可以仅仅拥有一个这样的指针,也可以有多个这样的指针。
最后,你需要释放这个指针,通过函数:
lua_close(L);
注意这个事实,在你的主机程序中,open()与close()永远是成对出现的,在c++中,如果有一些事情是成对出现的,这通常意味着你需要一个构造函数和一个析构函数,所以,我们首先对lua_State做一下封装:
#ifndef LUA_EXTRALIBS #define LUA_EXTRALIBS /* empty */ #endif
static const luaL_reg lualibs[] = { {"base", luaopen_base}, {"table", luaopen_table}, {"io", luaopen_io}, {"string", luaopen_string}, {"math", luaopen_math}, {"debug", luaopen_debug}, {"loadlib", luaopen_loadlib}, /* add your libraries here */ LUA_EXTRALIBS {NULL, NULL} };
这是lua提供给用户的一些辅助的lib,在使用lua_State的时候,你可以选择打开或者关闭它。
完整的类实现如下:
//lua_State class state { public: state(bool bOpenStdLib = false) : err_fn(0) { L = lua_open();
assert(L);
if (bOpenStdLib) { open_stdlib(); } }
~state() { lua_setgcthreshold(L, 0); lua_close(L); }
void open_stdlib() { assert(L);
const luaL_reg *lib = lualibs; for (; lib->func; lib++) { lib->func(L); /* open library */ lua_settop(L, 0); /* discard any results */ } }
lua_State* get_handle() { return L; }
int error_fn() { return err_fn; }
private: lua_State *L;
int err_fn; };
通常我们仅仅在游戏代码中使用一个lua_State*的指针,所以我们为它实现一个单件,默认打开所有lua提供的lib:
//return the global instance state* lua_state() { static state L(true);
return &L; }
在你的游戏中应用Lua(3):using lua in cpp(封装栈操作)
前面提到了lua与主机程序是通过一个运行时栈来交换信息的,所以我们把对栈的访问做一下简单的封装。
我们利用从c++的函数重载机制对这些操作做封装,重载提供给我们一种以统一的方式来处理操作的机制。
向lua传递信息是通过压栈的操作来完成的,所以我们定义一些Push()函数:
inline void Push(lua_State *L, int value); inline void Push(lua_State *L, bool value); ...
对应简单的c++内建类型,我们实现出相同的Push函数,至于函数内部的实现是非常的简单,只要利用lua提供的api来实现即可,例如:
inline void Push(lua_State *L, int value) { lua_pushnumber(L, value); }
这种方式带来的好处是,在我们的代码中我们可以以一种统一的方式来处理压栈操作,如果有一种类型没有定义相关的压栈操作,将产生一个编译期错误。
后面我会提到,如何将一个用户自定义类型的指针传递到lua中,在那种情况下,我们的基本代码无须改变,只要添加一个相应的Push()函数即可。
记住close-open原则吧,它的意思是对修改是封闭的,对扩充是开放的,好的类库设计允许你扩充它,而无须修改它的实现,甚至无须重新编译。
《c++泛型设计新思维》一书提到了一种技术叫type2type,它的本质是很简单:
template <typename T> struct type2type { typedef T U; };
正如你看到的,它并没有任何数据成员,它的存在只是为了携带类型信息。
类型到类型的映射在应用于重载函数时是非常有用的,应用type2type,可以实现编译期的分派。
下面看看我们如何在从栈中取得lua信息时应用type2type:
测试类型:由于lua的类型系统与c++是不相同的,所以,我们要对栈中的信息做一下类型检测。
inline bool Match(type2type<bool>, lua_State *L, int idx) { return lua_type(L, idx) == LUA_TBOOLEAN; }
类似的,我们要为cpp的内建类型提供相应的Match函数:
inline bool Match(type2type<int>, lua_State *L, int idx); inline bool Match(type2type<const char*>, lua_State *L, int idx);
...
可以看出,type2type的存在只是为了在调用Match时决议到正确的函数上,由于它没有任何成员,所以不存在运行时的成本。
同样,我们为cpp内建类型提供Get()函数:
inline bool Get(type2type<bool>, lua_State *L, int idx) { return lua_toboolean(L, idx); }
inline int Get(type2type<int>, lua_State *L, int idx) { return static_cast<int>(lua_tonumber(L, idx)); }
...
我想你可能注意到了,在int Get(type2type<int>)中有一个转型的动作,由于lua的类型系统与cpp的类型不同,所以转型动作必须的。
除此之外,在Get重载函数(s)中还有一个小小的细节,每个Get的函数的返回值是不相同的,因为重载机制是依靠参数的不同来识别的,而不是返回值。
前面说的都是一些基础的封装,下来我们将介绍如何向lua注册一个多参数的c函数。还记得吗?利用lua的api只能注册int (*ua_CFunction)(lua_State *)型的c函数,别忘记了,lua是用c写的。
在你的游戏中应用Lua(3):using lua in cpp(注册不同类型的c函数)之一
前面说到,我们可以利用lua提供的api,向脚本提供我们自己的函数,在lua中,只有lua_CFunction类型的函数才能直接向lua注册,lua_CFunction实际上是一个函数指针: typedef int (*lua_CFunction)(lua_State *L);
而在实际的应用中,我们可能需要向lua注册各种参数和返回值类型的函数,例如,提供一个add脚本函数,返回两个值的和:
int add(int x, int y);
为了实现这个目的,首先,我们定义个lua_CFunction类型的函数:
int add_proxy(lua_State *L) { //取得参数 if (!Match(TypeWrapper<int>(), L, -1)) return 0; if (!Match(TypeWrapper<int>(), L, -2)) return 0;
int x = Get(TypeWrapper<int>(), L, -1); int y = Get(TypeWrapper<int>(), L, -1); //调用真正的函数 int result = add(x, y); //返回结果 Push(result); return 1; }
现在,我们可以向lua注册这个函数:
lua_pushstring(L, “add”); lua_pushcclosure(L, add_proxy, 0); lua_settable(L, LUA_GLOBALINDEX);
在脚本中可以这样调用这个函数:
print(add(100, 200))
从上面的步骤可以看出,如果需要向lua注册一个非lua_CFunction类型的函数,需要: 1. 为该函数实现一个封装调用。 2. 在封装调用函数中从lua栈中取得提供的参数。 3. 使用参数调用该函数。 4. 向lua传递其结果。
注意,我们目前只是针对全局c函数,类的成员函数暂时不涉及,在cpp中,类的静态成员函数与c函数类似。
假设我们有多个非lua_CFunction类型的函数向lua注册,我们需要为每一个函数重复上面的步骤,产生一个封装调用,可以看出,这些步骤大多是机械的,因此,我们需要一种方式自动的实现上面的步骤。
首先看步骤1,在cpp中,产生这样一个封装调用的函数的最佳的方式是使用template,我们需要提供一个lua_CFunction类型的模板函数,在这个函数中调用真正的向脚本注册的函数,类似于这样: template <typename Func> inline int register_proxy(lua_State *L)
现在的问题在于:我们要在这个函数中调用真正的函数,那么我们必须要在这个函数中取得一个函数指针,然而,lua_CFunction类型的函数又不允许你在增加别的参数来提供这个函数指针,现在该怎么让regisger_proxy函数知道我们真正要注册的函数呢?
在oop中,似乎可以使用类来解决这个问题:
template <Func> struct register_helper { explicit register_helper(Func fn) : m_func(fn) {} int register_proxy(lua_State *L);
protected: Func m_func; };
可是不要忘记,lua_CFunction类型指向的是一个c函数,而不是一个成员函数,他们的调用方式是不一样的,如果将上面的int register_proxy()设置为静态成员函数也不行,因为我们需要访问类的成员变量m_func;
让我们再观察一下lua_CFunction类型的函数:
int register_proxy(lua_State *L);
我们看到,这里面有一个lua_State*型的指针,我们能不能将真正的函数指针放到这里面存储,到真正调用的时候,再从里面取出来呢?
Lua提供了一个api可以存储用户数据: Lua_newuserdata(L, size)
在适当的时刻,我们可以再取出这个数据:
lua_touserdata(L, idx)
ok,现在传递函数指针的问题我们已经解决了,后面再看第二步:取得参数。
在你的游戏中应用Lua(3):using lua in cpp(注册不同类型的c函数)之二
在解决了传递函数指针的问题之后,让我们来看看调用函数时会有一些什么样的问题。
首
先,当我们通过函数指针调用这个函数的时候,由于我们面对的是未知类型的函数,也就是说,我们并不知道参数的个数,参数的类型,还有返回值的类型,所以我
们不能直接从lua栈中取得参数,当然,我们可以通过运行时测试栈中的信息来得到lua传递进来的参数的个数和类型,这意味着我们在稍后通过函数指针调用
函数时也需要动态的根据参数的个数和类型来决议到正确的函数,这样,除了运行时的成本,cpp提供给我们的强类型检查机制的好处也剩不了多少了,我们需要
的是一种静态的编译时的“多态”。
在cpp中,至少有两种方法可以实现这点。最直接简单的是使用函数重载,还有一种是利用模板特化机制。
简单的介绍一下模板特化:
在cpp中,可以针对一个模板函数或者模板类写出一些特化版本,编译器在匹配模板参数时会寻找最合适的一个版本。类似于这样:
templat <typename T> T foo() { T tmp(); return tmp; }
//提供特化版本 template <> int foo() { return 100; }
在main()函数中,我们可以显示指定使用哪个版本的foo:
int main(int argc, char **argv) { cout << foo<int>() << endl; return 0; }
程序将输出100,而不是0,以上代码在 g++中编译通过,由于vc6对于模板的支持不是很好,所以有一些模板的技术在vc6中可能不能编译通过。
所以最好使用重载来解决这个问题,在封装函数调用中,我们首先取得这个函数指针,然后,我们要提供一个Call函数来真正调用这个函数,类似于这样: //伪代码 int Call(pfn, lua_State *L, int idx)
可是我们并不知道这个函数指针的类型,现在该怎么写呢?别忘记了,我们的register_proxy()是一个模板函数,它有一个参数表示了这个指针的类型:
template <typename Func> int register_proxy(lua_State *L) { //伪代码,通过L参数取得这个指针 unsigned char *buffer = get_pointer(L);
//对这个指针做强制类型转化,调用Call函数 return Call(*(Func*)buffer, L, 1); }
由重载函数Call调用真正的函数,这样,我们可以使用lua api注册相关的函数,下来我们提供一个注册的函数:
template <typename Func> void lua_pushdirectclosure(Func fn, lua_State *L, int nUpvalue) { //伪代码,向L存储函数指针 save_pointer(L);
//向lua提供我们的register_proxy函数 lua_pushcclosure(L, register_proxy<Func>, nUpvalue + 1); }
再定义相关的注册宏: #define lua_register_directclosure(L, func) \ lua_pushstring(L, #func); lua_pushdirectclosure(func, L, 1); lua_settable(L, LUA_GLOBALINDEX)
现在,假设我们有一个int add(int x, int y)这样的函数,我们可以直接向lua注册:
lua_register_directclosure(L, add);
看,最后使用起来很方便吧,我们再也不用手写那么多的封装调用的代码啦,不过问题还没有完,后面我们还得解决Call函数的问题。
在你的游戏中应用Lua(3):using lua in cpp(注册不同类型的c函数)之三
下面,让我们集中精力来解决Call重载函数的问题吧。
前面已经说过来,Call重载函数接受一个函数指针,然后从lua栈中根据函数指针的类型,取得相关的参数,并调用这个函数,然后将返回值压入lua栈,类似于这样:
//伪代码 int Call(pfn, lua_State *L, int idx)
现在的问题是pfn该如何声明?我们知道这是一个函数指针,然而其参数,以及返回值都是未知的类型,如果我们知道返回值和参数的类型,我们可以用一个typedef来声明它:
typedef void (*pfn)();
int Call(pfn fn, lua_State *L, int idx);
我们知道的返回值以及参数的类型只是一个模板参数T,在cpp中,我们不能这样写:
template <typename T> typedef T (*Func) ();
一种解决办法是使用类模板:
template <typename T> struct CallHelper { typedef T (*Func) (); };
然后在Call中引用它:
template <typename T> int Call(typename CallHelper::Func fn, lua_State *L, int idx)
注意typename关键字,如果没有这个关键字,在g++中会产生一个编译警告,它的意思是告诉编译器,CallHelper::Func是一个类型,而不是变量。
如果我们这样来解决,就需要在CallHelper中为每种情况大量定义各种类型的函数指针,还有一种方法,写法比较古怪,考虑一个函数中参数的声明:
void (int n);
首先是类型,然后是变量,而应用于函数指针上:
typedef void (*pfn) (); void (pfn fn);
事实上,可以将typedef直接在参数表中写出来:
void (void (*pfn)() );
这样,我们的Call函数可以直接这样写:
//针对没有参数的Call函数 template <typename RT> int Call(RT (*Func) () , lua_State *L, int idx); { //调用Func RT ret = (*Func)();
//将返回值交给lua Push(L, ret);
//告诉lua有多少个返回值 return 1; }
//针对有一个参数的Call template <typename T, typename P1> int Call(RT (*Func)(), lua_State *L, int idx) { //从lua中取得参数 if (!Match(TypeWrapper<P1>(), L, -1) return 0;
RT ret = (*Func) (Get(TypeWrapper<P1>(), L, -1));
Push(L, ret); return 1; }
按照上面的写法,我们可以提供任意参数个数的Call函数,现在回到最初的时候,我们的函数指针要通过lua_State *L来存储,这只要利用lua提供的api就可以了,还记得我们的lua_pushdirectclosure函数吗:
template <typename Func> void lua_pushdirectclosure(Func fn, lua_State *L, int nUpvalue) { //伪代码,向L存储函数指针 save_pointer(L);
//向lua提供我们的register_proxy函数 lua_pushcclosure(L, register_proxy<Func>, nUpvalue + 1); }
其中,save_pointer(L)可以这样实现:
void save_pointer(lua_State *L) { unsigned char* buffer = (unsigned char*)lua_newuserdata(L, sizeof(func)); memcpy(buffer, &func, sizeof(func)); }
而在register_proxy函数中:
template <typename Func> int register_proxy(lua_State *L) { //伪代码,通过L参数取得这个指针 unsigned char *buffer = get_pointer(L); //对这个指针做强制类型转化,调用Call函数 return Call(*(Func*)buffer, L, 1); } get_pointer函数可以这样实现:
unsigned char* get_pointer(lua_State *L) { return (unsigned char*) lua_touserdata(L, lua_upvalueindex(1)); }
这一点能够有效运作主要依赖于这样一个事实:
我们在lua栈中保存这个指针之后,在没有对栈做任何操作的情况下,又把它从栈中取了出来,所以不会弄乱lua栈中的信息,记住,lua栈中的数据是由用户保证来清空的。
到现在,我们已经可以向lua注册任意个参数的c函数了,只需简单的一行代码:
lua_register_directclosure(L, func)就可以啦。
在你的游戏中应用Lua(3):Using Lua in cpp(基本数据类型、指针和引用)之一
Using Lua in cpp(基本数据类型、指针和引用)
前面介绍的都是针对cpp中的内建基本数据类型,然而,即使是这样,在面对指针和引用的时候,情况也会变得复杂起来。
使用前面我们已经完成的宏lua_register_directclosure只能注册by value形式的参数的函数,当参数中存在指针和引用的时候(再强调一次,目前只针对基本数据类型):
1、 如果是一个指针,通常实现函数的意图是以这个指针传递出一个结果来。 2、 如果是一个引用,同上。 3、 如果是一个const指针,通常只有面对char*的时候才使用const,实现函数的意图是,不会改变这个参数的内容。其它情况一般都避免出现使用const指针。 4、 如果是一个const引用,对于基本数据类型来说,一般都避免出现这种情况。
Lua和cpp都允许函数用某种方式返回多个值,对于cpp来说,多个返回值是通过上述的第1和第2种情况返回的,对于lua来说,多个返回值可以直接返回:
--in Lua function swap(x, y) tmp = x x = y y = tmp
return x, y end
x = 100 y = 200
x, y = swap(x, y)
print(x..y)
程序输出:200100
同样的,在主机程序中,我们也可以向Lua返回多个值:
int swap(lua_State *L) { //取得两个参数 int x = Get(TypeWrapper<int>(), L, -1); int y = Get(TypeWrapper<int>(), L, -2);
//交换值 int tmp = x; x = y; y = tmp;
//向Lua返回值 Push(L, x); Push(L, y);
//告诉Lua我们返回了多少个值 return 2; }
现在我们可以在Lua中这样调用这个函数:
x = 100 y = 200
x, y = swap(x, y)
在我们的register_proxy函数中只能对基本数据类型的by value方式有效,根据我们上面的分析,如果我们能够在编译期知道,对于一个模板参数T: 1、 这是一个基本的数据类型,还是一个用户自定义的数据类型? 2、 这是一个普通的指针,还是一个iterator? 3、 这是一个引用吗? 4、 这是一个const 普通指针吗? 5、 这是一个const 引用吗?
如果我们能知道这些,那么,根据我们上面的分析,我们希望:(只针对基本数据类型) 1、 如果这是一个指针,我们希望把指针所指的内容返回给Lua。 2、 如果这是一个引用,我们希望把引用的指返回给Lua。 3、 如果这是const指针,我们希望将从Lua栈中取得的参数传递 给调用函数。 4、 如果这是一个const引用,我们也希望把从Lua栈中取得的参 数传递给调用函数。
|
Lua脚本语言入门
作者: 沐枫
Lua 程序设计初步
作者: 沐枫 (第二人生成员) 版权所有转载请注明原出处
在这篇文章中,我想向大家介绍如何进行Lua程序设计。我假设大家都学过至少一门编程语言,比如Basic或C,特别是C。因为Lua的最大用途是在宿主程序中作为脚本使用的。 Lua 的语法比较简单,学习起来也比较省力,但功能却并不弱。 在Lua中,一切都是变量,除了关键字。请记住这句话。
I. 首先是注释 写一个程序,总是少不了注释的。 在Lua中,你可以使用单行注释和多行注释。 单行注释中,连续两个减号"--"表示注释的开始,一直延续到行末为止。相当于C++语言中的"//"。 多行注释中,由"--[["表示注释开始,并且一直延续到"]]"为止。这种注释相当于C语言中的"/*…*/"。在注释当中,"[["和"]]"是可以嵌套的。 II. Lua编程 经典的"Hello world"的程序总是被用来开始介绍一种语言。在Lua中,写一个这样的程序很简单: print("Hello world") 在Lua中,语句之间可以用分号";"隔开,也可以用空白隔开。一般来说,如果多个语句写在同一行的话,建议总是用分号隔开。 Lua 有好几种程序控制语句,如:
条件控制:if 条件 then … elseif 条件 then … else … end While循环:while 条件 do … end Repeat循环:repeat … until 条件 For循环:for 变量 = 初值,终点值,步进 do … end For循环:for 变量1,变量2,… ,变量N in表或枚举函数 do … end
注意一下,for的循环变量总是只作用于for的局部变量,你也可以省略步进值,这时候,for循环会使用1作为步进值。 你可以用break来中止一个循环。 如果你有程序设计的基础,比如你学过Basic,C之类的,你会觉得Lua也不难。但Lua有几个地方是明显不同于这些程序设计语言的,所以请特别注意。
.语句块 语句块在C++中是用"{"和"}"括起来的,在Lua中,它是用do 和 end 括起来的。比如: do print("Hello") end 你可以在 函数 中和 语句块 中定局部变量。
.赋值语句 赋值语句在Lua被强化了。它可以同时给多个变量赋值。 例如: a,b,c,d=1,2,3,4 甚至是: a,b=b,a -- 多么方便的交换变量功能啊。 在默认情况下,变量总是认为是全局的。假如你要定义局部变量,则在第一次赋值的时候,需要用local说明。比如: local a,b,c = 1,2,3 -- a,b,c都是局部变量
.数值运算 和C语言一样,支持 +, -, *, /。但Lua还多了一个"^"。这表示指数乘方运算。比如2^3 结果为8, 2^4结果为16。 连接两个字符串,可以用".."运处符。如: "This a " .. "string." -- 等于 "this a string"
.比较运算 < > <= >= == ~= 分别表示 小于,大于,不大于,不小于,相等,不相等 所有这些操作符总是返回true或false。 对于Table,Function和Userdata类型的数据,只有 == 和 ~=可以用。相等表示两个变量引用的是同一个数据。比如: a={1,2} b=a print(a==b, a~=b) -- true, false a={1,2} b={1,2} print(a==b, a~=b) -- false, true
.逻辑运算 and, or, not 其中,and 和 or 与C语言区别特别大。 在这里,请先记住,在Lua中,只有false和nil才计算为false,其它任何数据都计算为true,0也是true! and 和 or的运算结果不是true和false,而是和它的两个操作数相关。 a and b:如果a为false,则返回a;否则返回b a or b:如果 a 为true,则返回a;否则返回b
举几个例子: print(4 and 5) --> 5 print(nil and 13) --> nil print(false and 13) --> false print(4 or 5) --> 4 print(false or 5) --> 5
在Lua中这是很有用的特性,也是比较令人混洧的特性。 我们可以模拟C语言中的语句:x = a? b : c,在Lua中,可以写成:x = a and b or c。 最有用的语句是: x = x or v,它相当于:if not x then x = v end 。
.运算符优先级,从高到低顺序如下: ^ not - (一元运算) * / + - ..(字符串连接) < > <= >= ~= == and or
III. 关键字 关键字是不能做为变量的。Lua的关键字不多,就以下几个: and break do else elseif end false for function if in local nil not or repeat return then true until while
IV. 变量类型 怎么确定一个变量是什么类型的呢?大家可以用type()函数来检查。Lua支持的类型有以下几种:
Nil 空值,所有没有使用过的变量,都是nil。nil既是值,又是类型。 Boolean 布尔值 Number 数值,在Lua里,数值相当于C语言的double String 字符串,如果你愿意的话,字符串是可以包含'\0'字符的 Table 关系表类型,这个类型功能比较强大,我们在后面慢慢说。 Function 函数类型,不要怀疑,函数也是一种类型,也就是说,所有的函数,它本身就是一个变量。 Userdata 嗯,这个类型专门用来和Lua的宿主打交道的。宿主通常是用C和C++来编写的,在这种情况下,Userdata可以是宿主的任意数据类型,常用的有Struct和指针。 Thread 线程类型,在Lua中没有真正的线程。Lua中可以将一个函数分成几部份运行。如果感兴趣的话,可以去看看Lua的文档。
V. 变量的定义 所有的语言,都要用到变量。在Lua中,不管你在什么地方使用变量,都不需要声明,并且所有的这些变量总是全局变量,除非,你在前面加上"local"。 这一点要特别注意,因为你可能想在函数里使用局部变量,却忘了用local来说明。 至于变量名字,它是大小写相关的。也就是说,A和a是两个不同的变量。 定义一个变量的方法就是赋值。"="操作就是用来赋值的 我们一起来定义几种常用类型的变量吧。 A. Nil 正如前面所说的,没有使用过的变量的值,都是Nil。有时候我们也需要将一个变量清除,这时候,我们可以直接给变量赋以nil值。如: var1=nil -- 请注意 nil 一定要小写
B. Boolean
布尔值通常是用在进行条件判断的时候。布尔值有两种:true 和
false。在Lua中,只有false和nil才被计算为false,而所有任何其它类型的值,都是true。比如0,空串等等,都是true。不要被
C语言的习惯所误导,0在Lua中的的确确是true。你也可以直接给一个变量赋以Boolean类型的值,如: varboolean = true
C. Number 在Lua中,是没有整数类型的,也不需要。一般情况下,只要数值不是很大(比如不超过100,000,000,000,000),是不会产生舍入误差的。在很多CPU上,实数的运算并不比整数慢。 实数的表示方法,同C语言类似,如: 4 0.4 4.57e-3 0.3e12 5e+20
D. String 字符串,总是一种非常常用的高级类型。在Lua中,你可以非常方便的定义很长很长的字符串。 字符串在Lua中有几种方法来表示,最通用的方法,是用双引号或单引号来括起一个字符串的,如: "This is a string." 和C语言相同的,它支持一些转义字符,列表如下: \a bell \b back space \f form feed \n newline \r carriage return \t horizontal tab \v vertical tab \\ backslash \" double quote \' single quote \[ left square bracket \] right square bracket
由于这种字符串只能写在一行中,因此,不可避免的要用到转义字符。加入了转义字符的串,看起来实在是不敢恭维,比如: "one line\nnext line\n\"in quotes\", 'in quotes'" 一大堆的"\"符号让人看起来很倒胃口。如果你与我有同感,那么,我们在Lua中,可以用另一种表示方法:用"[["和"]]"将多行的字符串括起来,如: page = [[ <HTML> <HEAD> <TITLE>An HTML Page</TITLE> </HEAD> <BODY> <A HREF="http://www.lua.org">Lua</A> [[a text between double brackets]] </BODY> </HTML> ]]
值得注意的是,在这种字符串中,如果含有单独使用的"[["或"]]"就仍然得用"\["或"\]"来避免歧义。当然,这种情况是极少会发生的。
E. Table
关系表类型,这是一个很强大的类型。我们可以把这个类型看作是一个数组。只是C语言的数组,只能用正整数来作索引;在Lua中,你可以用任意类型来
作数组的索引,除了nil。同样,在C语言中,数组的内容只允许一种类型;在Lua中,你也可以用任意类型的值来作数组的内容,除了nil。 Table的定义很简单,它的主要特征是用"{"和"}"来括起一系列数据元素的。比如:
T1 = {} -- 定义一个空表 T1[1]=10 -- 然后我们就可以象C语言一样来使用它了。 T1["John"]={Age=27, Gender="Male"} 这一句相当于: T1["John"]={} -- 必须先定义成一个表,还记得未定义的变量是nil类型吗 T1["John"]["Age"]=27 T1["John"]["Gender"]="Male" 当表的索引是字符串的时候,我们可以简写成: T1.John={} T1.John.Age=27 T1.John.Gender="Male" 或 T1.John{Age=27, Gender="Male"} 这是一个很强的特性。
在定义表的时候,我们可以把所有的数据内容一起写在"{"和"}"之间,这样子是非常方便,而且很好看。比如,前面的T1的定义,我们可以这么写:
T1= { 10, -- 相当于 [1] = 10 [100] = 40, John= -- 如果你原意,你还可以写成:["John"] = { Age=27, -- 如果你原意,你还可以写成:["Age"] =27 Gender=Male -- 如果你原意,你还可以写成:["Gender"] =Male }, 20 -- 相当于 [2] = 20 }
看起来很漂亮,不是吗?我们在写的时候,需要注意三点: 第一,所有元素之间,总是用逗号","隔开; 第二,所有索引值都需要用"["和"]"括起来;如果是字符串,还可以去掉引号和中括号; 第三,如果不写索引,则索引就会被认为是数字,并按顺序自动从1往后编;
表类型的构造是如此的方便,以致于常常被人用来代替配置文件。是的,不用怀疑,它比ini文件要漂亮,并且强大的多。
F. Function 函数,在Lua中,函数的定义也很简单。典型的定义如下: function add(a,b) -- add 是函数名字,a和b是参数名字 return a+b -- return 用来返回函数的运行结果 end
请注意,return语言一定要写在end之前。假如你非要在中间放上一句return,那么请写成:do return end。 还记得前面说过,函数也是变量类型吗?上面的函数定义,其实相当于: add = function (a,b) return a+b end 当你重新给add赋值时,它就不再表示这个函数了。你甚至可以赋给add任意数据,包括nil (这样,你就清除了add变量)。Function是不是很象C语言的函数指针呢?
和C语言一样,Lua的函数可以接受可变参数个数,它同样是用"…"来定义的,比如: function sum (a,b,…) 如果想取得…所代表的参数,可以在函数中访问arg局部变量(表类型)得到。 如 sum(1,2,3,4) 则,在函数中,a = 1, b = 2, arg = {3, 4} 更可贵的是,它可以同时返回多个结果,比如: function s() return 1,2,3,4 end a,b,c,d = s() -- 此时,a = 1, b = 2, c = 3, d = 4 前面说过,表类型可以拥有任意类型的值,包括函数!因此,有一个很强大的特性是,拥有函数的表,哦,我想更恰当的应该说是对象吧。Lua可以使用面向对象编程了。不信?那我举例如下:
t = { Age = 27 add = function(self, n) self.Age = self.Age+n end } print(t.Age) -- 27 t.add(t, 10) print(t.Age) -- 37
不过,t.add(t,10) 这一句实在是有点土对吧?没关系,在Lua中,你可以简写成: t:add(10) -- 相当于 t.add(t,10)
G. Userdata 和 Thread 这两个类型的话题,超出了本文的内容,就不打算细说了。
VI. 结束语 就这么结束了吗?当然不是,接下来,需要用Lua解释器,来帮助你理解和实践了。这篇小文只是帮助你大体了解Lua的语法。如果你有编程基础,相信会很快对Lua上手了。 就象C语言一样,Lua提供了相当多的标准函数来增强语言的功能。使用这些标准函数,你可以很方便的操作各种数据类型,并处理输入输出。有关这方面的信息,你可以参考《Programming in Lua 》一书,你可以在网络上直接观看电子版,网址为:http://www.lua.org/pil/index.html 当然,Lua的最强大的功能是能与宿主程序亲蜜无间的合作,因此,下一篇文章,我会告诉大家,如何在你的程序中使用Lua语言作为脚本,使你的程序和Lua脚本进行交互。 --------------------------------------------------------------------------------------------------------- 使用流程 1. 函数的使用 以下程序演示了如何在Lua中使用函数, 及局部变量 例e02.lua -- functions function pythagorean(a, b) local c2 = a^2 + b^2 return sqrt(c2) end print(pythagorean(3,4))
运行结果 5
程序说明 在Lua中函数的定义格式为: function 函数名(参数) ... end 与Pascal语言不同, end不需要与begin配对, 只需要在函数结束后打个end就可以了. 本例函数的作用是已知直角三角形直角边, 求斜边长度. 参数a,b分别表示直角边长, 在函数内定义了local形变量用于存储斜边的平方. 与C语言相同, 定义在函数内的代 码不会被直接执行, 只有主程序调用时才会被执行. local表示定义一个局部变量, 如果不加local刚表示c2为一个全局变量, local的作用域 是在最里层的end和其配对的关键字之间, 如if ... end, while ... end等。全局变量的 作用域是整个程序。
2. 循环语句 例e03.lua -- Loops for i=1,5 do print("i is now " .. i) end
运行结果 i is now 1 i is now 2 i is now 3 i is now 4 i is now 5
程序说明 这里偶们用到了for语句 for 变量 = 参数1, 参数2, 参数3 do 循环体 end 变量将以参数3为步长, 由参数1变化到参数2 例如: for i=1,f(x) do print(i) end for i=10,1,-1 do print(i) end
这里print("i is now " .. i)中,偶们用到了..,这是用来连接两个字符串的, 偶在(1)的试试看中提到的,不知道你们答对了没有。 虽然这里i是一个整型量,Lua在处理的时候会自动转成字符串型,不需偶们费心。
3. 条件分支语句 例e04.lua -- Loops and conditionals for i=1,5 do print(“i is now “ .. i) if i < 2 then print(“small”) elseif i < 4 then print(“medium”) else print(“big”) end end
运行结果 i is now 1 small i is now 2 medium i is now 3 medium i is now 4 big i is now 5 big
程序说明 if else用法比较简单, 类似于C语言, 不过此处需要注意的是整个if只需要一个end, 哪怕用了多个elseif, 也是一个end. 例如 if op == "+" then r = a + b elseif op == "-" then r = a - b elseif op == "*" then r = a*b elseif op == "/" then r = a/b else error("invalid operation") end
4.试试看 Lua中除了for循环以外, 还支持多种循环, 请用while...do和repeat...until改写本文中的for程序 ---------------------------------------------------------------------------------------------------------- 数组的使用
1.简介 Lua语言只有一种基本数据结构, 那就是table, 所有其他数据结构如数组啦, 类啦, 都可以由table实现.
2.table的下标 例e05.lua -- Arrays myData = {} myData[0] = “foo” myData[1] = 42
-- Hash tables myData[“bar”] = “baz”
-- Iterate through the -- structure for key, value in myData do print(key .. “=“ .. value) end
输出结果 0=foo 1=42 bar=baz
程序说明 首先定义了一个table myData={}, 然后用数字作为下标赋了两个值给它. 这种 定义方法类似于C中的数组, 但与数组不同的是, 每个数组元素不需要为相同类型, 就像本例中一个为整型, 一个为字符串.
程序第二部分, 以字符串做为下标, 又向table内增加了一个元素. 这种table非常 像STL里面的map. table下标可以为Lua所支持的任意基本类型, 除了nil值以外.
Lua对Table占用内存的处理是自动的, 如下面这段代码 a = {} a["x"] = 10 b = a -- `b' refers to the same table as `a' print(b["x"]) --> 10 b["x"] = 20 print(a["x"]) --> 20 a = nil -- now only `b' still refers to the table b = nil -- now there are no references left to the table b和a都指向相同的table, 只占用一块内存, 当执行到a = nil时, b仍然指向table, 而当执行到b=nil时, 因为没有指向table的变量了, 所以Lua会自动释放table所占内存
3.Table的嵌套 Table的使用还可以嵌套,如下例 例e06.lua -- Table ‘constructor’ myPolygon = { color=“blue”, thickness=2, npoints=4; {x=0, y=0}, {x=-10, y=0}, {x=-5, y=4}, {x=0, y=4} }
-- Print the color print(myPolygon[“color”])
-- Print it again using dot -- notation print(myPolygon.color)
-- The points are accessible -- in myPolygon[1] to myPolygon[4]
-- Print the second point’s x -- coordinate print(myPolygon[2].x)
程序说明 首先建立一个table, 与上一例不同的是,在table的constructor里面有{x=0,y=0}, 这是什么意思呢? 这其实就是一个小table, 定义在了大table之内, 小table的 table名省略了. 最后一行myPolygon[2].x,就是大table里面小table的访问方式. ----------------------------------------------------------------------------------------------------------- 如何简化你的宏.
虽然以上介绍让我们了解道宏可以完成非常强大的功能,但暴雪实在太小气了,仅仅只给我们255个字符来编写宏的内容,假如你的宏的功能比较罗嗦,那就很麻烦了,所以以下我介绍一下一些简化宏的小技巧:
1、定义全局变量 看
完之前Lua介绍的人该都知道把,在Lua里,所有的变量都是全局变量,也就是说任何一个变量只要你在开始游戏后做过定义,那么到游戏结束时只要你不重新
定义他都是有效的。但为了不让我们自己不混淆做全局用的变量和局部使用的变量,我们可以采用大小写区分的办法,即大写一律做为全局变量使用,小写都用局部
变量。 这样,我们可以在一个宏里把自己常用的魔法/技能都定义成变量来表示,比如我是个术士,就可以这样: F="腐蚀术(等级 3)" X="献祭(等级 3)"....... 之后,我们要使用这样魔法的时候,只要直接用F或X来代替就可以了,连""都可以省掉,是不是很方便呢~ 或者还可以把一些常见的API函数变量也自己定义: T="target" P="player"..... 使用的时候和上面一样。
2、自定义函数 说实在话,魔兽的有些函数实在长的过头,很多时候珍贵的字节都给函数占去了。所以必要的时候我们就得用自定义函数的方法去简化这些函数。 自定义函数的语句为: function 函数名称(函数变量1、函数变量2....) return 函数返回值 end 比如,使用法术的这个函数是CastByName(),我们可以在宏里这样写: /scirpt function C(a) CastByName(a) end 运行后,我们其他宏使用法术就只要直接用C()就可以了,是不是很方便呢? 或是说话的函数: /script function S(a) SendChatMessage(a,"SAY") end 之后你要控制人物说话就用S()就可以了。
如果是有返回值的函数: /script function N(a) return UNitName(a) --return之后就是表示函数的返回值,但return必须在end前面. end 如果以后你要调用目标的名字,直接用 x=N("target"),如果按前面第一点定义了全局变量的话,更简单x=N(T)。
这样,我们就可以把重要的字节都用在宏的判断内容上,而不是沉长的函数上了。如果你还有什么更好的简化方法,可以跟贴哦。 ------------------------------------------------------------------------------------------------------- 关于背包物品使用整理类的宏的制作
由于游戏提供的函数无法直接由物品名称调用该物品,所以通常简单的使用物品宏是比较麻烦的,一定要把使用的物品放在背包内特定的位置
;或则大多术士都需要的问题,能随时监视自己的灵魂碎片(当然,有插件可以做到这一点)。
以下我写写关于如何制作这类宏:
首先,我们要在背包里找到自己需要的东西,必须用循环里遍历这些包。由于放的位置有2个参数,1个是包的编号,一个是包内槽位的编号,
所以我们需要一个循环嵌套来搜索:
以下假设我们身上都是16格的包: for bag=0,4,1 do --包的编号为从右到左,0,1,2,3,4 for cw=1,16,1 do --槽位的编号为上到下,左到右 1,2,3,4,5......16 .............. --这里我们可以写如判断物品是否为我们需要的东西的语句 end --表示内循环结束 end --外循环结束
或者用其他方式做这个循环: While循环:while 条件 do … end
Repeat循环:repeat … until 条件
然后,要处理的是物品的判断: 我们有两个函数可以使用 GetContainerItemLink() 和 GetContainerItemInfo() 这两个函数使用的变量都是2个,一个是包的编号,一个是槽位的编号,但他们的返回值不同
GetContainerItemLink()是返回一个带着物品名字的连接,如果你用聊天函数把返回值说出来就可以看到,说出来的不光是物品的名称,还是
一个可以连接到物品详细内容窗口的连接。
比如,你的包里4,1的位置放了一块熊肉,那么用/script SendChatMessage(GetContainerItemLink(4,1),"SAY")后,就可以看到自己说“[熊
肉]”,而且用鼠标点一下说的内容,还可以弹出一个描写这块肉的窗口。
但要注意,直接用"[熊肉]"这样字符串来判断这个物品是不行的,例如:
if GetContainerItemLink(4,1)=="[熊肉]" then ..... end 这个判断是无效的。
正确的方法是,先把要做判断的物品的赋一个变量,再用变量做出判断:
rou=GetContainerItemLink(4,1) --把物品连接值赋给rou
if GetContainerItemLink(4,1)==rou then ..... end --现在就可以正常判断物品了
最后要注意的是,这个函数无法对术士的灵魂碎片做出正确的判断,意思就是,虽然灵魂碎片用这个函数显示出来是一样的,但这个函数却认
为所有的灵魂碎片都是不同的东西,即你把这个灵魂碎片的连接赋给一个变量后,这个变量就只能判断这个灵魂碎片,其他的灵魂碎片就无法
作出判断,奇怪把。所以要判断灵魂碎片,就必须用到第二个函数GetContainerItemInfo()
GetContainerItemInfo()的返回值非常多,几乎所有的物品信息都可以返回,但我们这里判断只用它返回的第一个值。 我们可以先用聊天函数来看看第一个返回值是什么样子的: /script a=GetContainerItemInfo(4,1) SendChatMessage(a,"SAY")
可以看到,返回值相当长的英文,但物品的关键字是在后面。
这样,我们就有2种方法来使用这个函数来判断物品。
1、和前一个函数的方法一样,用变量存储值后再判断,前提是要把判断的物品放在特定的位置赋一下值。 2、只使用特定物品,把物品的判断关键字写在函数里,然后用string.find()来判断他。 例子:某物品的关键字是bd if string.find(GetContainerItemInfo(4,1),bd) then .....end --判断包1,4位置是否存在关键字为bd物品。
接着要处理的是物品的使用和交换。 使用特定背包位置的物品函数:UseContainerItem(index,slot) 这个好理解,不用多解释了把。
拾取/放下物品的函数:PickupContainerItem(index,slot) 这个函数有意思,你鼠标上没抓着东西的时候就是帮你拿起特定位置的物品,有的话就变成放下物品到特定的位置并交换拿起该位置的物品。
所以要完成2个物品在包内的交换要使用3次这个函数: PickupContainerItem(4,1) --拿起4,1位置的物品 PickupContainerItem(1,4) --放在1,4位置并拿起1,4位置的物品 PickupContainerItem(4,1) --把1,4位置的物品放在4,1位置
好拉,把以上几点组合后宏就基本完成了:
下面的例子是关于灵魂碎片的整理,把前4个包的灵魂碎片全放到最后一个包内:
/script bag=0 cw=1 sc=1 --定义好变量,bag是包的编号,cw表示查找包的槽位,sc指向最后一个包内的槽位 for bag=0,3,1 do --从0号包开始,到3号包结束,最后一个包不搜索。 for cw=1,16,1 do --这里假设所有的包都是16个槽位的,如果没那么多槽位的包也可以用。 if GetContainerItemLink(bag,cw)~=nil --判断这个槽位是否是空的,是空就直接跳到下一个槽位 then if string.find(GetContainerItemInfo(bag,cw),"Gem") --判断这个槽位里是否是灵魂碎片,Gem为灵魂碎片的关键字 then while string.find(GetContainerItemInfo(4,sc),"Gem") do sc=sc+1 end --这是一个小循环,用于判断最后一个包里原来是否已经有灵魂碎片,有的话就指向包的下一个槽位 PickupContainerItem(bag,cw) PickupContainerItem(4,sc) PickupContainerItem(bag,cw) --这3句控制灵魂碎片和最后一个包内物品的交换 sc=sc+1 --重要,不能忘记这个,每放置好一个碎片后就要把最后一个包的 槽位指针指向下一个槽位,上面的小循环是无法判断刚刚放好的碎片的。 end end end end -循环结束
完了么,当然不行。。。因为宏的限制是255个字。所以要简化我们的宏。
最长的内容估计就是函数了,就先从简化函数开始:
建立以下宏:
/script function P(c,d) PickupContainerItem(c,d) end /script
function I(e,f) if GetContainerItemInfo(e,f) then return
string.find(GetContainerItemInfo(e,f),"Gem") else return nil end end
原来的宏就变成了:
/script bag=0 cw=1 sc=1 for bag=0,3,1 do for cw=1,16,1 do if G(bag,cw)~=nil then if I(bag,cw) then while I(4,sc) do sc=sc+1 end P(bag,cw) P(4,sc) P(bag,cw) sc=sc+1 end end end end
多余的变量定义和过长的变量都可以更改:
/script s=1 for g=0,3 do for w=1,16 do if G(g,w) then if I(g,w) then while I(4,s) do s=s+1 end P(g,w) P(4,s) P(g,w) s=s+1 end end end end
现在写的下了吧。呵呵,至于使用物品的宏我虽然已经写好了,但没有测试过,等测试没问题后再放出来把。有兴趣的朋友也可以自己写写。
但要注意一点,使用物品的宏只要找到物品就可以马上跳出循环,所以用Repeat循环做比较合适。
|
这样软件开发人才级别的划分你同意吗(ZT)
本人做软件多年,一直与软件开发行业的各种级别的软件开发人才打交道,很多时候,
还扮演面视考官的角色(很遗憾,本人还没有被面试过)。 写下这篇文章,目的是区分各种层次的软件开发人员,也让软件开发人员能够对照自己,看看自己在什么层次。 软件开发工作,其实是一种很复杂的工作,需要多方面的技能。我认为,尤其以学习能力和创新能力为主。所以,我以下对软件人才的层次划分,也围绕这两个能力展开。 一、门外汉型:几乎没有学习能力,更没有创新能力。比如,买了一本《一步一步跟我学VB编程》之类的书,对照书上写的,把例子程序给做出来了,
还把例子程序的某些窗口标题给修改了一下。然后,就自认为自己可以做软件开发工作了。到处递简历,应聘的职位为软件开发工程师。这类人,以刚毕业的计算机
专业的大学生为多(当然,刚毕业的学生中也有非常高级的人才)。读书期间,就以玩游戏为主,考试的时候,就搞点舞弊过关。 二、入门型:该类型的人员(不叫人才,所以叫人员),可能入门某一种到两种开发语言,10年前,我上大学的时候,这类人的典型特点是热衷于
DOS命令的n种用法。比如,dir命令的各种参数。学习过basic语言,知道C语言中printf函数的各种参数的用法,到了2005年,这类人是热
衷于windows下的注册表,热种于在自己的机器上安装各种开发工具(VB,VC,dephi,asp等)。但是,仅仅停留在编译开发工具中自带的几个
例子程序中。(可能还会做点修改)。经过一段时间的学习,可能还自己能够编写个简单的windows应用程序,修改注册表的程序等等。其很多时间还是在玩
游戏,上QQ聊天泡MM,看了一篇如何修改某病毒的文章,一定会对照文章上的说明,把病毒给修改了,然后到处发,以显示自己的能力。当然,很多时候,该类
人即使对照文章的说明,也不能将病毒修改。那就找那些带配置工具的黑客程序去弄吧,比如。BO等就是他们最常用来炫耀的。中国的破解者与初级黑客,绝大部
分是这一类人。懂的不多,还喜欢炫耀(为炫耀目的的破解和修改病毒就是这一类人的最大特点)。该类人员,一般都没有在软件公司从事软件开发工作。 三、基本型人才:该类型一般是大学毕业,并且从事软件开发工作超过2年的人为多,至少比较熟悉一门语言(以VB,dephi,java,asp
等其中的一种)。也有少数人熟悉C或者C++,但是如果是C或者C++,一般对指针等概念还是似懂非懂的状态。哦,对了,该类人员可能还会在自己的机器上
安装过linux或者sco
unix等。但由于对自己没有信心,大部分人会在半个月之后把linux删除。该类型人才,有一定学习能力。创新能力为零。适合培养成为软件蓝领,如果人
际交往能力还可以的话,可以培养成为一个初级营销人员。该类型的人典型的特点是:你要他做个项目,他首先就会问:用什么语言?(因为用他不熟悉的语言对他
来说,他就没有信心),该类人员,习惯看中文文档,不得以的情况下,才会看英文文档。另外,该类人员,喜欢购买软件开发类的书籍。该类人员,一般在软件公
司从事软件开发工作,待遇在4000元到10000元以下为主。 四、熟练工:该类型一般是毕业5年并一直从事软件开发工作,至少熟悉 VB,asp
,熟悉数据库,知道什么叫存储过程,什么叫触发器。知道软件工程管理的基本概念,如果做面象对象开发,可能还会用到Rose等工具。有过20人以下软件项
目管理的经验。对于linux,至少知道是个开源的项目。由于做过比较大的软件项目,项目中带的小兵一般都不具备unix下的开发经验,所以,项目中难免
会出现需要在unix下运行的代码,所以,就自己动手。用c编写过几段Unix下的小程序。学习能力比较强,该类人员,已经习惯看英文文档,有时候看翻译
的别扭的中文文档会觉得不爽。干脆就找英文文档。该类人员,是否喜欢买书不得而知,如果喜欢买书,一般以非软件开发类书籍为主了。在技术选型方面具备一定
的创新能力,至少,你叫他做一个报表程序,他会考虑用Excel的COM对象来实现。国内软件公司中的项目经理,绝大部分是这一类型的人才。待遇一般在
6000到15000元左右。 五、聪明型:该类人员的工作经历不重要,可以是还没毕业的学生,也可以是工作了10年的老鸟,1周内(甚至一小时)就熟悉了一门语言,并且可以
开始用该语言开发,该类人员,由于学习能力很强,短时间内就熟悉了许多语言,即使从来没用过该语言,也敢于在该语言上进行软件开发,选择什么样的语言,不
在于学没学过,而在于是否适合解决当前问题。对技术充满好奇与激情,举个例子,如果该类人员接触过linux,马上就会被Linux的魅力所吸引。即使与
自己的工作无关,也会一回家就研究linux,可以肯定的是,该类人员的笔记本电脑上,肯定安装有linux
,并且,linux的启动次数和windows的启动次数一样多甚至更多。如果该类人员接触到了人工智能,至少会编写一个推理机程序来用用。另外,该类型
人才的典型特点是学习能力超强。英语不一定很厉害,但是,不害怕看英文资料。该类型人才,许多并不是计算机专业毕业,可以是学数学的,物理的,音乐的等等
都有可能。我就见过一个学英语的学生属于这种类型。该类型的人才,几乎所有的病毒代码是他们写出来的(不算那些修改病毒代码的人)。爱表现,也是他们的特
点。如果该类人员在读书,那么,他们是软件公司青睐的人才,绝对不会出现简历递出三份还没有人要的情况,一旦进入公司,在半年内,其才能一定会得到公司领
导的认可,并作为重点培养对象。为了留住这样的人才,软件公司一般会每听说有别的公司要挖他的消息就会给他涨工资一次。薪水的增长速度往往令同事红眼。 六、技术天才型:该类人才,技术方面一流,如果只从技术方面的学习能力,创新能力来讲,都要超过以上的任何一种类型的人才。上帝造人总是很公平
的,他们在技术方面是天才,往往其他方面几乎白痴,不善与人交往,甚至害怕与人交往。另外,某些东西对他们有致命吸引力,比如,有些人就迷恋自己做一个操
作系统,有些人就迷恋人工智能。该类人员,不写软件则以,一写,肯定是一流的。全球一流。从语言来讲,因为他们几乎不用微软的开发工具做具体的项目,他们
所看的技术资料,全部是英文资料,在网上交流的,全是操英语或者法语的人。即使是中国人,他们也习惯用英语与别人进行技术沟通。该类人才,如果在工作,一
般是在某实验室,或者是在某基金的资助下开展研究,如果在软件公司,一定是主持举世瞩目的软件项目。或者,在自己开的小公司既当CEO又当CTO。由于其
技术的优势明显,即使他不是一个很称职的CEO,也能让这个公司维持下去。 七、数学家型:该类型人才,也许根本就不懂具体某种语言的开发(也可以懂),整天就研究算法。建模。一般不属于计算机专业。他们要把自己的成果
变成现实,往往习惯找聪明型或者天才型人才帮他们实现。该类人员,因为不学计算机,所以,无法描述他们在学习技术方面的能力,但是,创新能力绝对一流。该
类人才,没有在软件公司工作的,当然,如果其成果有一定商业价值,他们会成为某软件公司的顾问。或者干脆在某软件公司的实验室中当个主任什么的。 八、比尔型:因为比尔的影响力巨大,所以,我们把具有一定软件开发能力,又有很强的商业运作能力的人归到这一类。比尔型人才,学习能力,在聪明
型之上,在技术天才型之下。由于起社会知识面非常广泛,所以,知道什么软件能赚钱,怎么样做能赚钱。该类人写软件的目的只有一个,那就是赚钱,而不会太在
乎采用什么样的技术。他们写软件,会极力迎合用户,迎合市场。 对人的划分,有时候是很难的,有的人是跨类型的。但是,缺少创造的人,最多就到达熟练工型,具有超强创造力的人,可以达到技术天才型和数学家
型,如果还有商业头脑,成为比尔型也是可能。最后一句话,如果你连足够的学习能力都没有,那么,就请你离开软件开发行业,另谋出路比较合适。 这篇帖子,我首发在共享软件论坛,我认为,如果你不具备超强的学习能力,基本的创新能力和基本的商业能力,那么,就请你尽早不要做共享软件。
|
|
|
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|
27 | 28 | 29 | 30 | 31 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|
常用链接
留言簿(7)
随笔档案
文章分类
文章档案
相册
收藏夹
c++
搜索
最新评论
阅读排行榜
评论排行榜
|
|