精华区 [关闭][返回]

当前位置:网易精华区>>讨论区精华>>已关闭版区>>● 天神议事厅>>LPC语言手册>>lpc教程 <1>

主题:lpc教程 <1>
发信人: zhangsf()
整理人: zhangsf(1999-12-31 17:33:37), 站内信件
以下文章摘自泥潭王国,在此向winding表示感谢!
基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 25 may 1993

第一章: 程式撰写环境的简介

1.1 UNIX 档案结构

LPMud 使用基本的 UNIX 命令及档案结构. 如果你已经了解 UNIX 的命令, 请注


意 (除了几个例外) 命令无法指定选项 (options). 跟 DOS  一样, UNIX  也使


用阶层式 (heirarchical) 的目录结构. 所有的次目录 (sub-directories)  都


附属于根目录 ( / , root)  之下. 而每个次目录之下也可以有更多的次目录. 


一个目录可以有两种表示方法:  

 1) 用目录的全名 (full name), 或称作绝对名称 (absolute name).
 2) 使用相对名称 (relative name).

绝对名称就是从根目录一路写下来, 直到该目录的名字为止. 举例来说:

    /players/descartes/obj/monster

就是根目录 (第一个 / 号)  之下的 player 目录之下的 descartes  目录的之


下的 obj  目录之下的 monster 目录.

相对名称使用的是相对于其他目录的名字. 以上面的例子来说, 相对于
/players/descartes/obj, 这个目录叫作 monster; 对于 /players/descartes


来说, 这个目录叫 obj/monster; 对 /players,  同一个目录叫作
descartes/obj/monster;  最后, 对 /  来说, 此目录叫作
players/descartes/obj/monster.  你可以看出来, 绝对名称与相对名称之间的


不同之处在于绝对名称总是从 /  开始. 而你如果要知道一个目录的相对名称,


就得搞清楚是相对于哪个目录.

一个目录可以包括一些次目录和档案. LPMud 只使用 mudlib 里面的文字档案.


就如同目录一样, 档案也有绝对与相对名称. 最基本的相对名称是该档案的名字


. 去掉档案名字之后, 剩下的绝对名称就是路径 (path).  拿一个档案举例:
/players/descartes/castle.c , 则 castle.c 是档名, /players/descartes
则是其路径.

在其他的 mud 里, 用普通的档案列表命令列出档案时, 档名开头是 .  的档案


 (像是 .plan)  是看不到的.

1.2 UNIX 命令

跟 UNIX 档案结构一样, LPMud  也使用许多的 UNIX 命令. 大部份的 mud  中


, 使用的典型 UNIX 命令有:

pwd, cd, ls, rm, mv, cp, mkdir, rmdir, more, head, cat, ed.

如果你从来没见过 UNIX 命令, 你大概会觉得这些命令没啥意义. 好吧, 它们的


确没有意义, 但是你一定用得到它们. 在我们搞清楚它们是什么东西之前, 先来


讨论目前目录 (current directory). 如果你熟悉 DOS, 那你就知道什么是目前


工作目录 (current working directory). 不管何时, 你一定在某个目录里面.


这表示, 你在 UNIX 命令里面所给的任何相对档案名称或相对目录名称, 都相对


于你现在所处的那个目录. 譬如说: 如果我的目前目录是 /players/descartes


, 而我输入 "ed castle.c" (ed 是编辑档案的命令), 那它就假设我指定的是
/players/descartes/castle.c 这个档案.

pwd: 显示你目前所在的工作目录.
cd: 改变你目前的工作目录. 你可以给它相对或绝对路径名称. 如果没有指
定参数 (argument),  就切换到你自己的家目录 (home directory).
ls: 列出一个目录里面所有的档案. 如果不指定目录, 则列出目前工作目录
的所有档案.
rm: 删除指定的档案.
mv: 更改指定档案的名字.
cp: 复制指定的档案.
mkdir: 制作新的目录.
rmdir: 删除一个目录. 该目录里面的档案必须先全部删除才行.
more: 分一页一页阅读一个指定的档案, 这样你的萤幕上会一次显示一页.
cat: 一次就把所有的档案内容全部倒给你.
head: 显示档案的前面几行.
tail: 显示档案的最后几行.
ed: 让你能用 mud  的编辑程式编修一个档案.

1.3 本章总结

UNIX 使用树状的阶层式档案结构, 而这棵树的根部叫做 / (根目录 root).  从


根目录分支出去的目录, 和这些目录自己分出去的目录就叫作次目录
(sub-directory).  任何目录都可以包含档案及目录. 目录和档案都能使用以 /


开头的绝对名称, 或相对于其他目录的相对名称. 你可以使用一些典型的 UNIX 


命令来使用 UNIX 的档案结构. 像是: 档案列表、显示目前工作目录、等等命令

.
在你的 mud  上, 上面的那些档案都应该有详细的命令说明, 让你能搞懂那些命


令到底是做些什么的. 另外, 也该有一份 mud  编辑程式的详细说明档案. 如果


你没用过 ed,  你应该详细阅读那份说明档.

基础 LPC
                   作者: Descartes of Borg
                   第一版: 23 april 1993
                  第二版: 16 june 1993

第二章: LPC 程式

2.1 关于程式

这一章的名字取得不怎么好, 因为没有人用 LPC  写程式. 写 LPC  程式的人写


的是物件 (objects). 这两种说法有啥差别 ?  好吧, 就我们现在的目标来说,


差别在于两者档案执行的方式不同. 当你「跑」一个程式的时候, 都是从程式中


固定的地方开始执行. 换句话说, 就是所有的程式开始执行的时候, 一定有个地


方写清楚要从那里开始. 另外, 程式有一个固定的终止点, 所以执行程式只要执


行到该终止点, 程式就中止执行. 总之, 程式从固定的开头跑到固定的结尾.
LPC 物件就不是这么一回事.

在 mud  里面, LPC 物件只是游戏 (driver) C 程式中, 显而易见的部分. 换句


话说, mud 程式在 driver 里面开始与结束执行. 但是实际上, 对于创造你玩的


mud 世界来说, driver 并没有做多少事. 反之, driver 相当依赖 LPC  码, 并


需要执行物件中的程式码. 所以  LPC 物件不需要有起始点, 也不需要有固定的


终止点.

就像其他的程式语言, LPC 「程式」可以由一个或一个以上的档案组成. 很简单

,
程式要先载入 driver 的记忆体. driver  会根据本手册所教的结构, 读取物件


中一行行的程式. 有一件重要的事你要先搞清楚, 就是 LPC  物件执行时没有开


头也没有终止.

2.2 diiver-mudlib 之间的互动

我先前提过, driver  是在主机上执行的 C  程式. 它让你连上游戏, 并执行
LPC 码. 注意, 这是 mud  程式设计的一个理论而已, 也不需要比其他的方法好

.
整个 mud  游戏可以全部用 C  来写. 这样游戏的执行速度快上很多, 却让 mud


缺乏可塑性, 使巫师在游戏正在执行的时候无法加入新东西. DikuMUD 就是全部


用 C  写成的. 相反的, LPMUD 的理论就是 driver 不该决定游戏内容, 而游戏


内容应该决定于游戏中的个别事物, 并能够在游戏执行时加上东西. 这就是为什


么 LPMUD  使用 LPC  程式语言. 它能让你用 LPC  定义游戏内容, 交给 drive

r
依需要读取并执行. 况且学 LPC  要比 C  容易得多, 这样让更多人能加入创造


世界的过程.

一旦你用 LPC  写了一个档案 (假设是用正确的 LPC),  它只是躺在你主机的硬


碟里不动, 直到游戏中有东西参考 (reference)  它. 当游戏中有东西终于参考


到它时, 这个档案就会被复制一份到记忆体里面, 并且呼叫这个物件中一个特殊


的函式 (function). 呼叫这个函式的目的是初始化 (initialize)  这个物件中


的变数. 现在, 别管你脑袋里才看到的上两句话, 因为一个对程式设计完全陌生


的人来说, 哪里会知道函式或变数到底是啥东西. 现在重要的是要知道 driver


读取主机硬碟里面的物件档案, 复制一份之后扔进记忆体储存 (既然是复本, 也


就可以有许多不同的版本 ). 你稍后会知道什么是函式、什么是变数, 并搞清楚


到底游戏中的一些东西是怎么参考你的物件的.

2.3 将一个物件载入记忆体

虽然一个物件里面并没有规定要从一个固定的地方开始执行程式, driver  却要


先找到一个固定的地方并执行之, 才能初始化一个物件. 在精简模式的 driver 


上, 这是一个叫作 reset()  的函式. 在原始模式 mud  中, 则是 create().

LPC 物件是由变数 (variable) 所组成的 (会更改的值) 而函式是处理这些变数


的程式. 函式经由 LPC  语法结构来处理变数, 语法结构包括: 呼叫其他函式、


使用外部定义函式 (externally defined functions, efuns)、基本的 LPC  运


算式 (expression) 和流程控制 (flow control mechanism).

前面这些听起来乱七八糟的吧 ?  让我们从变数开始著手. 拿「等级」变数来说


吧, 等级可以随情形不同而改变它的数值, 而不同的事物也使用玩家的等级数字


作出不同的事. 举个例: 如果你是等级十九级的玩家, 则等级变数的数值就是
 19 . 如果你的 mud  是旧的 LPMud 2.4.5  系统, 等级 1  到 19 级是玩家,


 20 级以上是巫师, 则会有许多事物会询问你的等级变数值, 判断你能不能使用


巫师的动作. 基本上, 任何 LPC  物件就是一堆会随时间不同而改变的变数组成


的. 发生在物件身上的事, 都基于该物件的各个变数里头的数值. 而常常也有许


多事会更改变数.

所以无论何时, 一个 LPC  撰写的物件被其他在记忆体的物件拿来参考时, driv

er
就寻找这物件里面所要找的值在哪里 (但是现在还没有任何数值) . driver  找


过之后, 就呼叫物件中的 reset() 或 create() 函式  (视不同 driver 而定)


, 来设定该物件一开始的变数值. 就这样, 经由「呼叫」「函式」处理变数.

虽然绝大多数的 LPC  程式码都从 create() 或 reset()  开始执行, 此处却不


是 LPC  程式码开头的地方. 事实上, 没有这两个函式也没关系. 如果你的物件


一开始所有的值都是 NULL  (虚无) 指标 (在此, 虚无指标我们先当它是 0  吧

)
, 那你就不需要 create() 或 reset()  函式. 所以, 每个物件开始执行程式码


的地方都可能完全不同.

现在让我们搞清楚这整章在讲些什么. 问题是: 一个完整的 LPC  到底是由哪些


西组成的 ?  好, 一个 LPC  物件简单来说, 就是一个或一个以上的函式组合起


, 处理一个以上的变数 (或是不处理变数也行) . 各个函式之间完全不用管它们


摆的先后顺序. 换句话说:

-----
void init() { add_action("smile", "smile"); }

void create() { return; }

int smile(string str) { return 0; }
-----

跟底下的一样:

-----
void create() { return; }

int smile(string str) { return 0; }

void init() { add_action("smile", "smile"); }
_____

另外有个很重要的事提醒你, 下面这个物件只有:

-----
void nonsense() {}
-----

这样也可以, 但是这种微不足道的物件, 它大概不会与你的 mud  中的其他物件


作出正确的互动关系, 因为这样的物件没有重量、看不到......以此类推.

2.4 本章总结

LPC 码没有起点或终点, 因为 LPC  码是用来创造 driver 程式使用的物件, 而


非单独的程式. LPC 物件包括一个或多个函式, 其间先后顺序毫无关系, 而这些


函式之中, 处理多个变数 (或根本没有任何变数) . LPC 物件只是躺在主机的
硬碟里面, 等著游戏中其他的物件参考它 (换言之, 它们实际上不存在) . 一
旦一个物件被参考到, 它会被载入记忆体中, 并且它所有的变数都是零. 精简模


式 mud  呼叫此物件的 reset()  而原始模式 mud  呼叫 create()  (如果此物


件有这些函式的话 ), 让这些函式来指定一些变数的初始值. 物件中的其他函式


由 driver 或游戏中其他的物件使用之, 让物件之间达到互动并处理 LPC  变数



reset() 和 create() 的说明:

只有原始模式的 mud 使用 create() (请见本手册的 Introduction 一章, 有关


原始模式和精简模式的介绍). 此函式仅用来初始化刚被参考的物件.

原始模式及精简模式的 mud 都使用 reset() 函式. 在精简模式 mud  中, 
reset() 有两个功能. 第一, 它用来初始化刚被参考的物件. 第二, 在精简模式


的 mud  中, reset() 用来重新设定物件. 也就是说, 让物件回到最初的状态. 


这样可以让一个房间内的怪物重生, 或把一道门关回去......以此类推. 原始模


式的 mud  只用 reset()  作第二种功能 (就跟 reset  的意思一样). 

所以在 LP 式的 mud  中有两件重要的事情让 driver 呼叫物件中的函式. 第一


件事是创造物件. 此时, driver  呼叫物件中的一个函式来初始化物件的变数值


. 在精简模式的 mud  里, 由 reset()  做此工作 (要加上 0  参数, 后面的章


节再讨论参数是啥). 原始模式的 mud  下, 由 create() 做此工作.

第二件事是把房间重新设定回某些基本的状况. 这些基本的设定可能会与一开始


的初始值不同, 也可能相同, 而你当然也不想花时间一直去重覆做某些事 (像是


重新设定一些不会更改的变数) . 精简模式的 mud  用 reset()  函式来创造和


重新设定物件. 而原始模式的 mud  用 create() 创造物件, 用 reset()  重新


设定物件. 但是精简模式也不会失去所有的变数值, 因为有个方法能区分是创造


物件还是重新设定物件. 在精简模式要重新设定, 则 driver 传入 1  或重新设


定的数字当作 reset()  的参数. 现在这个对你来说没啥意义, 但是要记住, 你


在精简模式实际上是可以把两种情形区分开来的. 另外也要记住, reset() 在创


造物件时传入的参数是 0, 而重新设定物件时传入非零值.

基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 17 june 1993

第三章: LPC  的资料型态 (data type)

3.1 你现在该知道的事

LPC 物件由零个或多个变数组合而成, 而这些变数由一个或多个函式组合而成. 


在程式码中, 这些函式的先后顺序是无关紧要的. 当你写的 LPC  第一次被参考


时, driver  把它复制一份到记忆体中. 之后, 还可藉此复制出更多相同的拷贝


任何一份物件被载入记忆体时, 所有的变数一开始都指向「虚无值」. 精简模式


mud 的 reset()  函式与原始模式的 create() 函式都都用于指定物件的初始变


数值. 物件载入记忆体之后, 会立刻呼叫创造的函式. 不过, 如果你读这份课本


之前没有写过程式, 你大概不知道什么是函式 (function) , 或函式是怎么被呼


叫的. 就算你以前写过程式, 你大概也想知道新创造的物件中, 函式之间互相呼


叫对方的过程是什么. 回答以上这些问题以前, 你得多了解函式在处理什么. 所


以你应该先彻底了解 LPC  资料型态背后的观念. 说实在的, 在这份手册里头最


无聊的主题, 也是最重要的主题, 90% 以上就是用错 LPC  资料型态 (放错 {}


和 () 不算在内).  所以说, 你得要耐心看完非常重要的这一章, 因为我觉得你


如果搞懂这一章, 可以让你以后写程式大大轻松不少.

3.2 与电脑沟通

你应该已经知道电脑不懂人类所使用的单字与数字. 电脑所说的「语言」由 0 


与 1  的「字母」所组合而成. 当然, 你知道电脑不懂人类的自然语言. 但是实


际上, 它们也不懂我们写给它们的电脑语言. 像是 BASIC、C、C++、Pascal 等等


, 这些电脑语言全都是过渡语言. 这些电脑语言让你能把想法组织起来, 让思考


更易转换成电脑的 0  与 1  语言.

转换有两个方法: 编译 (compilation)  和直译 (interpretation) . 这两个方


法的差别在于程式语言转换成真正电脑语言的时候. 对编译的程式语言来说, 程


式设计者撰写程式码之后, 使用编译程式 (compiler) 把程式码转换成电脑真正


的语言. 程式在执行之前就已经转换完毕. 而直译的程式语言, 在程式执行的时


候才开始转换. 因此直译的程式语言所写的程式执行起来要比编译的慢上许多.



总而言之, 不管你用什么程式语言撰写程式, 最后都要转变成 0  与 1  才能让


电脑搞懂. 但是你储存在记忆体中的变数并不是单纯的 0  与 1. 所以你用的程


式语言要有个方法告诉电脑, 这些 0  和 1  到底要当作十进位数字、字元
 (characters) 、字串 (string) 、还是当作其他的东西看待. 你可以靠著指定


资料型态来办到.

举例来说, 假设你有个变数叫做 x ,  而你给它一个十进位的值 ── 65. 在
LPC 里面, 你会写出下面的叙述:

-----
x = 65;
-----

你等一下再做像这样的事:

_____
write(x+"\n");        /* \n 符号代表在此换行 (carriage return) */
y = x + 5;
-----

第一行让你送出 65 和换行到某个人的萤幕上. 第二行让你把 y  设定为 70.  


问题是你告诉电脑 x = 65; 时,  它不知道 65 到底是啥意思. 你认为是 65,  


对电脑来说也许认为是:
00000000000000000000000001000001
而且, 对电脑来说, A 这个字母就是:
00000000000000000000000001000001
所以, 不管你什么时候告诉电脑 write(x+"\n");,  电脑总要有个方法知道你想


看到 65 而不是 A.

电脑能透过资料型态了解 65 与 A  的不同. 资料型态只是说记忆体位置中储存


的指定变数到底是属于什么型态的资料. 所以说, 每一个 LPC  变数都有变数型


态指导如何转换资料. 在上面的范例里, 你应该会在程式码「之前」加上以下这


行:

-----
int x;
-----

这一行告诉 driver 无论 x  指向何处, 都当作「int」 资料型态来使用. int 


是整数 (interger, 或称 whole number)  的缩写. 现在我们已经初步介绍为什


么要有资料型态. 这样一来, driver  才能搞清楚电脑储存在记忆体中的 0  与


1 到底是代表什么意义. 

3.3 LPC 的资料型态

所有的 LPMud driver 都有以下的资料型态:

void (无), status  (状况), int (整数), string  (字串), object  (物件),

 
int *  (整数指标), string *  (字串指标), object *  (物件指标), 
mixed * (混合指标)

很多种 driver  (不是全部) 有下列资料型态值得讨论:

float  (浮点数), mapping  (映射), float *  (浮点数指标), 
mapping * (映射指标)

少数 driver 有下列罕用的资料型态, 并不值得讨论:

function (函式), enum, struct  (结构), char  (字元)

 (译注: 目前台湾绝大多数的 LPMud  所使用的 driver 是 MudOS, 其资料型态


有些许不同之处. 请详见参考译者所翻译之 MudOS  参考文件)

3.4 简单的资料型态

这份简介性质的课本会介绍 void, status, int, float, string, object, 
mixed 这几种资料型态. 你可以在中阶课本 (intermediate book,  译注: 本作


者另外有写一份中阶 LPC  手册, 译者亦有翻译) 找到像是 mapping (映射) 或


array (阵列)  这种更复杂的资料型态. 本章先介绍两种最简单的资料型态 (以


LPC 程式设计者的观点来看)  ── 整数 (int)  和字串 (string).

int 表示任何整数. 所以 1, 42, -17, 0, -10000023 都是整数 (int)  型态. 


string  是一个以上的字元或数字. 所以 "a", "we are borg", "42", 
"This is a string"  都是字串. 请注意, 字串前后都要加上双引号 "" , 
driver  才能分辨 int 42 和 string "42". 也才能区别变数名称 (像是 x )  


与字串 (像是 "x" ).

当你在程式码中使用变数, 你一开始要让 driver 知道这个变数所指的是哪种变


数型态. 这种处理方式叫做「宣告」 (declaration). 你得在函式一开始的地方


宣告, 或是在物件程式码的开头之处 (在函式之外, 任何函式用到该变数之前).


要宣告变数型态的话, 只要像底下一样, 把变数型态摆在变数的名字前便即可.



-----
void add_two_and_two() {
    int x;
    int y;

    x = 2;
    y = x + x;
}
-----

像这样, 这是一个完整的函式. 函式的名称是 add_two_and_two(). 函式一开始


宣告一个整数变数 x, 之后宣告一个整数变数 y. 所以, 在这里 driver 有两个


变数指向 NULL  (虚无) 值, 而这两个变数期待的变数值是整数型态.

关于虚无 (void) 和状态 (status) 资料型态:

无 (void) 是一种很普遍的资料型态, 它不指向任何东西. 它并不是用在变数上

面的
型态, 而是用于函式. 你稍后会了解这里所说的事. 而现在, 你只需要知道 voi

d
不指向任何值.

状况 (status) 资料型态是布林 (boolean)  资料型态. 就是说, 它的值是 0  


或 1. 这种值常常称为真 (true) 或伪 (false).

3.5 本章总结

对变数来说, driver  需要知道电脑储存在记忆体中的 0  与 1  要如何转换成


你想使用的形式. 最简单的 LPC  资料型态是 void, status, int, string. 变


数不使用 void 的资料型态, 但是这种资料型态用于函式. 另外, 资料型态用于


转换格式, 决定 driver 应该使用哪种规则处理运算, 像是 +, - ......以此类


推. 举例说, 运算式 (expression) 5+5, driver 知道 5  加上 5  的值是 10.


对字串来说, 对字串使用整数加法没有意义. 所以, "a"+"b" 把 "b"  加在 "a"


的后面, 最后得出 "ab". 当你试著把 "5"+5 就会产生错误. 因为把整数加上字


串是无意义的, 所以 driver 会把第二个 5  转换成 "5"  再加起来. 最后的结


果是 "55".  如果你想看的结果是 10 , 你最后只得到错误的程式码. 请记住,


大多数的情况下, driver  不会像前面这样产生 "55" 这种有用的结果. 它会产


生 "55" 是因为它早有一条规则处理整数加上字串的情况, 也就是把整数当成字


串看待. 在大多数的状况中, 如果你在运算式或函式中使用资料型态并没有事先


定义 (像是你试著把 "this is"  除以 "nonsense", "this is" / "nonsense")


, driver  会呕吐并回报错误给你.

基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 22 june 1993

第四章: 函式 (functions)

4.1 回顾

现在, 你应该了解 LPC  物件由许多处理变数的函式所组成. 函式执行时就处理


变数, 而经由「呼叫」执行这些函式. 在一个档案里, 函式之间的前后顺序是无


关紧要的. 变数在函式里面被处理, 变数储存在电脑的记忆体中, 而电脑把它们


当作 0  与 1  来处理. 利用定义资料型态这种方法, 这些 0  与 1  被转换成


可使用的输出及输入结果. 字串 (string) 资料型态告诉 driver , 让你看到或


你输入的资料应该是许多字元及数字的形式. 整数 (int)  型态的变数对你来说


就是整数值. 状况 (status) 型态对你来说就是 1  或 0. 无 (void) 资料型态


对你或对机器而言都没有值, 并不是用于变数上的资料型态.

4.2 什么是函式 ?

就像数学函式, LPC 函式获得输入值, 然后传回输出值. 像 Pascal 语言把程序


(procedure) 和函式 (function) 区分开来. 但是 LPC  不这样做, 而知道这种


区分也是有用的. Pascal  称为程序的东西, 在 LPC  就是无传回值 (void) 型


态的函式. 也就是说, 程序或无传回值函式没有传回输出值. Pascal  称为函式


的东西, 就是有传回输出值的. 在 LPC  里, 最短的正确函式是:

-----
void do_nothing() { }
-----

这个函式不接受输入, 没有任何指令, 也不传回任何值.

要写出正确的 LPC  函式有三个部分:
1)  宣告 (declaration)
2)  定义 (definition)
3)  呼叫 (call)

就像变数一样, 函式也要宣告. 这样一来, 让 driver 知道: 1)  函式输出的资


料是什么型态 2) 有多少个输入的资料以及它们的型态为何. 比较普通的讲法称


这些输入为参数 (parameter).
所以, 宣告一个函式的格式如下:
传回值型态  函式名称 (参数 1, 参数 2, ...,  参数 N);
底下宣告一个 drink_water()  的函式, 它接受一个字串输入, 而输出一个整数

:

-----
int drink_water(string str);
-----

str 是输入的变数名称, 会用于函式之中.

函式定义是描述函式实际上如何处理输入值的程式码. 

呼叫则是其他函式之中, 呼叫并执行此函式的地方. 对 write_vals() 和 add()


两个函式来说, 你可能会有这些程式码:

-----
/*  首先, 是函式宣告. 它们通常出现在物件码的开头.
*/
void write_vals();
int add(int x, int y);

/*  接著是定义 write_vals() 函式. 我们假设这函式将会在物件以外被呼叫.


*/
void write_vals() {
    int x;

    /*  现在我们指定 x  为呼叫 add()  的输出值. */
    x = add(2, 2);
    write(x+"\n");
}

/*  最后, 定义 add() */
int add(int x, int y) {
    return (x + y);
}
-----

请记得, 哪一个函式定义在前都没有关系. 这是因为函式并不是由前往后连续执


行的. 函式只有被呼叫时才会执行. 唯一的要求是, 一个函式的宣告必须出现在


函式的定义之前, 而且也必须在任何函式定义呼叫它之前.

4.3 外部函式 (efuns)

也许你已经听过有人提过外部函式. 它们是外部定义的函式. 跟名称一样, 它们


由 mud driver 所定义. 如果你已经撰写 LPC  程式码很久, 你大概已经发现你


听到的一些式子, 像是 this_player(), write(), say(), this_object()...  


等等, 看起来很像函式. 这是因为它们是外部函式. 外部函式的价值在于它们比


LPC 函式要快得多, 因为它们早已经以电脑了解的二进位格式存在著.

在前面的 write_vals() 函式里, 呼叫了两个函式. 第一个是 add()  函式, 是


你宣告及定义的函式. 第二个, 则是称做 write()  的外部函式. driver  早就


帮你宣告并定义这个函式. 你只需要呼叫它.

创造外部函式是为了处理普通的、每天都用得到的函式呼叫、处理 internet
socket  的输出与输入、其他用 LPC  难以处理的事. 它们是在 game driver 


内以 C  写成的, 并与 driver 一起编译在 mud  开始之前, 让它们执行起来快


得多. 但是对你来说, 外部函式呼叫就像对你的函式呼叫一样. 不过, 任何外部


函式还是要知道两件重要的事: 1)  它的传回值是什么, 2)  它要什么参数.

外部函式的详细资料, 像是输入参数和传回值, 常常可以在你的 mud 中的
/doc/efun 目录找到. 我没有办法在这里详细介绍外部函式, 因为每种 driver


的外部函式都不相同. 但是, 你常常可以藉由「man」 或「help」指令 (视
mudlib  而定) 找到详细的资料. 例如指令「man write」 会给你 write  外部


函式的详细资料. 如果都不行, 「more /doc/efun/write」也可以.

看过 write  的详细资料之后, 你应该找到 write  是宣告成这样:

-----
void write(string);
-----

这样告诉你, 要正确呼叫 write  不应该期待它有传回值, 而且要传入一个字串


型态的参数.

4.4 定义你自己的函式

虽然在档案中, 你的函式次序谁先谁后都没有关系, 但是定义一个函式的程式码


的先后顺序就非常重要. 当一个函式被呼叫时, 函式定义中的程式码按照出现的


先后顺序执行. 先前的 write_vals() 中, 这个指令:
    
-----
x = add(2, 2);
-----

如果你想看到 write()  使用正确的 x  值, 就必须把它放在 write()  呼叫之

前.

当函式要传回一个值时, 由「return」指令之后跟著与函式相同资料型态的值所


完成. 在先前的 add()  之中, 指令「return (x+y);」 把 (x+y)  的值传回给


write_vals()  并指定给 x. 在更普通的层次上来说, 「return」停止执行函式


, 并传回程式码执行的结果给呼叫此函式的函式. 另外, 它将跟在它后面任何式


子的值传回呼叫的函式. 要停止执行失去控制的无传回值函式, 使用 return;
而后面不用加上任何东西. 请再次记得, 使用「return」传回任何式子的资料型


态「必须」与函式本身的资料型态相符合.

4.5 本章总结

定义 LPC  物件的档案是由函式所组成的. 函式依次由三个部分组成:
    1)  宣告
    2)  定义
    3)  呼叫
函式宣告通常出现在档案的最前面, 在任何定义之前. 不过函式只要求在函式定


义之前以及任何函式呼叫它之前宣告它.
函式定义可以任何顺序出现在档案里, 只要它们都放在宣告之后. 另外, 你不可


以再一个函式里面定义另一个函式.
函式呼叫则出现在其他任何函式中, 任何程式码想执行你的函式的地方. 呼叫也


可以出现在自己的函式定义中, 但是这种做法并不建议给新手去做, 因为它很容


易变成无穷回圈.

函式定义依序由底下的部分所组成:
    1)  函式传回值型态
    2)  函式名称
    3)  一个左小括号 (  接著列出参数再加上一个右小括号 )
    4)  一个左大括号 {  指示 driver 从这里开始执行
    5)  宣告只用在这个函式中的任何变数
    6)  指令、式子、视需要呼叫其他函式
    7)  一个右大括号 }  描述函式码在此结束. 对于无传回值函式来说, 如果


        在此还没有碰到「return」指令 (只适用于无传回值函式) , 会如同有


        碰到「return」指令一样回到原来呼叫的函式执行.

最短的函式是:

-----
void do_nothing() {}
-----

因为这个函式不接受任何输入, 不做任何事, 也不传回任何输出.

任何无传回值型态以外的函式「必须」传回一个与函式资料型态相同的值.

每一种 driver 都有一套早已经帮你定义好的函式, 它们叫做外部函式. 你不需


要宣告或定义它们, 因为它们早已经帮你做好这些事. 更深入一点, 执行这些函


式比起执行你的函式要快得多, 因为外部函式是 driver 的一部份. 再者, 每一


个 mudlib 都有特殊函式像是外部函式一样, 早已经为你宣告并定义好. 但是不


同的是, 它们用 LPC  定义在 mudlib 里面. 它们叫做模拟外部函式
(simul_efuns, 或 simulated efuns). 在大多数的 mud  里, 你可以在
/doc/efun 目录底下找到关于它们的详细资料. 另外, 很多 mud  有称作
「man 」或「help」的命令, 让你可以方便地叫出这些资料档案.

程式风格的注解:

有些 driver 可能不会要求你宣告函式, 有些不会要求你指定函式的传回值型态


无论如何, 底下有两个理由劝你不要省略以上这些动作:
    1)  对其他人来说 (还有你自己过了一段时间之后) , 会比较容易读懂你的


        程式码并了解程式码的意义. 这对除错时特别有用, 有很多错误 (除了


        放错地方的各种括号) 发生在资料型态上 (有没有碰过「Bad arg 1 to


         foo() line 32」? (程式第三十二行, 呼叫 foo() 时的第二个参数有

错) ).
    2)  大家认为这样子写程式是个好习惯.

基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 01 july 1993

第五章: 基础的继承 (inheritance)

5.1 回顾

你现在应该了解函式基本的功能. 你应该可以宣告并呼叫一个函式. 另外, 你应


该能认识函式定义, 虽然你可能是第一次接触 LPC. 你现在并不见得能定义你自


己的函式. 函式是 LPC  物件的基石. 函式中的程式码, 要别的函式呼叫它们的


时候才会执行. 呼叫一个函式时, 作出呼叫的函式要给它输入值, 才能执行被呼


叫的函式. 被呼叫的函式执行其程式码, 并传回某种资料型态的传回值给呼叫它


的函式. 没有传回值的函式属于无传回值 (void) 型态.

仔细看过你自己的工作室程式码之后, 它看起来大概像这样 (视 mudlib 而定):



-----
inherit "/std/room";

void create() {
    ::create();
    set_property("light", 2);
    set_property("indoors", 1);
    set("short", "Descartes 的工作室");
    set("long", "此处是 Descartes 工作的地方.\n这里是一个立方体.\n");


    set_exits( ({ "/d/standard/square" }), ({ "square" }) );
}
-----

如果你到目前为止, 所有的课本内容都了解的话, 你应该能认出以下的程式码:


    1)  create()  是函式的定义. (嘿 !   他没有宣告它)
    2)  它呼叫 set_property() 、set()、set_exits(), 没有一个函式在这段


        程式码中曾有宣告或定义.
    3)  最上面有一行, 不是宣告变数或函式, 也不是函式定义 !

这一章会找出这些问题的解答, 你现在应该脑中应该有这些问题:
    1)  为什么没有宣告 create() ?
    2)  为什么 set_property() 、set() 、set_exits() 已经宣告并定义过了

 ?
    3)  档案最上面那一行到底是啥东西 ?

5.2 物件导向程式设计 (object oriented programming, OOP)

继承 (inheritance)  是定义真正物件导向程式设计的特性之一. 它让你创造通


用的程式码, 能以多种用途用于许多不同的程式中. 一个 mudlib 所作的, 就是


创造这些通用的档案 (物件) , 让你用来制造特定物件.

如果你必须把定义前面工作室全部所需要的程式码写出来, 你大概必须要写 100

0
行程式码才能得到一个房间所有的功能. 当然, 那根本是浪费磁碟空间. 再者, 


这种程式码与玩家和其他房间的互动性很差, 因为每一个创造者都写出自己的函


式以作出一个房间的功能. 所以, 你可能使用 query_long() 写出房间的长叙述


, 其他的巫师可能使用 long() . 这就是 mudlib 彼此不相容最主要的原因, 因


为它们使用不同的物件互动协定.

OOP 克服了这些问题. 前面的工作室中, 你继承已经定义在 "/std/room.c" 档案


中的函式. 它拥有普通房间所需要的全部函式定义其中. 当你要制造一个特定的


房间, 你拿这个房间档案中定义好的通用函式功能, 并加上你自己的函式
 create() 以制造一个独特的房间.

5.3 继承如何作用

你现在大概猜得出来, 这一行:

-----
inherit "/std/room";
-----

让你继承 "std/room.c" 的函式功能. 藉由继承函式功能, 它代表你可以使用
"/std/room.c" 里面已经宣告并定义好的函式. 在 Nightmare Mudlib 中, 
"/std/room.c" 里面有许多函式, 其中有 set_property() 、set() 、
set_exits() 函式, 都已经宣告并定义过. 在你的 creat()  函式里, 你呼叫那


些函式来设定你房间一开始的值. 这些值让你的房间不同于别的房间, 却保留与


记忆体中其他房间互动的能力.

实际的写作中, 每一个 mudlib 都不同, 所以要你使用不同一套的标准函式来达


到相同的功能. 说明有哪些函式存在和它们是作什么用的, 已经超出了这本课本


的范围. 如果你的 mudlib 有自己详细的说明资料, 你会找到教你如何使用各种


继承档案的说明文件以创造物件. 这些说明应该会告诉你有哪些函式、它们需要


哪些输入、它们输出的资料型态、以及它们的功能.

5.4 本章总结

本章距离完整解释继承如此复杂的主题还有一大段距离. 本文的目的只是让你能


了解如何使用继承来创造你的物件. 以后的课本将对此会有完整的讨论. 现在你


应该已经了解底下几点:
    1)  每一个 mudlib 都有一套通用物件库, 有它们自己的通用函式. 创造者


透过继承使用它们, 让撰写物件程式码这件工作更轻松, 并与其他物件之间能良


好互动.
    2)  可被继承的档案里头的函式, 每个 mudlib 都不一样. 你的 mud  里应


        该有说明文件解释如何使用这些可被继承的档案. 如果你还不知道有哪


        些函式可用, 那你就没有办法用它们. 任何时候, 都请你特别注意输入


        和输出的资料型态.
    3)  你藉由底下这行继承函式的功能:

-----
inherit "filename";
-----
       
filename  是被继承的物件档案名称. 这行放在你程式码的开头. 

注解:

你可能看到有几处地方有 ::create() 或 ::init() 或 ::reset()  语法. 你现


在不需要完全了解这个, 但是应该告诉你一点线索, 知道它到底是什么. 「::」


运算子是一种特殊的方法来呼叫继承物件的函式 (叫做范围解析运算子 scope
 resolution operator). 例如, 大多数 mud  的 room.c 都有叫做 create()  


函式. 当你继承 room.c 并设定 create() 时, 你所作的事称为超越 (override

)
 room.c 的 create() 函式. 这表示不管任何东西呼叫你房间的 create() , 它


会呼叫「你的」版本, 而不是 room.c 里面的那一个. :: 运算子让你能呼叫
 room.c 里的 create() 而不是你的 create().

一个例子:

-----
#1

inherit "/std/room";

void create() { create(); }
-----

-----
#2

inherit "/std/room";

void create() { ::create(); }
-----

第一个例子是个恐怖的例子. 当它被载入时, driver  呼叫 create() , 之后
 create() 再呼叫 create(), create() 又呼叫 create(), 这时 create()  又


呼叫 create()......换句话说, 所有的 create()  就一直呼叫自己直到 drive

r
侦测到太深的递回 (recursion) 并跳出来.

第二个例子基本上只是浪费记忆体, 它的功能跟 room.c 没有两样. 对它而言,


 driver 先呼叫它的 room.c , 然后呼叫 ::create() , 也就是 room.c 里的
 create() . 其他的地方就跟 room.c 的功能一样.

基础 LPC 作者: Descartes of Borg 第一版: 23 april 1993 第二版: j

uly 5 1993
第六章: 变数 (variable) 处理6.1 回顾现在你应该能利用你 mud  的标准物件

库, 撰写一些简单的物件. 继承能让你使
用那些物件中已经定义好的函式, 而不用自己去定义. 另外, 你应该知道如何宣

告你自己的函式. 这一章将教你 LPC  的基本元素, 让你能藉由处理变数来定义


你自己的函式.6.2 数值与物件基本上, mud 里头的物件都不一样的原因有两个:

1)  有的物件拥有不同的函式2)  所有的物件都有不同的数值
现在, 所有的玩家物件都有同样的函式. 它们不一样的地方在于它们自己所拥有


的数值不同. 举例来说, 名字叫做「Forlock」的玩家跟「Descartes」「至少」


他们各自的 true_name  变数值不同, 一个是 "descartes", 另一个是 "forloc

k".
所以, 游戏中的改变伴随著游戏中物件值的改变. 函式名称就是用来处理变数的

过程名称. 例如说, create()  函式就是特别用来初始化一个物件的过程. 函式


之中, 有些特别的事称为指令. 指令就是负责处理变数的.6.3 区域 (local) 和

全域 (global) 变数
跟大多数程式设计语言的变数一样, LPC 变数可以宣告为一个特定函式的「区域

」变数, 或是所有函式可以使用的「全域」变数. 区域变数宣告在使用它们的函


式之内. 其他函式并不知道它们存在, 因为这些值只有在那个函式执行时才储存

在记忆体中. 物件码宣告全域变数之后, 则让后面所有的函式都能使用它. 因为


只要物件存在, 全域变数就会占据记忆体. 你只有在整个物件中都需要某个值的

时候, 才要用全域变数. 看看下面两段程式码:-----int x;
int query_x() { return x; }void set_x(int y) { x = y; }----------
void set_x(int y) {    int x;    x = y;    write("x 设定为 "+x+" 并且会

消失无踪.\n");}
-----第一个例子里, x 宣告在所有的函式之外, 所以在 x  宣告之后的所有函式

都能使用它. x 在此是全域变数.
第二个例子中, x 宣告在 set_x()  函式里. 它只有在 set_x()  执行的时候存

在. 之后, 它会消失. 在此, x 是区域变数.
6.4 处理变数的值给 driver 的指令 (instruction)  用来处理变数值. 一个指

令的范例是:-----x = 5;-----
上面的指令很清楚. 它把 5  这个数值指定给 x  变数. 不过, 这个指令牵涉到


一些对普通指令来说很重要的观念. 第一个观念是运算式 (expression).一个运

算式就是有值的一系列符号. 在上面的指令中, 运算式 5  的值指定给变
数 x. 常数 (constant) 是最简单的运算式. 一个常数就是不变的值, 像是整数


 5  或是字串 "hello". 最后一个观念就是运算子 (operator).  在上面的例子


中, 使用了 =  这个指定运算 (assignment operator).在 LPC  有更多其他的运

算子, 还有更复杂的运算式. 如果我们进入一个更复杂
的层次, 我们得到:-----y = 5;x = y +2;-----第一个指令使用指定运算子以指

定常数运算式 5  的值给变数 y. 第二个指令把
 (y+2)  的值以加法运算子把 y  和常数运算式 2  加起来, 再用指定运算子指

定给 x. 听起来一点意义都没有吧 ?
换另一种方法来讲, 使用多个运算子可以组成复杂的运算式. 在前面的范例中,一

个指令 x = y + 2; 里面含有两个运算式:    1)  运算式 y+2
    2)  运算式 x = y + 2前面曾提过, 所有的运算是都有其值. 运算式 y+2  

的值是 y  和 2  的总和
 (在此是 7) ;  运算式 x = y + 2 「也」有其值 ── 7.所以运算子有两个重

要的工作:    1)  它们「可以」像函式一样当作输入.
    2)  它们运算起来就像本身有值一样.现在, 不是所有的运算子的功能都像 

1) 一样. = 运算子将它右边的值指定给 x.
但是 +  就没有这种功能. 而且, 它们两个也有自己的值.6.5 复杂的运算式
前面你大概已经注意到, 运算式 x = 5  「本身」也有个值是 5. 实际上, 因为


LPC 运算子如同运算式一样也有自己的值, 它们能让你写出一些非常难解、看起

来毫无意义的东西, 像是: 
    i = ( (x=sizeof(tmp=users())) ? --x : sizeof(tmp=children("/std/mo

nster"))-1)
基本上只是说:    把外部函式 users()  传回的阵列指定给 tmp, 然后把此阵列

元素的数目指
    定给 x. 如果指定给 x  的运算式值为真 (不是 0) , 就指定 x  为 1  并


    指定 i  的值为 x-1  的值. 如果 x  为伪, 则设定 tmp  为外部函式
     children() 传回的阵列, 并指定 i  为阵列 tmp  的元素数目再减 1.
你曾经用过以上的叙述吗 ?  我很怀疑. 不过你可能看过或使用与它相似的运算

式, 因为一次合并这么多的东西在一行里面, 能提升你程式码的执行速度. 比较


常使用 LPC  运算子这种特性的写法大概像这样:    x = sizeof(tmp = users(

));
    while(i--) write((string)tmp[i]->query_name()+"\n");取代这样子的写

法:
    tmp = users();    x = sizeof(tmp);    for(i=0; iquery_name()+"\n")

;
像是 for()、while() 、阵列......等等东西稍后会解释.不过第一段程式码比较

简洁, 执行起来也比较快.
附注: 在本章总结之后会对所有的 LPC  运算子有更详细的说明.6.6 本章总结你

目前知道如何宣告变数, 并了解宣告、使用全域和区域变数之间的不同. 一旦
你熟悉你 driver 的外部函式, 你就能用许多不同的方法显示那些值. 另外, 藉


由 LPC  运算子, 你知道怎么改变并运算变数里头的值. 这当然对你很有用, 因

为它让你能做一些事, 像是算出从树上摘下了多少颗苹果, 一旦苹果都摘完了, 


就没有人有苹果可摘. 很不幸, 你现在只会写寥寥几行能执行的程式. 换句话说

, 到下一章以前先别管苹果的问题, 因为你还不知道如何检查全部摘下的苹果数


目和树上原先的苹果数目是否相等. 你也不知道特殊的函式 init(),  能让你给

玩家使用新的指令. 但是你已经准备好撰写良好而复杂的区域程式码.
6.7 LPC  运算子这一段将详细列出比较简单的 LPC  运算子, 包括对它们使用的

值所作的事 (如果有值的话), 以及它们自己拥有的值.
在此说明的运算子有:=    +    -    *    /    %    +=    -=    *=    /= 

   %=
--    ++    ==    !=    >    < >=    <= ! && ||->    ? :
下面, 这些运算子将全部用相当简单的方式说明之, 但是你最好把每个运算子至

少都看过一次, 因为有些运算子的功能「不见得」如你所想的一样


-----
void init() { add_action("smile", "smile"); }

void create() { return; }

int smile(string str) { return 0; }
-----

跟底下的一样:

-----
void create() { return; }

int smile(string str) { return 0; }

void init() { add_action("smile", "smile"); }
_____

另外有个很重要的事提醒你, 下面这个物件只有:

-----
void nonsense() {}
-----

这样也可以, 但是这种微不足道的物件, 它大概不会与你的 mud  中的其他物件

作出正确的互动关系, 因为这样的物件没有重量、看不到......以此类推.

2.4 本章总结

LPC 码没有起点或终点, 因为 LPC  码是用来创造 driver 程式使用的物件, 而

非单独的程式. LPC 物件包括一个或多个函式, 其间先后顺序毫无关系, 而这些

函式之中, 处理多个变数 (或根本没有任何变数) . LPC 物件只是躺在主机的
硬碟里面, 等著游戏中其他的物件参考它 (换言之, 它们实际上不存在) . 一
旦一个物件被参考到, 它会被载入记忆体中, 并且它所有的变数都是零. 精简模

式 mud  呼叫此物件的 reset()  而原始模式 mud  呼叫 create()  (如果此物

件有这些函式的话 ), 让这些函式来指定一些变数的初始值. 物件中的其他函式

由 driver 或游戏中其他的物件使用之, 让物件之间达到互动并处理 LPC  变数


reset() 和 create() 的说明:

只有原始模式的 mud 使用 create() (请见本手册的 Introduction 一章, 有关

原始模式和精简模式的介绍). 此函式仅用来初始化刚被参考的物件.

原始模式及精简模式的 mud 都使用 reset() 函式. 在精简模式 mud  中, 
reset() 有两个功能. 第一, 它用来初始化刚被参考的物件. 第二, 在精简模式

的 mud  中, reset() 用来重新设定物件. 也就是说, 让物件回到最初的状态. 

这样可以让一个房间内的怪物重生, 或把一道门关回去......以此类推. 原始模

式的 mud  只用 reset()  作第二种功能 (就跟 reset  的意思一样). 

所以在 LP 式的 mud  中有两件重要的事情让 driver 呼叫物件中的函式. 第一

件事是创造物件. 此时, driver  呼叫物件中的一个函式来初始化物件的变数值

. 在精简模式的 mud  里, 由 reset()  做此工作 (要加上 0  参数, 后面的章

节再讨论参数是啥). 原始模式的 mud  下, 由 create() 做此工作.

第二件事是把房间重新设定回某些基本的状况. 这些基本的设定可能会与一开始

的初始值不同, 也可能相同, 而你当然也不想花时间一直去重覆做某些事 (像是

重新设定一些不会更改的变数) . 精简模式的 mud  用 reset()  函式来创造和

重新设定物件. 而原始模式的 mud  用 create() 创造物件, 用 reset()  重新

设定物件. 但是精简模式也不会失去所有的变数值, 因为有个方法能区分是创造

物件还是重新设定物件. 在精简模式要重新设定, 则 driver 传入 1  或重新设

定的数字当作 reset()  的参数. 现在这个对你来说没啥意义, 但是要记住, 你

在精简模式实际上是可以把两种情形区分开来的. 另外也要记住, reset() 在创

造物件时传入的参数是 0, 而重新设定物件时传入非零值.

基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 17 june 1993

第三章: LPC  的资料型态 (data type)

3.1 你现在该知道的事

LPC 物件由零个或多个变数组合而成, 而这些变数由一个或多个函式组合而成. 

在程式码中, 这些函式的先后顺序是无关紧要的. 当你写的 LPC  第一次被参考

时, driver  把它复制一份到记忆体中. 之后, 还可藉此复制出更多相同的拷贝

任何一份物件被载入记忆体时, 所有的变数一开始都指向「虚无值」. 精简模式

mud 的 reset()  函式与原始模式的 create() 函式都都用于指定物件的初始变

数值. 物件载入记忆体之后, 会立刻呼叫创造的函式. 不过, 如果你读这份课本

之前没有写过程式, 你大概不知道什么是函式 (function) , 或函式是怎么被呼

叫的. 就算你以前写过程式, 你大概也想知道新创造的物件中, 函式之间互相呼

叫对方的过程是什么. 回答以上这些问题以前, 你得多了解函式在处理什么. 所

以你应该先彻底了解 LPC  资料型态背后的观念. 说实在的, 在这份手册里头最

无聊的主题, 也是最重要的主题, 90% 以上就是用错 LPC  资料型态 (放错 {}

和 () 不算在内).  所以说, 你得要耐心看完非常重要的这一章, 因为我觉得你

如果搞懂这一章, 可以让你以后写程式大大轻松不少.

3.2 与电脑沟通

你应该已经知道电脑不懂人类所使用的单字与数字. 电脑所说的「语言」由 0 

与 1  的「字母」所组合而成. 当然, 你知道电脑不懂人类的自然语言. 但是实

际上, 它们也不懂我们写给它们的电脑语言. 像是 BASIC、C、C++、Pascal 等等

, 这些电脑语言全都是过渡语言. 这些电脑语言让你能把想法组织起来, 让思考

更易转换成电脑的 0  与 1  语言.

转换有两个方法: 编译 (compilation)  和直译 (interpretation) . 这两个方

法的差别在于程式语言转换成真正电脑语言的时候. 对编译的程式语言来说, 程

式设计者撰写程式码之后, 使用编译程式 (compiler) 把程式码转换成电脑真正

的语言. 程式在执行之前就已经转换完毕. 而直译的程式语言, 在程式执行的时

候才开始转换. 因此直译的程式语言所写的程式执行起来要比编译的慢上许多.


总而言之, 不管你用什么程式语言撰写程式, 最后都要转变成 0  与 1  才能让

电脑搞懂. 但是你储存在记忆体中的变数并不是单纯的 0  与 1. 所以你用的程

式语言要有个方法告诉电脑, 这些 0  和 1  到底要当作十进位数字、字元
 (characters) 、字串 (string) 、还是当作其他的东西看待. 你可以靠著指定

资料型态来办到.

举例来说, 假设你有个变数叫做 x ,  而你给它一个十进位的值 ── 65. 在
LPC 里面, 你会写出下面的叙述:

-----
x = 65;
-----

你等一下再做像这样的事:

_____
write(x+"\n");        /* \n 符号代表在此换行 (carriage return) */
y = x + 5;
-----

第一行让你送出 65 和换行到某个人的萤幕上. 第二行让你把 y  设定为 70.  

问题是你告诉电脑 x = 65; 时,  它不知道 65 到底是啥意思. 你认为是 65,  

对电脑来说也许认为是:
00000000000000000000000001000001
而且, 对电脑来说, A 这个字母就是:
00000000000000000000000001000001
所以, 不管你什么时候告诉电脑 write(x+"\n");,  电脑总要有个方法知道你想

看到 65 而不是 A.

电脑能透过资料型态了解 65 与 A  的不同. 资料型态只是说记忆体位置中储存

的指定变数到底是属于什么型态的资料. 所以说, 每一个 LPC  变数都有变数型

态指导如何转换资料. 在上面的范例里, 你应该会在程式码「之前」加上以下这

行:

-----
int x;
-----

这一行告诉 driver 无论 x  指向何处, 都当作「int」 资料型态来使用. int 

是整数 (interger, 或称 whole number)  的缩写. 现在我们已经初步介绍为什

么要有资料型态. 这样一来, driver  才能搞清楚电脑储存在记忆体中的 0  与

1 到底是代表什么意义. 

3.3 LPC 的资料型态

所有的 LPMud driver 都有以下的资料型态:

void (无), status  (状况), int (整数), string  (字串), object  (物件),
 
int *  (整数指标), string *  (字串指标), object *  (物件指标), 
mixed * (混合指标)

很多种 driver  (不是全部) 有下列资料型态值得讨论:

float  (浮点数), mapping  (映射), float *  (浮点数指标), 
mapping * (映射指标)

少数 driver 有下列罕用的资料型态, 并不值得讨论:

function (函式), enum, struct  (结构), char  (字元)

 (译注: 目前台湾绝大多数的 LPMud  所使用的 driver 是 MudOS, 其资料型态

有些许不同之处. 请详见参考译者所翻译之 MudOS  参考文件)

3.4 简单的资料型态

这份简介性质的课本会介绍 void, status, int, float, string, object, 
mixed 这几种资料型态. 你可以在中阶课本 (intermediate book,  译注: 本作

者另外有写一份中阶 LPC  手册, 译者亦有翻译) 找到像是 mapping (映射) 或

array (阵列)  这种更复杂的资料型态. 本章先介绍两种最简单的资料型态 (以

LPC 程式设计者的观点来看)  ── 整数 (int)  和字串 (string).

int 表示任何整数. 所以 1, 42, -17, 0, -10000023 都是整数 (int)  型态. 

string  是一个以上的字元或数字. 所以 "a", "we are borg", "42", 
"This is a string"  都是字串. 请注意, 字串前后都要加上双引号 "" , 
driver  才能分辨 int 42 和 string "42". 也才能区别变数名称 (像是 x )  

与字串 (像是 "x" ).

当你在程式码中使用变数, 你一开始要让 driver 知道这个变数所指的是哪种变

数型态. 这种处理方式叫做「宣告」 (declaration). 你得在函式一开始的地方

宣告, 或是在物件程式码的开头之处 (在函式之外, 任何函式用到该变数之前).

要宣告变数型态的话, 只要像底下一样, 把变数型态摆在变数的名字前便即可.


-----
void add_two_and_two() {
    int x;
    int y;

    x = 2;
    y = x + x;
}
-----

像这样, 这是一个完整的函式. 函式的名称是 add_two_and_two(). 函式一开始

宣告一个整数变数 x, 之后宣告一个整数变数 y. 所以, 在这里 driver 有两个

变数指向 NULL  (虚无) 值, 而这两个变数期待的变数值是整数型态.

关于虚无 (void) 和状态 (status) 资料型态:

无 (void) 是一种很普遍的资料型态, 它不指向任何东西. 它并不是用在变数上
面的
型态, 而是用于函式. 你稍后会了解这里所说的事. 而现在, 你只需要知道 voi
d
不指向任何值.

状况 (status) 资料型态是布林 (boolean)  资料型态. 就是说, 它的值是 0  

或 1. 这种值常常称为真 (true) 或伪 (false).

3.5 本章总结

对变数来说, driver  需要知道电脑储存在记忆体中的 0  与 1  要如何转换成

你想使用的形式. 最简单的 LPC  资料型态是 void, status, int, string. 变

数不使用 void 的资料型态, 但是这种资料型态用于函式. 另外, 资料型态用于

转换格式, 决定 driver 应该使用哪种规则处理运算, 像是 +, - ......以此类

推. 举例说, 运算式 (expression) 5+5, driver 知道 5  加上 5  的值是 10.

对字串来说, 对字串使用整数加法没有意义. 所以, "a"+"b" 把 "b"  加在 "a"

的后面, 最后得出 "ab". 当你试著把 "5"+5 就会产生错误. 因为把整数加上字

串是无意义的, 所以 driver 会把第二个 5  转换成 "5"  再加起来. 最后的结

果是 "55".  如果你想看的结果是 10 , 你最后只得到错误的程式码. 请记住,

大多数的情况下, driver  不会像前面这样产生 "55" 这种有用的结果. 它会产

生 "55" 是因为它早有一条规则处理整数加上字串的情况, 也就是把整数当成字

串看待. 在大多数的状况中, 如果你在运算式或函式中使用资料型态并没有事先

定义 (像是你试著把 "this is"  除以 "nonsense", "this is" / "nonsense")

, driver  会呕吐并回报错误给你.

基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 22 june 1993

第四章: 函式 (functions)

4.1 回顾

现在, 你应该了解 LPC  物件由许多处理变数的函式所组成. 函式执行时就处理

变数, 而经由「呼叫」执行这些函式. 在一个档案里, 函式之间的前后顺序是无

关紧要的. 变数在函式里面被处理, 变数储存在电脑的记忆体中, 而电脑把它们

当作 0  与 1  来处理. 利用定义资料型态这种方法, 这些 0  与 1  被转换成

可使用的输出及输入结果. 字串 (string) 资料型态告诉 driver , 让你看到或

你输入的资料应该是许多字元及数字的形式. 整数 (int)  型态的变数对你来说

就是整数值. 状况 (status) 型态对你来说就是 1  或 0. 无 (void) 资料型态

对你或对机器而言都没有值, 并不是用于变数上的资料型态.

4.2 什么是函式 ?

就像数学函式, LPC 函式获得输入值, 然后传回输出值. 像 Pascal 语言把程序

(procedure) 和函式 (function) 区分开来. 但是 LPC  不这样做, 而知道这种

区分也是有用的. Pascal  称为程序的东西, 在 LPC  就是无传回值 (void) 型

态的函式. 也就是说, 程序或无传回值函式没有传回输出值. Pascal  称为函式

的东西, 就是有传回输出值的. 在 LPC  里, 最短的正确函式是:

-----
void do_nothing() { }
-----

这个函式不接受输入, 没有任何指令, 也不传回任何值.

要写出正确的 LPC  函式有三个部分:
1)  宣告 (declaration)
2)  定义 (definition)
3)  呼叫 (call)

就像变数一样, 函式也要宣告. 这样一来, 让 driver 知道: 1)  函式输出的资

料是什么型态 2) 有多少个输入的资料以及它们的型态为何. 比较普通的讲法称

这些输入为参数 (parameter).
所以, 宣告一个函式的格式如下:
传回值型态  函式名称 (参数 1, 参数 2, ...,  参数 N);
底下宣告一个 drink_water()  的函式, 它接受一个字串输入, 而输出一个整数
:

-----
int drink_water(string str);
-----

str 是输入的变数名称, 会用于函式之中.

函式定义是描述函式实际上如何处理输入值的程式码. 

呼叫则是其他函式之中, 呼叫并执行此函式的地方. 对 write_vals() 和 add()

两个函式来说, 你可能会有这些程式码:

-----
/*  首先, 是函式宣告. 它们通常出现在物件码的开头.
*/
void write_vals();
int add(int x, int y);

/*  接著是定义 write_vals() 函式. 我们假设这函式将会在物件以外被呼叫.

*/
void write_vals() {
    int x;

    /*  现在我们指定 x  为呼叫 add()  的输出值. */
    x = add(2, 2);
    write(x+"\n");
}

/*  最后, 定义 add() */
int add(int x, int y) {
    return (x + y);
}
-----

请记得, 哪一个函式定义在前都没有关系. 这是因为函式并不是由前往后连续执

行的. 函式只有被呼叫时才会执行. 唯一的要求是, 一个函式的宣告必须出现在

函式的定义之前, 而且也必须在任何函式定义呼叫它之前.

4.3 外部函式 (efuns)

也许你已经听过有人提过外部函式. 它们是外部定义的函式. 跟名称一样, 它们

由 mud driver 所定义. 如果你已经撰写 LPC  程式码很久, 你大概已经发现你

听到的一些式子, 像是 this_player(), write(), say(), this_object()...  

等等, 看起来很像函式. 这是因为它们是外部函式. 外部函式的价值在于它们比

LPC 函式要快得多, 因为它们早已经以电脑了解的二进位格式存在著.

在前面的 write_vals() 函式里, 呼叫了两个函式. 第一个是 add()  函式, 是

你宣告及定义的函式. 第二个, 则是称做 write()  的外部函式. driver  早就

帮你宣告并定义这个函式. 你只需要呼叫它.

创造外部函式是为了处理普通的、每天都用得到的函式呼叫、处理 internet
socket  的输出与输入、其他用 LPC  难以处理的事. 它们是在 game driver 

内以 C  写成的, 并与 driver 一起编译在 mud  开始之前, 让它们执行起来快

得多. 但是对你来说, 外部函式呼叫就像对你的函式呼叫一样. 不过, 任何外部

函式还是要知道两件重要的事: 1)  它的传回值是什么, 2)  它要什么参数.

外部函式的详细资料, 像是输入参数和传回值, 常常可以在你的 mud 中的
/doc/efun 目录找到. 我没有办法在这里详细介绍外部函式, 因为每种 driver

的外部函式都不相同. 但是, 你常常可以藉由「man」 或「help」指令 (视
mudlib  而定) 找到详细的资料. 例如指令「man write」 会给你 write  外部

函式的详细资料. 如果都不行, 「more /doc/efun/write」也可以.

看过 write  的详细资料之后, 你应该找到 write  是宣告成这样:

-----
void write(string);
-----

这样告诉你, 要正确呼叫 write  不应该期待它有传回值, 而且要传入一个字串

型态的参数.

4.4 定义你自己的函式

虽然在档案中, 你的函式次序谁先谁后都没有关系, 但是定义一个函式的程式码

的先后顺序就非常重要. 当一个函式被呼叫时, 函式定义中的程式码按照出现的

先后顺序执行. 先前的 write_vals() 中, 这个指令:
    
-----
x = add(2, 2);
-----

如果你想看到 write()  使用正确的 x  值, 就必须把它放在 write()  呼叫之
前.

当函式要传回一个值时, 由「return」指令之后跟著与函式相同资料型态的值所

完成. 在先前的 add()  之中, 指令「return (x+y);」 把 (x+y)  的值传回给

write_vals()  并指定给 x. 在更普通的层次上来说, 「return」停止执行函式

, 并传回程式码执行的结果给呼叫此函式的函式. 另外, 它将跟在它后面任何式

子的值传回呼叫的函式. 要停止执行失去控制的无传回值函式, 使用 return;
而后面不用加上任何东西. 请再次记得, 使用「return」传回任何式子的资料型

态「必须」与函式本身的资料型态相符合.

4.5 本章总结

定义 LPC  物件的档案是由函式所组成的. 函式依次由三个部分组成:
    1)  宣告
    2)  定义
    3)  呼叫
函式宣告通常出现在档案的最前面, 在任何定义之前. 不过函式只要求在函式定

义之前以及任何函式呼叫它之前宣告它.
函式定义可以任何顺序出现在档案里, 只要它们都放在宣告之后. 另外, 你不可

以再一个函式里面定义另一个函式.
函式呼叫则出现在其他任何函式中, 任何程式码想执行你的函式的地方. 呼叫也

可以出现在自己的函式定义中, 但是这种做法并不建议给新手去做, 因为它很容

易变成无穷回圈.

函式定义依序由底下的部分所组成:
    1)  函式传回值型态
    2)  函式名称
    3)  一个左小括号 (  接著列出参数再加上一个右小括号 )
    4)  一个左大括号 {  指示 driver 从这里开始执行
    5)  宣告只用在这个函式中的任何变数
    6)  指令、式子、视需要呼叫其他函式
    7)  一个右大括号 }  描述函式码在此结束. 对于无传回值函式来说, 如果

        在此还没有碰到「return」指令 (只适用于无传回值函式) , 会如同有

        碰到「return」指令一样回到原来呼叫的函式执行.

最短的函式是:

-----
void do_nothing() {}
-----

因为这个函式不接受任何输入, 不做任何事, 也不传回任何输出.

任何无传回值型态以外的函式「必须」传回一个与函式资料型态相同的值.

每一种 driver 都有一套早已经帮你定义好的函式, 它们叫做外部函式. 你不需

要宣告或定义它们, 因为它们早已经帮你做好这些事. 更深入一点, 执行这些函

式比起执行你的函式要快得多, 因为外部函式是 driver 的一部份. 再者, 每一

个 mudlib 都有特殊函式像是外部函式一样, 早已经为你宣告并定义好. 但是不

同的是, 它们用 LPC  定义在 mudlib 里面. 它们叫做模拟外部函式
(simul_efuns, 或 simulated efuns). 在大多数的 mud  里, 你可以在
/doc/efun 目录底下找到关于它们的详细资料. 另外, 很多 mud  有称作
「man 」或「help」的命令, 让你可以方便地叫出这些资料档案.

程式风格的注解:

有些 driver 可能不会要求你宣告函式, 有些不会要求你指定函式的传回值型态

无论如何, 底下有两个理由劝你不要省略以上这些动作:
    1)  对其他人来说 (还有你自己过了一段时间之后) , 会比较容易读懂你的

        程式码并了解程式码的意义. 这对除错时特别有用, 有很多错误 (除了

        放错地方的各种括号) 发生在资料型态上 (有没有碰过「Bad arg 1 to

         foo() line 32」? (程式第三十二行, 呼叫 foo() 时的第二个参数有
错) ).
    2)  大家认为这样子写程式是个好习惯.

基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 01 july 1993

第五章: 基础的继承 (inheritance)

5.1 回顾

你现在应该了解函式基本的功能. 你应该可以宣告并呼叫一个函式. 另外, 你应

该能认识函式定义, 虽然你可能是第一次接触 LPC. 你现在并不见得能定义你自

己的函式. 函式是 LPC  物件的基石. 函式中的程式码, 要别的函式呼叫它们的

时候才会执行. 呼叫一个函式时, 作出呼叫的函式要给它输入值, 才能执行被呼

叫的函式. 被呼叫的函式执行其程式码, 并传回某种资料型态的传回值给呼叫它

的函式. 没有传回值的函式属于无传回值 (void) 型态.

仔细看过你自己的工作室程式码之后, 它看起来大概像这样 (视 mudlib 而定):


-----
inherit "/std/room";

void create() {
    ::create();
    set_property("light", 2);
    set_property("indoors", 1);
    set("short", "Descartes 的工作室");
    set("long", "此处是 Descartes 工作的地方.\n这里是一个立方体.\n");

    set_exits( ({ "/d/standard/square" }), ({ "square" }) );
}
-----

如果你到目前为止, 所有的课本内容都了解的话, 你应该能认出以下的程式码:

    1)  create()  是函式的定义. (嘿 !   他没有宣告它)
    2)  它呼叫 set_property() 、set()、set_exits(), 没有一个函式在这段

        程式码中曾有宣告或定义.
    3)  最上面有一行, 不是宣告变数或函式, 也不是函式定义 !

这一章会找出这些问题的解答, 你现在应该脑中应该有这些问题:
    1)  为什么没有宣告 create() ?
    2)  为什么 set_property() 、set() 、set_exits() 已经宣告并定义过了
 ?
    3)  档案最上面那一行到底是啥东西 ?

5.2 物件导向程式设计 (object oriented programming, OOP)

继承 (inheritance)  是定义真正物件导向程式设计的特性之一. 它让你创造通

用的程式码, 能以多种用途用于许多不同的程式中. 一个 mudlib 所作的, 就是

创造这些通用的档案 (物件) , 让你用来制造特定物件.

如果你必须把定义前面工作室全部所需要的程式码写出来, 你大概必须要写 100
0
行程式码才能得到一个房间所有的功能. 当然, 那根本是浪费磁碟空间. 再者, 

这种程式码与玩家和其他房间的互动性很差, 因为每一个创造者都写出自己的函

式以作出一个房间的功能. 所以, 你可能使用 query_long() 写出房间的长叙述

, 其他的巫师可能使用 long() . 这就是 mudlib 彼此不相容最主要的原因, 因

为它们使用不同的物件互动协定.

OOP 克服了这些问题. 前面的工作室中, 你继承已经定义在 "/std/room.c" 档案

中的函式. 它拥有普通房间所需要的全部函式定义其中. 当你要制造一个特定的

房间, 你拿这个房间档案中定义好的通用函式功能, 并加上你自己的函式
 create() 以制造一个独特的房间.

5.3 继承如何作用

你现在大概猜得出来, 这一行:

-----
inherit "/std/room";
-----

让你继承 "std/room.c" 的函式功能. 藉由继承函式功能, 它代表你可以使用
"/std/room.c" 里面已经宣告并定义好的函式. 在 Nightmare Mudlib 中, 
"/std/room.c" 里面有许多函式, 其中有 set_property() 、set() 、
set_exits() 函式, 都已经宣告并定义过. 在你的 creat()  函式里, 你呼叫那

些函式来设定你房间一开始的值. 这些值让你的房间不同于别的房间, 却保留与

记忆体中其他房间互动的能力.

实际的写作中, 每一个 mudlib 都不同, 所以要你使用不同一套的标准函式来达

到相同的功能. 说明有哪些函式存在和它们是作什么用的, 已经超出了这本课本

的范围. 如果你的 mudlib 有自己详细的说明资料, 你会找到教你如何使用各种

继承档案的说明文件以创造物件. 这些说明应该会告诉你有哪些函式、它们需要

哪些输入、它们输出的资料型态、以及它们的功能.

5.4 本章总结

本章距离完整解释继承如此复杂的主题还有一大段距离. 本文的目的只是让你能

了解如何使用继承来创造你的物件. 以后的课本将对此会有完整的讨论. 现在你

应该已经了解底下几点:
    1)  每一个 mudlib 都有一套通用物件库, 有它们自己的通用函式. 创造者

透过继承使用它们, 让撰写物件程式码这件工作更轻松, 并与其他物件之间能良

好互动.
    2)  可被继承的档案里头的函式, 每个 mudlib 都不一样. 你的 mud  里应

        该有说明文件解释如何使用这些可被继承的档案. 如果你还不知道有哪

        些函式可用, 那你就没有办法用它们. 任何时候, 都请你特别注意输入

        和输出的资料型态.
    3)  你藉由底下这行继承函式的功能:

-----
inherit "filename";
-----
       
filename  是被继承的物件档案名称. 这行放在你程式码的开头. 

注解:

你可能看到有几处地方有 ::create() 或 ::init() 或 ::reset()  语法. 你现

在不需要完全了解这个, 但是应该告诉你一点线索, 知道它到底是什么. 「::」

运算子是一种特殊的方法来呼叫继承物件的函式 (叫做范围解析运算子 scope
 resolution operator). 例如, 大多数 mud  的 room.c 都有叫做 create()  

函式. 当你继承 room.c 并设定 create() 时, 你所作的事称为超越 (override
)
 room.c 的 create() 函式. 这表示不管任何东西呼叫你房间的 create() , 它

会呼叫「你的」版本, 而不是 room.c 里面的那一个. :: 运算子让你能呼叫
 room.c 里的 create() 而不是你的 create().

一个例子:

-----
#1

inherit "/std/room";

void create() { create(); }
-----

-----
#2

inherit "/std/room";

void create() { ::create(); }
-----

第一个例子是个恐怖的例子. 当它被载入时, driver  呼叫 create() , 之后
 create() 再呼叫 create(), create() 又呼叫 create(), 这时 create()  又

呼叫 create()......换句话说, 所有的 create()  就一直呼叫自己直到 drive
r
侦测到太深的递回 (recursion) 并跳出来.

第二个例子基本上只是浪费记忆体, 它的功能跟 room.c 没有两样. 对它而言,

 driver 先呼叫它的 room.c , 然后呼叫 ::create() , 也就是 room.c 里的
 create() . 其他的地方就跟 room.c 的功能一样.

基础 LPC 作者: Descartes of Borg 第一版: 23 april 1993 第二版: j
uly 5 1993
第六章: 变数 (variable) 处理6.1 回顾现在你应该能利用你 mud  的标准物件
库, 撰写一些简单的物件. 继承能让你使
用那些物件中已经定义好的函式, 而不用自己去定义. 另外, 你应该知道如何宣
告你自己的函式. 这一章将教你 LPC  的基本元素, 让你能藉由处理变数来定义

你自己的函式.6.2 数值与物件基本上, mud 里头的物件都不一样的原因有两个:
1)  有的物件拥有不同的函式2)  所有的物件都有不同的数值
现在, 所有的玩家物件都有同样的函式. 它们不一样的地方在于它们自己所拥有

的数值不同. 举例来说, 名字叫做「Forlock」的玩家跟「Descartes」「至少」

他们各自的 true_name  变数值不同, 一个是 "descartes", 另一个是 "forloc
k".
所以, 游戏中的改变伴随著游戏中物件值的改变. 函式名称就是用来处理变数的
过程名称. 例如说, create()  函式就是特别用来初始化一个物件的过程. 函式

之中, 有些特别的事称为指令. 指令就是负责处理变数的.6.3 区域 (local) 和
全域 (global) 变数
跟大多数程式设计语言的变数一样, LPC 变数可以宣告为一个特定函式的「区域
」变数, 或是所有函式可以使用的「全域」变数. 区域变数宣告在使用它们的函

式之内. 其他函式并不知道它们存在, 因为这些值只有在那个函式执行时才储存
在记忆体中. 物件码宣告全域变数之后, 则让后面所有的函式都能使用它. 因为

只要物件存在, 全域变数就会占据记忆体. 你只有在整个物件中都需要某个值的
时候, 才要用全域变数. 看看下面两段程式码:-----int x;
int query_x() { return x; }void set_x(int y) { x = y; }----------
void set_x(int y) {    int x;    x = y;    write("x 设定为 "+x+" 并且会
消失无踪.\n");}
-----第一个例子里, x 宣告在所有的函式之外, 所以在 x  宣告之后的所有函式
都能使用它. x 在此是全域变数.
第二个例子中, x 宣告在 set_x()  函式里. 它只有在 set_x()  执行的时候存
在. 之后, 它会消失. 在此, x 是区域变数.
6.4 处理变数的值给 driver 的指令 (instruction)  用来处理变数值. 一个指
令的范例是:-----x = 5;-----
上面的指令很清楚. 它把 5  这个数值指定给 x  变数. 不过, 这个指令牵涉到

一些对普通指令来说很重要的观念. 第一个观念是运算式 (expression).一个运
算式就是有值的一系列符号. 在上面的指令中, 运算式 5  的值指定给变
数 x. 常数 (constant) 是最简单的运算式. 一个常数就是不变的值, 像是整数

 5  或是字串 "hello". 最后一个观念就是运算子 (operator).  在上面的例子

中, 使用了 =  这个指定运算 (assignment operator).在 LPC  有更多其他的运
算子, 还有更复杂的运算式. 如果我们进入一个更复杂
的层次, 我们得到:-----y = 5;x = y +2;-----第一个指令使用指定运算子以指
定常数运算式 5  的值给变数 y. 第二个指令把
 (y+2)  的值以加法运算子把 y  和常数运算式 2  加起来, 再用指定运算子指
定给 x. 听起来一点意义都没有吧 ?
换另一种方法来讲, 使用多个运算子可以组成复杂的运算式. 在前面的范例中,一
个指令 x = y + 2; 里面含有两个运算式:    1)  运算式 y+2
    2)  运算式 x = y + 2前面曾提过, 所有的运算是都有其值. 运算式 y+2  
的值是 y  和 2  的总和
 (在此是 7) ;  运算式 x = y + 2 「也」有其值 ── 7.所以运算子有两个重
要的工作:    1)  它们「可以」像函式一样当作输入.
    2)  它们运算起来就像本身有值一样.现在, 不是所有的运算子的功能都像 
1) 一样. = 运算子将它右边的值指定给 x.
但是 +  就没有这种功能. 而且, 它们两个也有自己的值.6.5 复杂的运算式
前面你大概已经注意到, 运算式 x = 5  「本身」也有个值是 5. 实际上, 因为

LPC 运算子如同运算式一样也有自己的值, 它们能让你写出一些非常难解、看起
来毫无意义的东西, 像是: 
    i = ( (x=sizeof(tmp=users())) ? --x : sizeof(tmp=children("/std/mo
nster"))-1)
基本上只是说:    把外部函式 users()  传回的阵列指定给 tmp, 然后把此阵列
元素的数目指
    定给 x. 如果指定给 x  的运算式值为真 (不是 0) , 就指定 x  为 1  并

    指定 i  的值为 x-1  的值. 如果 x  为伪, 则设定 tmp  为外部函式
     children() 传回的阵列, 并指定 i  为阵列 tmp  的元素数目再减 1.
你曾经用过以上的叙述吗 ?  我很怀疑. 不过你可能看过或使用与它相似的运算
式, 因为一次合并这么多的东西在一行里面, 能提升你程式码的执行速度. 比较

常使用 LPC  运算子这种特性的写法大概像这样:    x = sizeof(tmp = users(
));
    while(i--) write((string)tmp[i]->query_name()+"\n");取代这样子的写
法:
    tmp = users();    x = sizeof(tmp);    for(i=0; iquery_name()+"\n")
;
像是 for()、while() 、阵列......等等东西稍后会解释.不过第一段程式码比较
简洁, 执行起来也比较快.
附注: 在本章总结之后会对所有的 LPC  运算子有更详细的说明.6.6 本章总结你
目前知道如何宣告变数, 并了解宣告、使用全域和区域变数之间的不同. 一旦
你熟悉你 driver 的外部函式, 你就能用许多不同的方法显示那些值. 另外, 藉

由 LPC  运算子, 你知道怎么改变并运算变数里头的值. 这当然对你很有用, 因
为它让你能做一些事, 像是算出从树上摘下了多少颗苹果, 一旦苹果都摘完了, 

就没有人有苹果可摘. 很不幸, 你现在只会写寥寥几行能执行的程式. 换句话说
, 到下一章以前先别管苹果的问题, 因为你还不知道如何检查全部摘下的苹果数

目和树上原先的苹果数目是否相等. 你也不知道特殊的函式 init(),  能让你给
玩家使用新的指令. 但是你已经准备好撰写良好而复杂的区域程式码.
6.7 LPC  运算子这一段将详细列出比较简单的 LPC  运算子, 包括对它们使用的
值所作的事 (如果有值的话), 以及它们自己拥有的值.
在此说明的运算子有:=    +    -    *    /    %    +=    -=    *=    /= 
   %=
--    ++    ==    !=    >    < >=    <= ! && ||->    ? :
下面, 这些运算子将全部用相当简单的方式说明之, 但是你最好把每个运算子至
少都看过一次, 因为有些运算子的功能「不见得」如你所想的一样

--
※ 修改:.zhangsf 于 Sep 24 22:03:09 修改本文.[FROM: 202.100.198.52]
※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.100.198.52]

[关闭][返回]