㈠ 请问什么是LPC
第一章 Lpc程序和编程环境
-----------------------------------------------------------
第一节 编程环境
通常我们所见到的Mud大多是LpMud。LpMuds使用Unix的指令和文件结构。如果你对Unix有所了解,那么LpMud中的一些指令和它的文件结构与普通的Unix基本一样。如果你从未使用过Unix,那么它与Dos不同的是在文件的路径用"/",而不是Dos的"\".
一个典型的LpMud的文件是这样的:
/clone/player/player.c
"/clone/player/"是路径,player.c是文件名。
在多数的LpMud中,下面这些的基本的Unix指令是可以使用的:
pwd, cd, ls, rm, mv, cp, mkdir, rmdir, more, tail, cat, ed
如果从未使用过Unix,那么下面这张表也许是有用的。
pwd: 显示当前目录
cd: 改换你当前的工作目录,和Dos的cd一样。
ls: 列出指定目录下的所有文件,如果没有指定任何目录,那就列出当前目录底下的文件。和Dos的dir一样。
rm: 删除一个文件 和Dos的rmdir一样
mv: 从命名一个文件 和Dos的move一样
cp: 复制一个文件 和Dos的一样
mkdir: 创建一个目录
rmdir: 删除一个目录
more: 按页显示一个文件在你的当前屏幕。
cat: 显示整个文件。和Dos的type一样。
tail: 显示一个文件的结尾几行。
ed: 允许你使用Mud自带的编辑器,编辑一个文件。
-----------------------------------------------------------
第二节 Lpc程序
2.1 Lpc程序。
Lpc的程序看起来和一般的C区别不断大,语法基本一样,但是Lpc和一般的语言有着根本的不同,Lpc程序是编写一个一个的"Object"。这有什么区别呢?一般的程序是在执行过程中,通常有一个明显的开始和和结束。程序从一个地方开始,然后顺序执行下去,到了结束的地方就中断了。Lpc的Object不是这样的。
所谓的不同的Mud,实际上是一些不同的Lpc的Object在一个Driver的上的各种不同的表现。也就说,Lpc的Object是运行在一个Driver上的。这些Object组成了LpMud的丰富多彩的世界。Driver几乎不参与创建你所接触到的世界,它所完成的工作只是让那些Lpc的Object活动起来。Lpc的Object可能没有明显的开始和结束的标志,它可能
永远在工作。
和一般的程序一样,Lpc“程序”也是由一个或多个文件组成。一个Lpc的Object是按如下方式被执行的:Driver把和这个Object相关的文件读入内存,然后解释执行。但是要记住的是,读入内存,并不是说,它就开始按顺序执行。
2.2 Driver和Mudlib关系
在有些游戏中,整个游戏包括Driver和游戏世界都用C写好,这样能快一些,但是游戏的扩充性很差,巫师们不可能在游戏进行中添加任何东西。LpMud则相反。Driver理论上应该和玩家所接触的世界几乎没有任何直接的联系。游戏世界应该是自己独立的,而且是“即玩即加”的。这就是为什么LpMud使用Lpc作为编程语言的原因。它允许你创建一个游戏世界,再由Driver在需要时读入解释执行。Lpc甚至比C更简单,更容易明白,但是它可以创建一个可以让许多人在上面游戏的世界。
在你写完一个Lpc的文件时,它存在于主机的硬盘上。在游戏进行中,当需要整个Object时,这份文件将被调入内存,一个特殊的函数被调用来初始化这个Object的一些变量。现在你不用关心什么是变量,什么是函数以及游戏本身怎样来调用这个object,你只要记住Driver从硬盘中读入一个文件,然后放在内存中,如果没有任何
错误的话。
2.3 一个Object被装人内存。
一个Object不会也不必有一个特点的地方让Driver去执行它,通常Drvier会在Object中找一个地方去初始化它。一般都是这个函数叫做create()。
Lpc的Object是一些变量(它的值能变化)和函数(函数通常是用来操纵那些变量的一段程序)的组合。函数操纵变量的方式有:调用其他函数,使用Driver内部定义的函数(efun),基本的Lpc表达式以及流控制。
我们来看个变量的例子: wiz_level。这个变量记录你的巫师等级,如果是0呢,通常是普普通通的玩家了。这个值如果越大就表示你的巫师等级越高。这个也同时控制了你能不能执行一些巫师指令。基本上来说,一个Object就是一些变量“堆”在一起的东西。一个Object改变了,也就是某一个或者一些变量改变了。总的来说,一个Object如果要被内存中的另一个Object调用,Driver会去找这个Object的那堆变量放在哪里,如果这些变量没有值,那么Driver会调用一个特定的函数create来初始化这些变量。
但是create()不是Lpc代码开始执行的地方,只是大多数的Object从这里开始。事实上,create()可以不存在。如果这个Object不需要对变量初始化,那么create()可以不存在。那么这样的Object开始执行的地方就完全的不同于一般的Object,可以从任何地方开始。
那么究竟什么是Lpc的Object?Lpc的Object就是一堆变量的集合,它带有一个或者更多的函数来操纵控制这些变量,函数的排列顺序是无所谓的,随便那个排在前面对这个Object的特性没有影响。
2.3 代码风格
在上面说过函数的顺序对这个Object的特性是毫无影响的。但是一个有着良好代码风格的程序对LpMud是很重要的。因为LpMud通常不会也不可能是一个人完成的,如果程序没有较好的可读性,那么别人理解你的“作品”是很困难的。而且有个良好的程序风格能给人以优雅的感觉,因此希望大家写的Lpc程序能有个好的风格。大家中的有些人可能以后会加入XO team创建自己梦想中的世界,我们要求你采用如下的格式书写程序。
2.3.1 头文件
在一个文件的开头是一段说明。采用如下格式:
/* /u/trill/obj/test.c
* from XO Object Library
* 测试用的Object
* created by trill 19970808
* version @(#) test.c 2.1@(#)
* last modified by trill 19971008
* 测试tell_wizard这个simul_efun
*/
第一行是这个文件的绝对路径,就是全路径。
第二行是它所在的Mudlib
第三行是它的功能的简单的描述,可以超过一行。
第四行是这个文件的作者和创建时间。
第五行是它的版本号,可能做了多次修改,甚至可能会重写,这个数字2.1标志了它大概做过多少次改动。
第六行是最后一次修改的人和时间。
第七行是最后一些修改什么东西。
对于一个Object我们要求必须有这样一段说明,特别是前面的五行必须存在,如果做了改动那么最后两行也要加上。这样一般的一个Object,我们从这段说明就能了解到一些很重要的信息。
下面是include一些文件和继承(inherit)一些Object。
#include <ansi.h>
#include "include/test.h"
inherit NPC;
先系统的文件,后自己定义的一些头文件。特别要求的是必须有个和这个Object同名的".h"文件,比如"test.h"放在这个Object所在的目录的下一级目录"include"底下,就是说在include部分的最后一行是#include "include/test.h"。
在test.h定义所有在test.c用到的函数的原形,以及定义一些宏和常量。
这样做的好处是:
第一不用出现一个函数在引用时没有说明,
第二如果想知道这个Object有什么函数,直接看这个文件就可以了,不必去看那个test.c,可能test.c非常长。
第三如果建立一个help系统,用来查询每个Object存在的函数,那么这样直接去读test.h就可以,否则是一件很麻烦的事。
关于inherit我们在继承部分再说。
2.3.2 变量说明
在变量说明部分,大家最好在每个变量后面加一个简单的说明。
2.3.3 函数
一个Object的函数的顺序和名字对这个Object的表现是毫无影响的。但是为了让这个Object有良好的可读性,我们要求一个Object的函数按如下方式排列和命名:
首先是变量的接口部分,这些函数统一用Set+变量名来改变该变量的值,用Get+变量来返回变量的值。比如
static int level;
void SetLevl(int i)
{
level = i;
}
int GetLevel()
{
return level;
}
其次是一些操纵和控制变量的一些函数。比如
void AddLevel(int i)
{
一个Object的函数的顺序和名字对这个Object的表现是毫无影响的。但是为了让这个Object有良好的可读性,我们要求一个Object的函数按如下方式排列和命名:
首先是变量的接口部分,这些函数统一用Set+变量名来改变该变量的值,用Get+变量来返回变量的值。比如
static int level;
void SetLevl(int i)
{
level = i;
}
int GetLevel()
{
return level;
}
其次是一些操纵和控制变量的一些函数。比如
void AddLevel(int i)
{
level += i;
}
这两类函数要求每个单词的第一字母大写。
再是一些Object所能做的事件(event),比如战斗,结婚等等。比如
void eventQuit()
{
...
}
这些函数要求事件的每个单词的第一字母大写,比如eventFight,eventMarry等等。
再下面的是由Driver调用的一些函数,比如create(), heart_beat,setup()。
最后是一些这个Object自己私有的函数,完成一些特别的功能。这些函数通常让要求每个单词的小写,中间用下划线(_)隔开。
要注意的是每个函数之间用一个空行隔开。
这些是对一个文件的整体要求,如果你有兴趣将来在XO team写程序,最好从现在开始就养成这样的编程习惯。如果你是别的Mud里面的巫师,我想一个Mud里面最好也有一个统一的整齐的风格。
也许你会问,这样要求有必要吗?这样太麻烦,程序写了自己能明白就可以了。这是不对的,LpMud是大家合作的项目,如果你做的程序别人没法看懂,不知道写的东西里面有些什么,能调用什么函数,那么实际上你写的东西是失败的,没人会去用它,它可能永远“死”在硬盘上。而且函数统一的命名法能尽快找到你所需要的函数,同时也能提高整个程序的可读性。
对于代码风格XO还有一些别的要求,我们将在以后的文章中介绍,如果你加入了XO team,代码风格将是第一篇要读的文章。
小结:
关于Lpc程序和编程环境,就介绍到这里。看完这一章,我想大家要记住的是LpMud是采用Lpc做为编程语言,Unix文件结构作为文件组织形式。Lpc是编写Object的一种语言,它的程序没有特殊的开始和结束的标志。如果Object被使用到,那么它被调入内存,如果这个Object有一个叫create()的函数,首先被执行,来初始化一些变量。
Lpc的Object是一堆变量的集合,同时带有一些能操纵改变这些变量的函数。Lpc的代码风格,我想一个Mud最好有一个统一的风格,特别的XO有自己的特别的要求。
题外话:
当了好久的巫师,也用Lpc写了一些东西。我一直在试着理解Lpc,因为以我看如果一个巫师没有真正理解Lpc,他就不可能真正理解LpMud。理解Lpc并不仅仅意味着会使用它,许多巫师能使用它但是并不真正理解它。我希望在这个Lpc的介绍文章,能给大家一个Lpc的整体的印象,真正把握和理解Lpc,能创造自己心中梦想的世界。
-----------------------------------------------------------
-----------------------------------------------------------
第二章 Lpc的数据类型
-----------------------------------------------------------
第一节 序言
Lpc的Object是由零个或更多一些的被一个或一个以上函数操纵控制的变量组成的。在代码中函数排列的顺序是不影响Object的特性,但是影响代码的可读性。当你写的那个Object被第一次调用时,Driver将你写的代码装入内存。当每一个Object被调入内存时,所有的变量是没有值的。create()这个函数被调用来初始化Object值。
create()这个函数在Object装入内存后立即被调用。在你读本文时可能对编程一无所知,你可能不知道什么是函数以及它是怎么调用的;或许你有了一些编程的经验,你可能对一个新创建的Object的函数相互调用过程是怎样开始感到迷惑。在这些困惑得到解决之前,你更有必要了解的是这些函数操纵控制的到底是什么东西。所以你最好先来读读这一章:Lpc的数据类型。可以这么说,几乎90%的错误(包括丢失{}和())是由于错误的使用Lpc的数据类型。我认为真正理解这一章能帮助你更容易的编程。
-----------------------------------------------------------
第二节 让计算机理解你
2.1 计算机语言
众所周知的计算机懂得的语言实际上由“0”和“1”组成的机器码。计算机根本不懂得人类的自然语言,实际上它也不懂得我们使用的高级语言,比如BASIC,C,C++,Pascal等等。这些高级语言能让我们更容易的实现我们的想法。但是这些高级语言最终都要被翻译成“0”和“1”组成的计算机语言。
有两种方法能把高级语言翻译成计算机语言:编译和解释。编译类的在程序写完之后用一个编译器将其翻译成计算机语言。编译在程序执行之前就完成了。解释类的翻译的过程在程序执行时进行。由于解释类的语言程序是边执行边解释,所以一般都要比编译编译执行的慢。
不管是哪种语言,他们最终都要被翻译成0和1。但是变量,那些
你存在内存里面的变量,却不可能只是0和1。
所以你必须有一种你
使用的那种编程语言里面的方法来告诉计算机这些0和1应该被当做
整数还是字符,或者是字符串,或者别的什么东西。这样就必须使
用到数据类型。
2.2 数据类型
一个简单的例子:你现在有了一个变量,你把它叫做‘x’并且
赋予它一个十进制整数值65。在Lpc你可以这样的语句来做这件事:
------
x = 65;
------
接着你可以做象下面这样的事:
-----
write(x + "\n");
y = x + 5;
-----
第一行把65和字母"a"输出到屏幕上
第二行把70这个值赋于变量y
对计算机来说有个问题:它不知道你所说的 x = 65;中的65什么意思.
你认为是65,但是计算机可能认为是:
但是,对计算机来说,字母'A'也是被当做:
所以,当你想让计算机明白 write( x + "\n" );, 它必须有一种方法
知道你想看到的是65而不是'A'.
计算机就是通过数据类型来区分65和'A'. 一种数据类型简单的说就
是在内存的某处, 那里代表了或者说指向某个给定的变量, 这些内存
储存的数据是什么类型的. 每个LPC的变量都必须有它对应的变量类型.
在上面给的例子, 本应在那些代码之前有下面一行:
-----
int x;
----
这一行告诉Driver x应该指向什么类型的值, 它应该被当做数据类型'int'
来使用. 'int' 是一个32位的整数. 到这里, 你应该有数据类型的基本
印象, 以及为什么必须有数据类型. 他们可以让Driver知道计算机存在
内存里面的'0'和'1'到底是什么东西.
2.3 Lpc的数据类型
所有的LpMud的Driver都会有以下的数据类型:
void, int, string, object, mixed, int *, string *,
object *, mixed *
大多数的Driver都会有下面这些重要的数据类型:
float, mapping, float *, mapping *
有一些Driver同时还支持下面这些数据类型:
function, struct, class, char
特别的有MudOS支持的数据类型:(以v22pre8为例)
void, int, string, object, float, mapping, function,
class, mixed, int *, string *, object *, float *,
mapping *, function *, class *, mixed *
2.4 一些简单的数据类型
在Lpc入门里面将介绍以下的数据类型:
void, int, float, string, object, 以及mixed. 对于复杂的数据
类型比如: mapping, array, 以及一些不常用的类型比如: class,
function, 我将在Lpc进阶介绍. 这一节我们主要介绍三种数据类型:
int(整型), float(浮点数)和string(字符串).
一个int(整型)是一个整数, 比如1, 42, -18, 0, -10002938这些
都是整型. 在MudOS中一个整型是一个32位的整数, 有符号的整数.
在实际中int得到广泛的使用, 比如开始介绍变量中的wiz_level,
再比如生物的天赋, 年龄等都通常都是int(整型).
一个float(浮点数)是一个实数, 比如2.034, -102.3453, 0.0,
1132741034.33这些都是一个浮点数. 在MudOS中一个浮点数也是一个
32位的实数, 有符号的实数. float通常不常用.
在Object的数值性质中, 我们通常也就使用int和float, 甚至只用
int, 在变量的初始化中int和float自动被赋为0. 但是一般的Driver
比如MudOS不检查数值越界的情况, 还要注意的是这里的int和float
都是有符号的数, 这两点要注意.
string(字符串)是由一或更多的字符组成, 比如"a", "我是飞鸟!",
"42", "飞鸟15岁.", "I am Trill.", 这些都是字符串. 注意的是,
字符串都是被""括起来, 这样第一能区别象int(整型)42和string(字
符串)"42", 第二可以区别变量名(比如 room)和同名的字符串(比如
"room"). string类型的变量在初始化时, 如果没有显式的赋于一个
字符串比如: 空字符串"", 那将是0, 就是一空的指针.
作为最基本的数据类型int, float和string, 是一些复杂的数据
类型的基础. 对这些数据进行运算和操作的操作符, 将在后面介绍,
不过Lpc的操作符和一般的C/C++的操作符一致. 只是有一点, 就+
Lpc支持string和int或者float直接相加, 比如我们上面提到的:
write(x + "\n");
"\n"是一个字符, x是一个整型的变量, 在Lpc解释执行中, 自动将
x代表的数值转化成一个字符串, 然后把两个字符串接在一起.
当你在编程中使用一个变量, 你必须首先让Driver知道这个变量
代表什么样的数据类型. 这个过程叫做变量声明. 这必须在一个函
数或者一个Object的开始部分进行变量声明. 怎么做呢阿? 就是把
数据类型的名字放在你所要用的变量名前面, 举个例子:
-----
void add_x_and_y()
{
int x;
int y;
x = 2;
y = x + x * x;
}
----
上面就是一个完整的函数. 函数名就是add_x_and_y(). 在这个函数中
一开始声明落两个变量x, y都是int.
下面介绍MudOS支持的数据类型:
int
一个整数(32位).
float
一个浮点数(32位).
string
无限长的字符串.
object
指向一个对象的指针.
mapping
一个关系型数组.
function
一种特殊的指针, 指向(object, 函数名)这样一个组合.
arrays
数组的声明采用使用 '*' 跟在一种基本的类型.
void
这种类型对变量没有任何用处, 只对函数有用. 它表明这个函数
不返回任何值.
mixed
这是一种特殊的数据类型, 可以指向任何的数据类型. 如果一个
变量被声明成mixed, 那么Driver不会对它做任何检查.
class
自定义的数据类型, 类似C的struct而和C++和class不一样.
一上是MudOS支持的数据类型.
小结:
对一个变量, Driver需要知道存在计算机内存中的'0'和'1'到底
指的什么东西, 这样我们引入落数据类型. 我们学习3种简单的数据
类型, 同时了解了MudOS支持的各种数据类型. 对于各种操作符, 不
同数据类型有各自不同的操作符, 比如你让 "飞鸟"/"trill", 那
Driver一定会返回一个错误的. 大多数数的操作符和C/C++的一样,
只是+ 还支持字符串和数字相加.
-----------------------------------------------------------
-----------------------------------------------------------
第三章 Lpc的函数
-----------------------------------------------------------
第一节 序言
在前面的介绍中,大家应该知道了Lpc的Object包含能处理变量的函数。
当函数被执行时,它的工作就是处理操作变量,还有是调用(call)别的函
数。变量在函数中被改变操作。变量必须有个数据类型使得计算机能明白
它指向的内存中"0"和"1"到底是什么东西。一个Object的性质通常由它的
包含的变量确定,但是它的特性的表现却是依赖于它包含的函数。一个
Object如果不含有任何一个函数那是不可想象的。那么:什么是函数。
-----------------------------------------------------------
第二节 函数
2.1 什么是函数?
和数学的函数一样,你给Lpc的函数一个值,它能返回一个值。有些语
言,比如Pascal,会区分过程和函数。Lpc和C/C++一样,没有过程,但是
明白这种区别还是有用的。Pascal叫做过程的东西,Lpc叫做类型是void
的函数。换句话说,过程就是什么都不返回的函数。Pascal叫做函数的,
必须返回一些东西。在Lpc中,最无聊的,最简单的,但也是正确的函数
是这样的:
-----
void eventDoNothing() {}
-----
这个函数不接收任何输入,不执行指令,也不返回任何值。
每一个Lpc函数都由三部分组成:
1) 函数声明
2) 函数定义
3) 函数调用
和变量一样,函数必须先有个声明。这样可以让Driver知道:
1) 这个函数将返回的是哪种数据类型。
2) 需要的输入是什么,多少。通常把输入叫做参数。
一个函数声明通常是这样的:
类型 函数名(参数1, 参数2, ..., 参数N);
下面是一个函数声明的例子,这个函数叫 DrinkWater,有一个string
类型的参数,返回的是一个int。
-----
int eventDrinkWater(string str);
-----
在上面的声明中, str是输入的参数的变量名,也可以没有。就是说可以
象下面这样声明 eventDrinkWater()
-----
int eventDrinkWater(string);
-----
函数定义就是代码,它描述了这个函数对传人的参数究竟做了些什么。
函数调用就是别的函数在任何地方使用执行了这个函数。一个函数在它
写完后永远不会被调用,那这个函数的存在的唯一意义只能是浪费内存和
硬盘。一个函数写出来的目的是为了被调用。
下面是两个函数相互调用的例子,两个函数是 eventPrintValue() 和
add(),
-----
/* 首先是函数声明,这个通常是在一个Object的开始部分。
*/
void eventPrintValue();
int add(int x, int y);
/* 其次是函数 write_vals() 的函数定义。我们假定这个函数将被调用
* 是为了描述这个Object.
*/
void eventPrintValue()
{
int x;
x = add(2, 2); // 我们指定 x 接收调用函数 add() 后返回的值。
write(x + "\n");
}
/* 最后是函数 add() 的函数定义。 */
int add(int x, int y)
{
return (x + y);
}
-----
有一点是指明的,在XO的编程的风格我们要求所有的函数都必须有声
明,这个在我们最开始时候说明过。但是实际上必须有函数声明的函数
是那些被调用在函数定义之前的函数。我们规定必须有函数声明,这个
只是规定,但是它会给编程带来好处。
在这一节我们知道什么是函数,函数是由什么组成。要记住,写一个
函数的根本目的是为用它,调用它。一个函数永远不会被调用,那它就
失去了存在的价值。通常别人使用你写的函数,通常只关心它能对传人
的参数做些什么加工,就是这个函数的功能是什么,返回什么。因此一
个函数有一个好的函数名,能直接描述这个函数的功能是很重要的。我
在第一章中说明了XO规定的对函数的命名机制。采用统一的命名方式有
助于相互合作提高效率。
2.2 Efuns
也许你已经听说过efun这个词了,他们是外部定义的函数,是
externally defined function 的缩写。就是说,他们是由Mud Driver
定义好的。如果参加过Lpc的编程,或者看过Lpc的代码,你可能找到这
样的一些表达式:this_player(), strcmp(), implode(), filter(),
等等,看起来象是一个函数,而你找遍整个Object以及这个Object继承
的所有Object中都没有这些函数,这就表明他们是efun。efun存在价值
是因为他们执行起来要比一般的Object带有的函数速度快的多,为什么
快呢,因为他们是以计算机直接能理解的二进制的形式存在。对于Object
内部定义的函数,我们通常叫他们是lfun(local function)。一个巫师
主要工作也就是编写一些lfun组成的Object。
在上面的例子中的 eventPrintValue() 中调用了两个函数,第一个是
函数 add(), 这个是有你声明和定义的,这个就是lfun。第二次调用,
是调用函数 write() 这个函数通常就是efun。Driver已经替你声明和定
义好了。你所要做只是调用它。
efun被创立是为了
1) 处理一些很常用的,每天都有许多函数会调用的。
2) 处理internet socket的输入输出。
3) 以及一些Lpc很难处理的事,毕竟Lpc是C的很小的子集。
efun是用C写好的,内嵌在Driver里面的。在Mud起来之前,和Driver
一起编译好的,他们执行起来会快的多。但是正和你期望的一样,他们
的调用和你写的函数的调用方法是完全一样的。总的来说,需要关心的
和一般函数一样,它需要传入什么参数,它将会返回什么的东西。
怎样得到一些efun的信息,比如传入参数和返回的类型,通常在一个
Mud里面,你可以在类似这样的 /doc/efun 的目录底下找到,或者直接
用 help <efun名> 指令就可以得到帮助。efun及其依赖于你所在的Mud
的Driver,不同的Driver带有的efun区别是很大。
对于XO,使用的是MudOS,一般的efun,只要用 help 指令就能得到
帮助,或者你多看看源码,看看别人是怎样使用的,当然你如果无论如
何也不能明白一个efun,你可以问问大巫师,他们通常会很乐意和你探
讨的。但是有一点是指出,能自己解决的问题最好自己解决。
2.3 自己动手写函数
用Lpc写Object的函数,是为了表现这个Object的特性。这个特性的
函数实际上就是一些代码按顺序排列,排列的顺序决定了这个函数。一
个函数被调用,函数的代码就按照函数定义中代码按顺序执行。在
eventPrintValue()中,下面这个语句:
-----
x = add(2, 2);
-----
必须在 efun: write() 之前调用,如果你想看到正确的结果。
为了返回这个函