秋林拾叶【mud.Gameivy.COM】 >> 论坛 >> ┈┋MUD 交流区┋┈ >> 梦幻泥潭 >> LPC教学手册
LPC教学手册 ivy,2007-04-12 17:41:06

[B]LPC的环境需求  [/B]

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基本简介 ivy,2007-04-12 17:43:36

这一章的名字取得不怎麼好, 因為没有人用 LPC 写程式. 写 LPC 程式的人写的是物件 (objects). 这两种说法有啥差别 ? 好吧, 就我们现在的目标来说, 差别在於两者档案执行的方式不同. 当你「跑」一个程式的时候, 都是从程式中固定的地方开始执行. 换句话说, 就是所有的程式开始执行的时候, 一定有个地方写清楚要从那裡开始. 另外, 程式有一个固定的终止点, 所以执行程式只要执行到该终止点, 程式就中止执行. 总之, 程式从固定的开头跑到固定的结尾. LPC 物件就不是这麼一回事.

在 mud 裡面, LPC 物件只是游戏 (driver) C 程式中, 显而易见的部分. 换句话说, mud 程式在 driver 裡面开始与结束执行. 但是实际上, 对於创造你玩的 mud 世界来说, driver 并没有做多少事. 反之, driver 相当依赖 LPC 码, 并需要执行物件中的程式码. 所以 LPC 物件不需要有起始点, 也不需要有固定的终止点.

就像其他的程式语言, LPC 「程式」可以由一个或一个以上的档案组成. 很简单, 程式要先载入 driver 的记忆体. driver 会根据本手册所教的结构, 读取物件中一行行的程式. 有一件重要的事你要先搞清楚, 就是 LPC 物件执行时没有开头也没有终止.

2.2 driver-mudlib 之间的互动

我先前提过, driver 是在主机上执行的 C 程式. 它让你连上游戏, 并执行 LPC 码. 注意, 这是 mud 程式设计的一个理论而已, 也不需要比其他的方法好. 整个 mud 游戏可以全部用 C 来写. 这样游戏的执行速度快上很多, 却让 mud 缺乏可塑性, 使巫师在游戏正在执行的时候无法加入新东西. DikuMUD 就是全部用 C 写成的. 相反的, LPMUD 的理论就是 driver 不该决定游戏内容, 而游戏内容应该决定於游戏中的个别事物, 并能够在游戏执行时加上东西. 这就是為什麼 LPMUD 使用 LPC 程式语言. 它能让你用 LPC 定义游戏内容, 交给 driver 依需要读取并执行. 况且学 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 撰写的物件被其他在记忆体的物件拿来参考时, driver 就寻找这物件裡面所要找的值在哪裡 (但是现在还没有任何数值) . 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的资料型态 ivy,2007-04-12 17:45:51

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) 是一种很普遍的资料型态, 它不指向任何东西. 它并不是用在变数上面的 型态, 而是用於函式. 你稍后会了解这裡所说的事. 而现在, 你只需要知道 void 不指向任何值.

状况 (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的函式 ivy,2007-04-12 17:47:08

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的基础继承 ivy,2007-04-12 17:48:56

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 所作的, 就是 创造这些通用的档案 (物件) , 让你用来製造特定物件.

如果你必须把定义前面工作室全部所需要的程式码写出来, 你大概必须要写 1000 行程式码才能得到一个房间所有的功能. 当然, 那根本是浪费磁碟空间. 再者, 这种程式码与玩家和其他房间的互动性很差, 因為每一个创造者都写出自己的函式以作出一个房间的功能. 所以, 你可能使用 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() 就一直呼叫自己直到 driver 侦测到太深的递迴 (recursion) 并跳出来.

第二个例子基本上只是浪费记忆体, 它的功能跟 room.c 没有两样. 对它而言, driver 先呼叫它的 room.c , 然后呼叫 ::create() , 也就是 room.c 裡的 create() . 其他的地方就跟 room.c 的功能一样.

LPC的变数处理 ivy,2007-04-12 17:49:51

6.1 回顾

现在你应该能利用你 mud 的标準物件库, 撰写一些简单的物件. 继承能让你使
用那些物件中已经定义好的函式, 而不用自己去定义. 另外, 你应该知道如何宣
告你自己的函式. 这一章将教你 LPC 的基本元素, 让你能藉由处理变数来定义
你自己的函式.

6.2 数值与物件

基本上, mud 裡头的物件都不一样的原因有两个:

1) 有的物件拥有不同的函式
2) 所有的物件都有不同的数值

现在, 所有的玩家物件都有同样的函式. 它们不一样的地方在於它们自己所拥有
的数值不同. 举例来说, 名字叫做「Forlock」的玩家跟「Descartes」「至少」
他们各自的 true_name 变数值不同, 一个是 "descartes", 另一个是 "forlock".

所以, 游戏中的改变伴随著游戏中物件值的改变. 函式名称就是用来处理变数的
过程名称. 例如说, 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/monster"))-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; i<x; i++) write((string)tmp[i]->query_name()+"\n");
像是 for()、while() 、阵列......等等东西稍后会解释.
不过第一段程式码比较简洁, 执行起来也比较快.

附註: 在本章总结之后会对所有的 LPC 运算子有更详细的说明.

6.6 本章总结

你目前知道如何宣告变数, 并了解宣告、使用全域和区域变数之间的不同. 一旦
你熟悉你 driver 的外部函式, 你就能用许多不同的方法显示那些值. 另外, 藉
由 LPC 运算子, 你知道怎麼改变并运算变数裡头的值. 这当然对你很有用, 因
為它让你能做一些事, 像是算出从树上摘下了多少颗苹果, 一旦苹果都摘完了,
就没有人有苹果可摘. 很不幸, 你现在只会写寥寥几行能执行的程式. 换句话说
, 到下一章以前先别管苹果的问题, 因為你还不知道如何检查全部摘下的苹果数
目和树上原先的苹果数目是否相等. 你也不知道特殊的函式 init(), 能让你给
玩家使用新的指令. 但是你已经準备好撰写良好而复杂的区域程式码.

6.7 LPC 运算子

这一段将详细列出比较简单的 LPC 运算子, 包括对它们使用的值所作的事 (如
果有值的话), 以及它们自己拥有的值.

在此说明的运算子有:
= + - * / % += -= *= /= %=
-- ++ == != > < >= <= ! && ||
-> ? :

下面, 这些运算子将全部用相当简单的方式说明之, 但是你最好把每个运算子至
少都看过一次, 因為有些运算子的功能「不见得」如你所想的一样. 不过, 这段
说明可以当作相当好的一个参考.

= 指定运算子 (assignment operator):
范例: x = 5;
值: 在完成它的功能之后, 「左边」的变数值
说明: 把它「右边」任何运算式的值指定给它「左边」的变数. 注意, 你只
能於左边使用一个变数, 也不能指定给常数或复杂的运算式.

+ 加法运算子 (addition operator):
范例: x + 7
值: 左边值加上右边值的总和
说明: 把右边运算式的值加上左边运算式的值. 对整数 (int) 型态值来说
, 就表示数值总和. 对字串 (string) 来说, 表示右边的值接在左边
的值后面 ("a"+"b" 的值是 "ab"). 这个运算子不改变任何原始值 (
即变数 x 维持原来的值).

- 减法运算子 (subtraction operator):
范例: x - 7
值: 左边运算式的值减去右边的
解释: 除了它是减法以外, 与加法的特性相同.
字串: "ab" - "b" 的值是 "a".

* 乘法运算子 (multiplication operator):
范例: x*7
值与说明: 除了这个作数学乘法之外, 与加法、减法相同.

/ 除法运算子 (division operator):
范例: x/7
值与说明: 同上

+= 加法指定运算子(additive assignment operator):
范例: x += 5
值: 与 x + 5 相同
说明: 它把左边的变数值和右边的运算式值加起来, 把总和指定给左边的变
数.
例如: 如果 x = 2... x += 5 指定 7 值给变数 x. 整个运算式的值是 7.

-= 减法指定运算子 (subtraction assignment operator):
范例: x-=7
值: 左边的值减去右边的值.
说明: 除了减法以外, 与 += 相同.

*= 乘法指定运算子 (multiplicative assignment operator):
范例: x *= 7
值: 左边的值乘上右边的.
说明: 除了乘法以外, 与 -= 和 += 相似.

/= 除法指定运算子 (division assignment operator):
范例: x /= 7
值: 左边变数的值除以右边的值.
说明: 除了除法以外, 同上.

++ 后/前增加运算子 (post/pre-increment operators):
范例: i++ 或 ++i
值:
i++ 的值是 i
++i 的值是 i+1
说明: ++ 改变 i 的值, 将 i 加上 1. 但是, 运算式本身的值是多少,
要看你把 ++ 摆在哪裡. ++i 是前增加运算子. 这表示它的增加在给
予值「之前」. i++ 是后增加运算子. 它计算在 i 增加之前. 重点在
哪 ? 好, 目前这对你来说无关紧要, 但是你应该记住它代表的意思.

-- 后/前减少运算子 (post/pre-decrement operators):
范例: i-- 或 --i
值:
i-- 的值是 i
--i 的值是 i 减掉 1
说明: 除了是减法以外, 就像 ++

== 相等运算子 (equality operator):
范例: x == 5
值: 真或偽 (非 0 或 0)
说明: 它不更改任何值, 但是
如果两个值相等就传回真.
如果两边不相等则传回偽.

!= 不等运算子 (inequality operator):
范例: x != 5
值: 真或偽
说明: 如果左边的运算式不等於右边的运算式就传回真. 如果它们相等则传
回偽.

> 大於运算子 (greater than operator):
范例: x > 5
值: 真或偽
说明: 只有在 x 大於 5 时為真
如果相等或小於就為偽

< 小於运算子 (less than operator)
>= 大於或等於运算子 (greater than or equal to operator)
<= 小於或等於运算子 (less than or equal to operator):
范例: x < y x >= y x <= y
值: 真或偽
说明: 与 > 相似, 除了
< 如果左边小於右边就為真
>= 如果左边大於「或等於」右边则為真
<= 如果左边小於「或等於」右边就為真

&& 逻辑与运算子 (logical and operator)
|| 逻辑或运算子 (logical or operator):
范例: x && y x || y
值: 真或偽
说明: 如果右边的值和左边的值是非零值, && 為真.
如果任何一边是偽, 则 && 為偽.
对 || 来说, 只要两边任何一个值是真, 则為真. 只有两边都是偽值
时, 才為偽.

! 否定运算子 (negation operator)
范例: !x
值: 真或偽
说明: 如果 x 為真, 则 !x 為偽
如果 x 為偽, !x 就為真.

底下有两个更复杂的运算子, 在此為了存在而存在. 如果它们让你一头雾水也别
掛心.

-> 呼叫运算子 (the call other operator)
范例: this_player()->query_name()
值: 被呼叫函式的传回值
说明: 它呼叫右边这个函式, 而这个函式位於运算子左边的物件之内. 左边
的运算式「必须」是一个物件, 而右边的运算式「必须」是函式的名
字. 如果物件之中没有这个函式, 它会传回 0 (更精确一点, 没有定
义 (undefined) ).

? : 条件运算子 (conditional operator)
范例: x ? y : z
值: 上面的例子裡, 如果 x 為真, 其值為 y
如果 x 為偽, 其值為运算式 z
说明: 如果最左边的值為真, 这整个运算式的值就是中间的运算式. 不然,
就把整个运算式的值定為最右边的运算式.

相等 (equality) 的註解:

大家所犯的一种很难除错、很糟糕的错误是把该写 == 的地方写成 =. 因為运算
子有它的传回值, 这两种情况都能进行计算. 换句话讲, 这情形不会產生错误讯
息. 但是这两者的值大不相同. 例如:
if(x == 5) if(x = 5)
如果 x 是 5, 则其值為真. 反之则否.
x = 5 的值為 5 (所以它永远為真).
if 叙述会判断 () 之中的运算式是真还是偽, 所以如果你把 = 错当成 == ,
你就会得到永远為真的运算式. 你会扯掉许多根头髮, 也搞不清楚到底是為什麼
出错 :)

LPC的流程控制 ivy,2007-04-12 17:52:24

7.1 回顾变数

藉由 =、+=、-=、++、-- 等运算式, 可以指定或更改变数的值. 这些运算式可
以与 -、+ 、* 、/ 、% 结合使用. 但是, 到目前為止, 我们只告诉你如何用函
式, 以线性的方式写出这些. 例如:

int hello(int x) {
x--;
write("嗨, x 是 "+x+".\n");
return x;
}

你应该知道怎麼写出这个函式并了解它. 不过, 如果你只想於 x = 1 时显示
x 的值怎麼办 ? 不然, 如果你想在传回 x 之前, 一直显示出 x 的值直到
x = 1 又要怎麼做 ? LPC 使用的流程控制与 C 和 C++ 并无二致.

7.2 LPC 流程控制叙述

if(运算式) 指令;

if(运算式) 指令;
else 指令;

if(运算式) 指令;
else if(运算式) 指令;
else 指令

while(运算式) 指令;

do { 指令; } while(运算式);

switch(运算式) {
case (运算式): 指令; break;
default: 指令;
}

我们讨论这些东西之前, 先谈一下什麼是运算式和指令. 运算式是任何有值的东
西, 像是变数、比较式 (像 x > 5, 如果 x 是 6 或 6 以上, 则其值為 1,
不然其值為 0) 、指定式 (像 x += 2). 而指令是任何一行单独的 LPC 码, 像
是函式呼叫、值指定式 (value assignment) 、值修改式 (value modification) ......等等.

你也应该知道 && 、||、==、!=、! 这些运算子. 它们是逻辑运算子. 当条件為
真时, 它们传回非零值, 為偽时则传回 0. 底下是运算式值的列表:

(1 && 1) 值: 1 (1 和 1)
(1 && 0) 值: 0 (1 和 0)
(1 || 0) 值: 1 (1 或 0)
(1 == 1) 值: 1 (1 等於 1)
(1 != 1) 值: 0 (1 不等於 1)
(!1) 值: 0 ( 非 1)
(!0) 值: 1 ( 非 0)

使用 && 的运算式中, 如果要比较的第一项测试值為 0, 则第二项永远不会测试
之. 使用 || 时, 如果第一项為真 (1), 就不会测试第二项.

7.3 if()

我们介绍第一个改变流程控制的运算式是 if(). 仔细看看底下的例子:

1 void reset() {
2 int x;
3
4 ::reset();
5 x = random(100);
6 if(x > 50) set_search_func("floorboards", "search_floor");
7 }

每一行的编号仅供参考.
在第二行, 我们宣告一个称為 x 的整数型态变数. 第三行则优雅地留下一行空
白, 以明示宣告结束和函式码开始的界线. 变数 x 只能在 reset() 函式中使
用.
第四行呼叫 room.c 中的 reset().
第五行使用 driver 外部函式的 random() 以传回一个随机数字, 此数字介於 0
到参数减一. 所以在此我们想得到一个介於 0 到 99 的数字.
第六行中, 我们测试运算式 (x>50) 的值, 看它是真是偽. 如果為真, 则呼叫
room.c 的函式 set_search_func(). 如果為偽, 就不可能执行呼叫
set_search_func() .
第七行, 函式将 driver 的控制权交回呼叫此函式的函式 (在这个例子中, 呼叫
reset() 的是 driver 自己) , 也没有传回任何值.

如果你想执行一个以上的指令, 你必须按照以下的方法来做:

if(x>50) {
set_search_func("floorboards", "search_floor");
if(!present("beggar", this_object())) make_beggar();
}

注意运算式為真时, 要执行的指令以 {} 包围起来. 这个例子裡, 我们再次呼叫
room.c 中的 set_search_func() 来设定一个函式 (search_floor()) , 这个
函式稍后被你设定為: 玩家输入 "search floorboards" 时, 呼叫
search_floor(). (註: 这种例子要看 mudlib 而定. Nightmare 有这个函式呼
叫, 其他 mudlib 可能会有类似的东西, 也可能完全没有这一类用途的函式)
接著, 另一个 if() 运算式检查 (!present("beggar", this_object())) 运算
式是否為真. 测试运算式中的 ! 改变它后面运算式的真偽. 在此, 它改变外部
函式 present() 的真偽值. 在此, 如果房间裡有个乞丐, present() 就传回乞
丐这个物件 (this_object()), 如果没有乞丐, 则传回 0. 所以, 如果房间裡面
还有个活乞丐, (present("beggar", this_object())) 的值就会等於乞丐物件
(物件资料型态) , 不然它会传回 0. ! 会把 0 变成 1 , 把任何非零值 (像
是乞丐物件) 变成 0. 所以, 房间裡没有乞丐时, 运算式
(!present("beggar", this_object())) 為真, 反之, 有乞丐為 0. 如果房间裡
没乞丐, 它呼叫你房间码中定义的函式来製造一个新的乞丐, 并放进房间. (如
果房间中已经有一个乞丐, 我们不想多加一个 :) )

当然, if() 常常和一些条件一起出现 :). LPC 裡, if() 叙述的正式写法為:

if(运算式) { 一堆指令 }
else if(运算式) { 一堆指令 }
else { 一堆指令 }

这样表示:

如果运算式為真, 执行这些指令.
不然, 如果第二个运算式為真, 执行第二堆指令.
如果以上皆偽, 执行最后一堆指令.

你可以只用 if() :

if(x>5) write("Foo,\n");

跟著一个 else if():

if(x > 5) write("X 大於 5.\n");
else if(x >2) write("X 小於 6, 大於 2.\n");

跟著 else:

if(x>5) write("X 大於 5.\n");
else write("X 小於 6.\n");

或是把上面列出来的东西全写出来. 你有几个 else if() 都没关係, 但是你必
须有一个 if() (也只能有一个), 也不能有一个以上的 else . 当然, 上面那个
乞丐的例子中, 你可以在 if() 叙述中重复使用 if() 指令. 举例来说,
if(x>5) {
if(x==7) write("幸运数字 !\n");
else write("再试一次.\n");
}
else write("你输了.\n");

7.4 叙述: while() 和 do {} while()

原型:
while(运算式) { 一堆指令 }
do { 一堆指令 } while(运算式);

这两者让你在运算式為真时, 一直重复执行一套指令. 假设你想设定一个变数等
於玩家的等级, 并持续减去随机的金钱数量或可承受伤害值 (hp, hit points)
直到该等级变数為 0 (这样一来, 高等级的玩家失去的较多). 你可能会这样做:

1 int x;
2
3 x = (int)this_player()->query_level(); /* 这行内容等一下会解释 */
4 while(x > 0) {
5 if(random(2)) this_player()->add_money("silver", -random(50));
6 else this_player()->add_hp(-(random(10));
7 x--;
8 }

第三行中呼叫的 this_player()->query_level() 运算式 (译註: 之后内容遗失
, 在此由译者补充) 的意义: 呼叫 this_player() 外部函式, this_player()
传回一个物件, 為正在呼叫此函式的玩家物件. 再呼叫此玩家物件中的
query_level() 函式. (译註: 补充结束)

在第四行, 我们开始一个迴圈, 只要 x 大於 0 就重复执行.
我们可以用另一种写法:
while(x) {
(译註: 以下遗失, 由译者补充)
由於 x 本身稍后会一直减 1 直到到 x = 0 , 所以 x = 0 时也是偽值 (為 0).
第五行以 random(2) 随机传回 0 或 1. 如果它传回 1 (為真),
(译註: 补充完毕)
则呼叫玩家物件的 add_money() 将玩家身上的银币随机减少 0 到 49 枚.
在第六行, 如果 random(2) 传回 0, 我们呼叫玩家物件中的 add_hp() 函式来
减少 0 到 9 点的可承受伤害.
第七行裡, 我们把 x 减 1.
第八行执行到 while() 指令的终点, 就回到第四行看 x 是否还大於 0 . 此迴
圈会一直持续执行到 x 小於 1 才结束.

但是, 你也许想在你执行一些指令「之后」再测试一个运算式. 比如用上面的例
子, 如果你想让每个人至少执行到一次指令, 甚至还不到测试的等级:

int x;

x = (int)this_player()->query_level();
do {
if(random(2)) this_player()->add_money("silver", -random(50));
else this_player()->add_hp(-random(10));
x--;
} while(x > 0);

这个例子真的很奇怪, 因為没几个 mud 会有等级為 0 的玩家. 而且, 你可以
修改前面例子中的测试条件做到同样的事. 不管如何, 这个例子只是要展现出
do {} while() 的如何工作. 如你所见, 此处在迴圈开始的时候没有初始条件
(在此不管 x 的值為何, 立刻执行) , 迴圈执行完之后才测试. 这样能保证迴
圈中的指令至少会执行一次, 无论 x 為何.

7.5 for() 迴圈

原型:
for(初始值 ; 测试运算式 ; 指令) { 指令 }

初始值:
让你设定一些变数开始的值, 用於迴圈之内. 此处可有可无.

测试运算式:
与 if() 和 while() 的运算式相同. 当这一个 (或一些) 运算式為真时, 执行
迴圈. 你一定要有测试运算式.

指令:
一个 (或一些) 运算式, 於每个迴圈执行完毕之后执行一次. 此处可有可无.

註:
for(;运算式;) {}

while(expression) {}
「 完 全 相 同 」

范例:

1 int x;
2
3 for(x= (int)this_player()->query_level(); x>0; x--) {
4 if(random(2)) this_player()->add_money("silver", -random(50));
5 else this_player()->add_hp(-random(10));
6 }

这个 for() 迴圈与前面 while() 的例子「完全相同」. 还有, 如果你想初始
化两个变数:

for(x=0, y=random(20); x<y; x++) { write(x+"\n"); }

在此, 我们初始化 x 和 y 两个变数, 我们把它们用逗号分开来. 你可以在
for() 三个部分的运算式中如此使用.

7.6 叙述: switch()

原型:
switch(运算式) {
case 常数: 一些指令
case 常数: 一些指令
......
case 常数: 一些指令
default: 一些指令
}

这样有点像 if() 运算式, 而且对 CPU 也好得多, 但是 switch() 很少有人使
用它, 因為它看起来实在很复杂. 但是它并非如此.

第一点, 运算式不是测试条件. case 才是测试. 用普通的话来读:

1 int x;
2
3 x = random(5);
4 switch(x) {
5 case 1: write("X is 1.\n");
6 case 2: x++;
7 default: x--;
8 }
9 write(x+"\n");

就是:

设定变数 x 為一个 0 到 4 的随机数字.
x = 1 的 case 中, 显示 x 的值, 将 x 加上 1 之后再将 x 减 1.
x = 2 的 case 中, 将 x 加上 1 之后再减 1.
其他情形下, x 减 1.
显示 x 的值.

switch(x) 基本上告诉 driver, 变数 x 的值是我们想配合各个 case 的情形.
当 driver 找到一个能配合的 case 时, 这个 case 「以及所有在它之后」的
case 都会执行. 你可以使用 break 指令, 在执行一个 case 之后跳出
switch 叙述, 就像其他流程控制叙述一样. 稍后会解释这一点. 只要 switch()
流程还没中断, 任何 x 值都会执行 default 叙述. 你可以在 switch 叙述中
使用任何资料型态:

string name;

name = (string)this_player()->query_name();
switch(name) {
case "descartes": write("You borg.\n");
case "flamme":
case "forlock":
case "shadowwolf": write("You are a Nightmare head arch.\n");
default: write("You exist.\n");
}

对我来说, 我会看到:
You borg.
You are a Nightmare head arch.
You exist.

Flamme、Forlock 、或 Shadowwolf 会看到:
You are a Nightmare head arch.
You exist.

其他人会看到:
You exist.

7.7 改变函式的流程和流程控制叙述

以下的指令:

return continue break

能改变前面提过的那些东西, 它们原本的流程.
首先,

return
一个函式中, 不管它出现在哪裡, 都会终止执行这个函式并将控制权交回呼叫这
个函式的函式. 如果这个函式「不是」无传回值 (void) 的型态, 就必须在
return 叙述之后跟著一个传回值. 一个绝对值函式长得大概像这样:

int absolute_value(int x) {
if(x>-1) return x;
else return -x;
}

第二行裡, 函式终止执行, 并回到呼叫它的函式. 因為在此, x 已经是正整数.

continue 在 for() 和 while() 叙述中用得最多. 它停止目前执行的迴圈, 把迴
圈送回开头执行. 例如, 你想要避免除以 0 的情况:

x= 4;
while( x > -5) {
x--
if(!x) continue;
write((100/x)+"\n");
}
write("完毕.\n")

你会看到以下的输出:
33
50
100
-100
-50
-33
-25
完毕.

為了避免错误, 每一次迴圈都检查 x, 确定 x 不為 0. 如果 x 是 0, 则迴圈
回到开头处的测试运算式, 并不终止目前的迴圈.

用 for() 运算式来说就是:
for(x=3; x>-5; x--) {
if(!x) continue;
write((100/x)+"\n");
}
write("完毕.\n");

这样执行起来差不了多少. 注意, 这样子跟前面输出的结果一模一样. 当 x = 1
, 它测试 x 是否為 0, 如果不是, 就显示 100/x, 然后回到第一行, 将 x 减
1, 再检查 x 是否是 0 , 如果為 0, 回到第一行并把 x 再减 1.

break
它停止执行流程控制叙述. 不管它出现在叙述裡面的任何地方, 程式控制会结束
迴圈. 所以, 如果在上面的例子中, 我们把 continue 换成 break, 则输出的结
果会变成像这样:

33
50
100
完毕.

continue 最常用於 for() 和 while() 叙述. 但是 break 常用於 switch().

switch(name) {
case "descartes": write("You are borg.\n"); break;
case "flamme": write("You are flamme.\n"); break;
case "forlock": write("You are forlock.\n"); break;
case "shadowwolf": write("You are shadowwolf.\n"); break;
default: write("You will be assimilated.\n");
}

下面这个函式跟上面的一样:

if(name == "descartes") write("You are borg.\n");
else if(name == "flamme") write("You are flamme.\n");
else if(name == "forlock") write("You are forlock.\n");
else if(name == "shadowwolf") write("You are shadowwolf.\n");
else write("You will be assimilated.\n");

但是 switch 叙述对 CPU 比较好.
如果这些指令放在多层巢状 (nested) 的叙述中, 它们会改变最近的叙述.

7.8 本章总结

这一章讲的东西实在是太多了, 但是它们马上就用得到. 你现在应该完全了解
if()、for() 、while() 、do{} while()、switch() , 也该完全了解如何使用
return、continue、break 改变它们的流程. 使用 switch() 要比一大堆 if()
else if() 来得有效率, 所以应该儘量使用 switch() . 我们也向你介绍过怎麼
呼叫其他物件中的函式. 不过, 以后会详细解释这个主题. 你现在应该能轻轻鬆
鬆写出一个简单的房间 (如果你已经读过你 mudlib 有关建造房间的文件) 、简
单的怪物、其他简单的物件.

LPC的物件资料型态 ivy,2007-04-12 17:53:16

8.1 回顾

你现在应该能从你自己的物件中呼叫函式. 你也应该清楚, 至少在一开始物件载
入记忆体的时候, 你物件中的 create() (或 reset() ) 函式会被呼叫, 而你的
reset() 函式会一直被重复呼叫, 让你可以写些程式码来更新你的房间状况. 注
意一下, 你的物件中不一定要有这两个函式. driver 会先检查你的物件中有没
有这些函式. 如果没有, 也不会怎麼样. 你也已经认识 void (无传回值), int
(整数), string (字串) 这三种资料型态.

8.2 物件是一种资料型态

在这一章裡面, 你将会认识一种更复杂的资料型态──物件. 一个物件变数指向
一个已经载入 driver 记忆体的真正物件. 宣告物件变数的方法跟宣告其他资料
型态的变数一样:
object ob;
不过它不同的地方在於你不能在它身上用 +、- 、+=、-=、* 、/ (把一隻怪物
除以另一隻怪物到底有啥意义 ? ). 而且, 像是 say() 和 write() 外部函式
只要字串或整数, 你就不能 write() 或 say() 它们 (再次声明, 说一隻怪物
是啥意思 ? ). 但是你可以将它们用於其他 LPC 重要的外部函式上.

8.3 外部函式: this_object()

这个外部函式传回一个物件, 是正在执行 this_object() 的物件. 换句话说,
在一个档案裡, this_object() 就是你的档案物件复製出去的拷贝或是继承这个
档案的其他档案. 当你正在撰写一个会被别的档案继承的档案, this_object()
就很有用. 假设你正在写你自己的 living.c , user.c 和 monster.c 会继承
它, 但是 living.c 不可能会独自使用, 它只用来被这两个物件继承. 你想要把
设定玩家等级的 set_level() 函式记录下来, (但是你不想记怪物的).
你可能会这样做:

void set_level(int x) {
if(this_object()->is_player()) log_file("levels", "foo\n");
level = x;
}

既然 living.c 或 living.c 继承的档案都没有定义 is_player(), 我们就假设
if(is_player()) 会导致一个错误, 因為 driver 在你的档案裡、你继承的
档案中都找不到 is_player() 函式. 因為你的档案是被别的档案继承之故,
this_object() 让你能使用最后成品中可能拥有 (或没有) 的函式而不会出现错
误.

8.4 呼叫其他物件中的函式

这当然是向你介绍物件资料型态最重要的特色. 它让我们能使用其他物件中的函
式. 前面的范例裡, 你已经能找出一个玩家的等级、减少他们身上的钱、他们有
多少可承受伤害点数.
有两种方法可以呼叫其他物件中的函式:

物件->函式(参数)
call_other(物件, "函式", 参数);

范例:
this_player()->add_money("silver", -5);
call_other(this_player(), "add_money", "silver", -5);

某些情形下 (很概略的说法) , 游戏只是由玩家命令触发的一连串函式呼叫. 当
一个玩家开始一串函式呼叫时, 这个玩家就是 this_player() 外部函式所传回
的物件. 所以, 因為 this_player() 可以由触发事件的人决定, 你要小心你用
this_player() 呼叫函式的地方在哪裡. 你通常会把它摆在最后一个重要的区域
函式── init() 裡 (我们已经提过 create() 和 reset() ).

8.5 区域函式: init()

任何时候, 一个活著的东西碰到一个物件 (进入一个新的房间, 或其他物件进入
同一个房间) , 就会呼叫此物件新遇到所有物件裡面的 init() 函式. 在此, 你
可以加上一些玩家可以使用的命令. 以下是一朵花的 init() 函式范例.

void init() {
::init();
add_action("smell_flower", "smell");
}

上面呼叫 smell_flower() 函式. 所以你应该有个 smell_flower() 函式长得像
这样:

1 int smell_flower(string str); /* 玩家动作的函式是整数型态 */
2
3 int smell_flower(string str) {
4 if(str != "flower") return 0; /* 玩家闻的不是这朵花 */
5 write("你闻了这朵花.\n");
6 say((string)this_player()->query_cap_name()+"闻了闻花.\n");
7 this_player()->add_hp(random(5));
8 return 1;
9 }

第一行, 我们宣告函式.
第三行, 开始 smell_flower(). str 是跟在玩家命令之后的任何东西 (不包括
第一个空白字元).
第四行, 检查玩家输入的是否為 "smell flower". 如果玩家输入的是
"smell cheese", 则 str 就是 "cheese". 如果闻的不是花, 就传回 0,
让 driver 知道不该呼叫这个函式. 如果玩家身上有块乳酪, 乳酪也有个
smell 指令的话, driver 之后会呼叫乳酪的函式. driver 会持续呼叫同
样是 smell 的命令, 直到有一个传回 1 為止. 如果它们都传回 0, 则玩
家就看到「什麼 ?」
第五行, 呼叫 write() 外部函式. write() 把传入给它的字串印出来给
this_player() . 所以, 只要输入 "smell flower" 的玩家都会看到「你闻
了这朵花.」
第六行, 呼叫 say() 外部函式. say() 印出闻花动作的字串, 我们需要呼叫
this_player() 的 query_cap_name() 函式. 这样子碰上隐形的玩家会印出
「某个人」 (或像是隐形的东西), 而且会把第一个字元转為大写
(capitalize).
第七行, 我们呼叫 this_player() 物件中的 add_hp() 函式, 因為我们想在闻
了花之后对玩家作一点治疗 (註: 别把这些程式码写在你的 mud 裡, 管理
mud 平衡的人会毙了你).
第八行, 我们把游戏的控制交回给 driver, 传回 1 让 driver 知道它呼叫的
函式正确.

8.6 在你的房间加上物件

现在, 使用物件资料型态, 你可以把怪物加进房间裡面:

void create() {
::create();
set_property("light", 3);
set("short", "Krasna 广场");
set("long", "欢迎来到 Praxis 镇的中央广场.\n");
set_exits( ({ "d/standard/hall" }), ({ "east" }) );
}

void reset() {
object ob;

::reset();
if(present("guard")) return; /* 如果已经有一位警卫, */
ob = new("/std/monster"); /* 就别再增加一位 */
ob->set_name("guard");
ob->set("id", ({ "guard", "town guard" }) );
ob->set("short", "镇警卫");
ob->set("long", "它看守著 Praxis.\n");
ob->set_gender("male");
ob->set_race("human");
ob->set_level(10);
ob->set_alignment(200);
ob->set_humanoid();
ob->set_hp(150);
ob->set_wielding_limbs( ({ "right hand", "left hand" }) );
ob->move(this_object());
}

现在, 大多数的 mud 在此都大不相同. 前面提过, 有的 mud 把这些东西写在
一个独立设定的怪物物件裡. 原始模式的 mud 最后要呼叫怪物物件中的 move()
来把它搬进房间 (this_object() ) 裡. 在精简模式的 mud 裡, 你呼叫需要两
个参数的 move_object() 外部函式, 这两个参数是: 要搬动的物件和要放东西
进去的物件.

8.7 本章总结

行文至此, 你现在应该有相当的知识来撰写一些很棒的东西. 当然, 我一直强调
你真的需要去阅读如何在你 mud 写程式的说明文件, 它们会详细说明在什麼种
类的物件裡拥有哪些函式可以呼叫. 无论你对 mudlib 的知识有多少, 你已经有
足够的知识了解如何给玩家一些额外的事情做, 像是闻闻花、贴东西之类的事.
现在你应该能忙於撰写程式. 但是此刻, 事情看起来变得枯燥沉闷, 这表示你该
进入下一阶段、更深入的时间到了. 现在让你自己撰写一个小区域. 儘量使用你
mud room.c 裡头所有的特殊函式 (找找别人觉得用都用不到的冷僻文件) . 加
上一堆简洁的动作. 创造一些含有魔力的武器, 其魔力会渐渐消失. 以上这些你
现在应该都能写得出来. 一旦这些东西对你来说都变成例行公事, 就是你开始学
习中阶课程的时候. 注意, 只有很少人能真正进入中阶课程. 如果你全部都做完
, 我告诉你, 你在 mud 中能做到的领域只在少数. 这不仅是因為其他许多领域
很困难, 也因為有一些已经超越此领域的人充满了傲慢, 而且极少传播这些知识.
秘诀在於: 强迫你自己, 并想一些你觉得不可能做到的事. 如果你问某个人怎麼
做 X, 而他们跟你说那个不可能做到, 就自己想办法利用实验把它写出来.


gfdsa1,2007-05-20 04:00:57

好复杂啊。。。。
 97 1 8: 此主题共有9帖 此页9帖 每页12帖 

[查看完整模式]

LeadBBS 4.0 耗时0.008秒 query:2