发信人: zhangsf()
整理人: zhangsf(1999-12-31 17:34:03), 站内信件
|
基础 LPC
作者: Descartes of Borg
第一版: 23 april 1993
第二版: july 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 运算子, 包括对它们使用的值所作的事 (如
果有值的话), 以及它们自己拥有的值.
在此说明的运算子有:
= + - * / % += -= *= /= %=
-- ++ == != > < >= <= ! && ||
-> ? :
下面, 这些运算子将全部用相当简单的方式说明之, 但是你最好把每个运算子至
少都看过一次, 因为有些运算子的功能「不见得」如你所想的一样. 不过, 这段
说明可以当作相当好的一个参考.
= 指定运算子 (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
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 10 july 1993
第七章: 流程控制 (flow control)
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); xquery_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
作者: Descartes of Borg
第一版: 23 april 1993
第二版: 12 july 1993
第八章: 「物件」资料型态
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, 而他们跟你说那个不可能做到, 就自己想办法利用实验把它写出来.
George Reese
Descartes of Borg
12 july 1993
[email protected]
(译按: 已改为 [email protected] )
Descartes@Nightmare (intermud)
Descartes@Igor (not intermud)
译者:
Spock of Final Frontier 98.Feb.2.
-- ※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.100.198.52]
|
|