北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
查看: 9347|回复: 0

MUD编辑修改入门教程3(zz)

[复制链接]
发表于 2009-1-23 01:22:27 | 显示全部楼层 |阅读模式
人物也讲完了,其实根据这两个文件,你可只选其中一两点就可以改出很多你所需要的人物来,在初级阶段,把现成的文件改会避免太多的BUG的出现。但是我所要说的是:重在理解。理解了之后,什么都好办了。  以下列出一些定义在人物里的一些附加函数,以供参考。
void defeated_enemy(object victim)
  当这名人物打昏一个敌人时会呼叫这个附加函数,victim 即是被打昏的人。
  呼叫者: COMBAT_D
  有预设定义此一函数的系统物件: none
void killed_enemy(object victim)
  当这名人物杀死一个敌人时会呼叫这个附加函数,victim 是将要被杀死的人。
  呼叫者: COMBAT_D
  有预设定义此一函数的系统物件: none
int accept_fight(object who)
  当有其他生物对这个人物下 fight 指令的时候,会呼叫这个附加函数,who是下
  fight 指令的生物,只有当这个附加函数传回 1时才会接受挑战,否则显示某某不想
  跟你较量的讯息。
  呼叫者: "fight" 指令
  有预设定义此一函数的系统物件: NPC
int accept_object(object who, object item)
  当有人用 give 指令给这个非玩家人物东西时,会呼叫这个附加函数,传回 1
  表示愿意接受这个东西,传回 0 表示不接受。
  呼叫者: "give" 指令
  有预设定义此一函数的系统物件: none
void recruit_apprentice(objct apprentice)
  当一个人物收了另一个人物做弟子时会呼叫这个附加函数,你可以在这个函数里
  修改弟子的 rank 或其他东西。
  呼叫者: "apprentice" 指令
  有预设定义此一函数的系统物件: none
  有关房间、人物和物品这三种类型只是我们人为的划分,对于系统来说,它们应该都是一回事。为了实现某一种效果,既可以写在房间里进行执行,也可以写在人身上进行执行。看了这一章后,你可以尝试着写一些程序了。我们希望你的感性认识是建立在自己写了超过百个的编译通过的程序以后,下一章,我们就可以学习LPC的概念了。
编程终究是一件技术性的活,我们总不能一直通俗下去吧!在这一章,我们该开始回过头来,对我们工作系统里的一些基本的概念进行一一的理解,当然要接触一些深一些的概念了。
  通常我们在大陆所见到的中文MUD大多是LpMUD,一般是一些角色养成型的游戏模式。LpMUD使用Unix的指令和文件结构。而很多版本也是运行在Unix环境下(目前WINNT的版本也不少)。
&&--理解LPC
  LPC是什么东东?就是写LPMUD程序的C语言啦!它看起来和一般的C语言区别不大,语法基本一样,只是简单得多啦,可以说,它是我所见到的最简单的一种C语言。他们的根本不同点在于,Lpc程序是编写一个一个的"Object"。 这有什么区别呢?一般的程序是在执行过程中,通常有一个明显的开始和结束。程序从一个地方开始,然后顺序执行下去,到了结束的地方就中断了。而Lpc的Object不是这样的。Lpc的Object可能没有明显的开始和结束的标志,它可能永远在工作。在有些游戏中,整个游戏包括Driver和游戏世界都用C写好,这样处理的速度能快一些,但是游戏的扩充性很差,巫师们不可能在游戏进行中添加任何东西。 LpMud则相反。Driver理论上应该和玩家所接触的世界几乎没有任何直接的联系。游戏世界应该是自己独立的,而且是“即玩即加”的。举个例子,我们在玩三国志系列时,在你没能取得赤壁之战的胜利之前,你是不可能去打六出祁山的,甚至连四川都去不了,但是在MUD游戏中,却可以任由玩家选择,而且巫师也可视其需要,在任何时候、在任何地方再加上一场另外的战役。所以,在你写完一个Lpc的文件时,它就存于主机的硬盘上。在游戏进行中,当需要整个Object时,Driver就从硬盘中读入这个文件,然后放在内存中,一个特殊的函数被调用来初始化这个Object的一些变量。当然这要建立在这个文件没有任何错误的基础上。否则,轻则出错,重则宕机。
  在这里,既然我们不断谈到“变量”和“函数”,则有必要谈谈在LPC中它们之间的关系。变量就是一个能够变化的值,函数通常是用来操纵那些变量的一段程序。Lpc 的Object就是一些变量和函数的组合。在前面我们讲过,Object没有开始、也没有结束的地方,更不会有一个特别的地方让Driver去执行它。那当一个Object如果要被内存中的另一个Object调用时将怎么办呢?Driver一般会去找这个Object的那堆变量放在哪里。如果这些变量没有值,那么Driver就会调用一个特定的函数,即create()来初始化这些变量。(看到这里,你可以回去看看第二章的有关部分)。
  但是要强调一点,create()并不是 Lpc代码开始执行的地方,只是大多数的Object从这里开始。事实上,create()是可以不存在的。如果这个Object不需要对变量初始化,那么create()可以不存在。那么这样的Object开始执行的地方就完全的不同于一般的Object,它可以从任何地方开始。所以在Lpc的Object中,函数的排列顺序是无所谓的,随便那个排在前面对这个Object的特性没有影响。只不过在各个MUD中的巫师品质要求中稍作规定而已。
&&--理解Object
  既然LPC中最不同的就是Object,因此,当你想在程序中设计任何动作时, 都应当要考虑到这个动作是哪一个 object 所做的, 不然很容易导致错误。LPC 的语法并不严谨, 有些场合为了省事可以将函数是由哪个Object所做的省略掉, 例如我们在 create() 函数中最常看到的 set(),事实上严谨的写法应为this_object()->set()。因此我们首先要学习的就是如何利用系统提供的函数,去正确快捷地寻找Object。
  MUD系统为我们提供了两个最好用的两个函数this_object()与this_player(),在你写作一个对象(房间、物品......)时,this_object()表示自己这个对象(房间、物品......),也就是你写的这个文件本身啦。在这里,要提及一下object的所谓封闭性。每一个object有自己独立的数据结构,它能与其他object严格区分开来。即使是同一个长剑程序,在同一个房间里你复制了两把时,在内存中就会有两个完全独立的长剑object,你对其中一把无论进行改名、改威力都不会影响到另一把的数据,这就是object的封闭性。而this_player()则比较复杂, 它会传回一个属于玩家类型的对象。这个玩家在init中就是触发init的那个玩家。this_player()会跟著函数呼叫一直传递给所有被init呼叫的函数, 包括add_action中所定义出来的函数, 在这些函数中, 它又表示做动作的那个人。
  除此之外,当我们只知道一个对象的名字,而不是它的文件名时,是无法用个 object 类型的变量指向它,这时我们就要用到[present() 函数]
  用法:
    object=present(string "id",object env)
  函数在此时就从名字(id)找到这个object,有了这个object才好对它做操作。就可以派上用场。简单的想, present 函数其实就是在一个房间或者一个人身上(房间与人对于程序其实是一样子的)里找出叫某个名字的物品的函数,它是同类型找物品的函数中最有用的一个, 其余的函数还有:find_player(), find_living() 等等
  再看一组很实用的函数:[environment(),first_inventory(),next_inventory(), all_inventory()]。这一组函数跟对象所处在的位置有关:
  environment(object ob)传回了对象 ob 所处在的地点。假如 ob 是个玩家或生物,那么这个函数会传回 ob 所在的房间;如果 ob 是个物品,是有人带的就传回携带着 ob 的生物, 没人带的话就传回 ob 所在的房间。
  first_inventory(object ob) 所传回的是 ob 中的第一个对象,如果 ob是房间,则传回这个房间中的第一个物品或是生物;如果 ob 是生物, 则传回他身上所带的第一个物品。
  next_inventory(object ob) 通常是跟着 first_inventory() 一起使用,它的功用是传回 ob 的下一个物品。
  很简单, all_inventory(object ob) 所传回的是包含了所有物品的一整个阵列。一个object(除了房间之外)都要有自己的环境,就是这个object在什么地方放着,是一间房间里、还是一口箱子中、还是一个人身上,而这环境通常是另一个object。比如物品A和B放在一个人M身上,那么上面的函数就可以给出它们之间的关系 :
  M=environment(A);
  M=environment(B);
  A=first_inventory(M);
  B=next_inventory(M);
  A=all_inventory(M)[0];
  B=all_inventory(M)[1];
&&--理解Efun
  从上面你也许可以看到了,象environment()这样的各种函数,也许会在你编程时时不时地发现,而且用的地方特别地多,最常见的就是this_player()、this_object()、还有strcmp()、implode(),左看右看找不到在哪里定义的,而你就算是找遍你下载下来的单机版的所有文件,都找不到这些函数,其实它们就是efun。efun就是MUD外部定义的函数,也就是externally defined function 的缩写。是由Mud Driver定义好的。它是以计算机直接能理解的二进制的形式存在着的,所以它们执行起来要比一般的Object带有的函数速度快的多。而对于Object内部定义的函数,通常叫作lfun(local function)。一个巫师的主要工作也就是编写一些lfun组成的Object。
那么为什么要创立efun呢?
  1) 处理一些很常用的、经常会有许多函数会调用的。
  2) 处理internet socket的输入输出。
  3) 以及一些Lpc很难处理的事,因为毕竟Lpc是C的很小的子集。
  efun是用C写好的,内嵌在Driver里面的。在Mud起来之前,和Driver一起编译好的,它们的调用和你写的函数的调用方法是完全一样的。总的来说,你只需要关心:它需要传入什么参数、将会返回什么的东西就行了。
  通常在一个Mud里面,你可以在类似这样的/doc/efun的目录底下找到有关efun的说明和帮助,或者直接用help 指令就可以得到帮助。因为efun依赖于你所在的Mud的Driver,所以不同的Driver带有的efun区别很大。不要想当然地将别的MUD中的经验带到这里来,也不要在我们这里理解了某个efun,就自以为什么都懂了。对于一个新的巫师不说,你只需要简单地理解,并会使用一些常见的efun就行了
学习真是一件枯燥无味的事件,听得不耐烦了,赶快让我上机操作吧!
   好的,这一章,我们就来让你去开始进行一段见习工作了。我们的Mud系统使用的是仿Unix的指令和文件结构。如果有人已经对Unix有所了解的话,那是最好。没用过Unix也没关系,就象LPC虽然是一种很简单其实它与Dos十分地相似,我们只要记住它们最明显的一个区别就是文件路径是用"/",而不是"\"。下面的许许多多的命令还是让我们会时不时地想起DOS下的种种命令。

pwd: 显示你目前所在的当前目录
cd: 进入某一目录,..是上一目录,/是到根目录,什么都不加直接回车就是到你自己的的工作目录;
ls: 列出指定目录下的所有文件,就象是Dos的dir一样;
rm: 删除一个文件;
mv: 重新命名一个文件;
cp: 复制一个文件;
md: 创建一个目录;
rd: 删除一个目录(空的目录)。删除这个目录下的所有文件请加 -d参数;
more: 按页显示一个文件在你的当前屏幕;
cat: 显示整个文件;
tail: 显示一个文件的结尾几行,对于太大的文件,一般只能用它;
      对于写程序来说,最好的当然是在专门的可以识别C的编辑器上写,既方便、错误也会少得多,因为它们对于一些固定的函数、变量都会用特殊的颜色突出显示,尤其对于初学编程的巫师来说,多一个字母、少一个符事情的事情可以大大减少了。但是巫师更多的是在线处理一些问题、故障,大多数时候是要通过在线编辑----也就是edit命令进行程序修改,甚至小程序的在线写作。下面就着重讲一讲MUD所提供的编辑功能的使用。
1. 指令格式为:edit <档名>,只加文件名,默认为当前目录,加here,表示编辑你当前所处的房间, 回车后即进入线上编辑系统。
2. 如果这是一个已经有的档案,你可以使用 z 或 Z 来看档案。z表示一次显示20行,Z表示一次显示40行。为了编辑方便,最好在开始时用 n 表示每一行在开头处显示它的行数,再用一次 n 取消行数显示;
3. 还有一种方法 ,就是直接打入行数,则会跳至那行的内容上;
4. 如果你开始没打 n ,却想知道现在是第几行请打 = , 想知道内容请打 p ;
5 如果想直接到档案的结尾可输入 $;
6. 五种编辑命令 a i c m d :
  a = 从此各行之下插入编辑;
  i = 从此行之上插入编辑;
  c = 修改此行并插入编辑;
m = 把本行移到特定的行号去
d = 删除;
这些命令也可以和行数结合使用。如 :
  7a = 在第7行后插入编辑;
6i = 在第6行前插入编辑;
4c = 直接编辑第4行;
  5,8m1 = 将第 5~8 行移至原第 1 行之下。
3d = 删去第 3 行 ;
  2,10d = 删去第 2~10 行;
7. 如果这是一个新命名的档案, 这里面当然是没有行数了,一般你应该用 a 来开始编辑第一行;
8. 如果你对某一行或某几行编辑完毕后, 请在编辑结尾的次列开头处打 . 即可退出行编辑状态;
9. 如果想存档请打 x 表示存档退出。 否则请打 q 或 Q 表示放弃编辑退出。
10.其余功能可打 h 参考。由于有的MUDOS汉化不太好,下面列出 h 的中文注释:
/ 前向查找你后面所跟着的字符,比如/酒袋
? 后向查找你后面所跟着的字符
= 显示当前行是第几行
a 在当前行后新增加一行
A 类似'a'命令,但是将翻转自动缩进模式
c 将当前行内容覆盖掉输入新编辑内容
d 删除指定范围的行
e 退出当前档案, 开始编辑另一档(档案改变过未存盘无效)
E 类似'e'命令,但是文件被修改过也有效
f 显示或改变文件名
g 查找匹配行并对其执行相应命令
h 帮助文件(就是显示你现在看到的这些信息,可能是英文)
i 在当前行前面插入一行
I 排版整个代码 (Qixx version 1.0)
j 合并行,系统默认是将后续行连接到当前行
k 标记当前行- later referenced as 'a
l 显示指定行(可显示控制字符)
m 移动指定行(或几行)到指定位置
n 行号显示切换开关
O 同命令'i'
o 同命令'a'
p 输出指定范围行
q 退出编辑器
Q 退出编辑器,即使文件已经被修改且未存盘
r 在文件尾或指定行后读进另一文件
s 查找并替换(只对当前行第一个查找的字符串进行替换)
set 查询,改变和保存编辑器的设定值
t 复制指定行到指定位置
v 搜索并对不匹配行执行指定命令
x 保存文件并退出
w 将编辑的内容写到当前文件或指定的文件里
W 类似'w'命令,但是是将编辑的内容附加到指定或当前文件后
z 显示20行,可用参数 . + -
Z 显示40行,可用参数 . + -
      前面一章讲过,当你成功地编写了一个程序后,只是意味着这个程序已经在硬盘了,只有在别的使用者调用到它的时候,才会被呼叫出来,进入内存。文件本身是否有错误,将首先会在这时被发现。对于巫师来说,消极地等待别人去调用它时再去发现有没有错是十分不明智的,这时最好的就是先update它。
    update <文档名>
    注意了:如果系统显示:“重新编译 ***.c:成功!”的信息,并不就表示你的这个程序就完全正确了,它只是表示你的程序的基本语法没有错误,如果程序里还有一些由其它的条件或文件才能触发呼叫的函数的话,还有有可能存在一些隐患,保险的做法就是按照设计时的条件把它们一一触发,进行尝试,直到完全通过。比如象一些NPC里有是否接受拜师的函数,你则想法不同的条件的人去拜它试试,把每一种可能都试过,看看是否执行正常。有的房间里加了add_action(),你则一一试试这些add_action(),看一下后果。
      如果文档中有错误,一般系统会唰地一下子出来一大串的错误信息,对于新巫师来说,只要去看看第一行的出错内容就行了,后面的很多错误都是由前面的带来的。还有许多信息还显示其它的与一些重要的系统文件也出错,也大抵是如此,首先还是找出关于这个文档里的第一个出错的行数,再到这个文档里去查找,仔细查看该行数,以及前后几行,有没有少写多写括号、漏记多添逗号、定义变量类型错误等等,如果显示出错的行数在最后一行,甚至更后的话,那就要看看是不是函数声明出错,或者定义了无效的函数。每改一次,再update一次,直至编译成功。有关于各种出错信息的意思和处理办法,还是要在实践中多多尝试,但是,在此要忠告各位新巫师,如果你所工作实习的MUD是一个正在开放中的MUD,希望对于没有任何把握的文件的编译工作最好先在自己的单机版进行,有些恶性的错误严重时会导致整个游戏宕机。
      接下来就是任何一个新巫师一上任就十分感兴趣的命令----call!call就是直接呼叫(执行)函数的意思。在某种程度上,它就象征着巫师手中的神杖。这个对于玩家来说威力无比的功能,既是一种巫师利器,更是一种危险器械。因此,在大多数的MUDLIB中都对于call的命令的使用进行了记录,以备天神的查看和监督。call的命令格式如下:
      call <物件>-><函数>(<参数>, ...... )
      从其理论上来说,它可以呼叫任何没有被protect的函数。具体我们可以看这个程序: /adm/daemons/emoted.c d 在这个程序里面有一个这样的函数:
string *query_all_emote()
{
      return keys(emote);
}
      那么。我们就可使用call命令直接呼叫它:
call /adm/daemons/emoted.c->query_all_emote()
        ~~~~~~~~~~~~~~~~~~~~~   ~~~~~~~~~~~~~~~
                  (物件)                     (物件的函数)
      由于这个函数本身没有要传参数,就不用再加参数了。那么执行后,程序本身就会返回一个字符串的数组回来。而显示在我们屏幕上面的内容就是所有的emote的英文名字。
  在实际工作中,上面的这种用得还是很少的,大部分的修改和查看我们都可以用more或edit去完成,但是对于尤其象玩家档案这些以.o形式储存的文件用edit编辑则有些费劲了,所以这时使用call的命令来得更为方便些。
      巫师们常常会call me(或者id)->set("combat_exp",10000)
  在这里,me就是自己,其实它对应着一个物件程序:/obj/user.c,后面的set()也是系统放在一个文件里最基本的函数。后面的括号里面便是这个set函数的参数。它的意思就是在me这个物件里执行set()函数,通过set()函数将combat_exp这个参数设为10000。如果。要改变别的人,就可以在call后面加上这个人的id。set()这个函数可以执行什么呢?其实很简单,打开一个复杂一点的NPC,它里面所具有的参数,我们一般都能用call命令进行。
      call命令可以调用的函数非常多,一般由你call的物件有关。但在一般使用中,我们大多使用三种函数,一是set,也就相当于我们做程序中的set一样,你可以set这个物件任何可以set的参数;第二个就是query,用它可以查看你所call的物件有没有这个参数,这个参数内容是什么?第三个就是delete,顾名思义,它正与set相反,用以删除物件上的这个参数。其它一些固定的函数,例如武功的set_skill,设定姓名的set_name等等就不一一叙述了。
一共四章的《新巫师入门手册》写出去以后,叮当一直有一种诚惶诚恐的感觉。因为我无论在接触MUD之前还是之后,都未接触过任何的编程语言学习,更别提什么C了。象我这样的人写出的教材,是否会误人子弟呢?但叮当也相信,在网上,也一定会有许许多多与当初的叮当一样,对于已有的一些巫师教材看得云里雾里的感觉。不是责怪这些教材写得太深,而是确实自己的基础太差。正是基于这点,叮当才决定依据网上已有的一些教材为基础,从自身的体会与理解出发,编了这册不成样子的《新巫师入门手册》。但是上网后,想不到竟会收到了很多新巫师朋友的感谢、赞扬与鼓励。他们对手册的肯定,也增强了叮当的信心。于是决定在加上一篇补遗篇,补充说明LPC编程中的一些基本概念,完成这册入门教材。并斗胆考虑起中级教材的布局。

  同时,叮当也声明,所有的概念都是从我自己的理解出发,请勿与专业教材中的定义相提并论,若有贻笑大方之处,还望各路高手多多指点。
第一节:变量

  首先,我发现新巫师们编程结束后,一旦update就呼啦啦地出现一大群的编译错误,其90%以上都是一些逗号,分号,括弧的基本错误。到底这些符号应该怎样使用呢?它们之间有何规律呢?但是在解释它们之前,我们必须来理解LPC中的变量与变量类型。
  变量是什么?我觉得你应该把它理解为一种不确定的替代值,有点象现实中的经纪人。其代表的人只要在第一次出来一下:声明某某是我的经纪人后,就可完全由变量来处理了。变量还有局部变量与全局之分,也就是仅仅在一个函数中起作用与在整个系统中起作用的分别。这点还是很好理解的。因此,对于我们来说,编程中之所以用到变量,其目的就是要让程序处理更快、更有效率。举例象这样一段程序:
  if(this_player()->query("qi")<25)
    this_player()->add(qi,-this_player("qi")/5);
  else if(this_player()->query("qi")>100)
    this_player()->add(qi,-this_player("qi")/2);
  else
    this_player()->add(qi,-this_player("qi")/3);
  这段程式中反复调用this_player()->query("qi")这个值,每出现一次,程序就要找一次this_player(),将它调出来,再从他的身上取出query("qi")这个值进行处理。而使用了变量则会简化了许多。比如,象this_player(),我就定义一个me来代替它,这样,我只要在一开始声明一下,me就是this_player(),这个变量就将this_player()找出,并定义在自己身上,以后每次执行时直接使用me就行了,也就是无须再次调用。其次,我们发现this_player()->query("qi")调用也很频繁,我们可以再定义一个变量i,用它来代替它。这样,这段程式可以改写成下面这样:
  object me = this_player();
  int i = me->query("qi");
  if(i<25)
    me->add("qi",-i/5);
  else if(i>100)
    me->add("qi",-i/2);
  else
    me->add("qi".-i/3);
  发现了吗,两个变量只是在开头定义时分别调用了一次,然后需对这两个变量进行操作便可以了。
  接着,细心的你可能会发现,这两个变量,我在定义的时候是用不同的方式的定义的。一个是object,另一个是int。这是因为我想让它们代表的类型不同。总体来说,在LPC里,变量大约有以下几种:
  object(对象型)、int(整数数值型)、float(浮点数值型即含小数点的数值)、string(字符串型)、mapping(映射型)、array(数组型)、mixed(混合型)、以及不常用的class(自定义型)。等等。
  一、object的意思,是定义一个对象,具体说来一个NPC、一个物品、一个场景、甚至一个运行于内存里的文件。它实际上是一段由后面很多变量按一定运算方式组合在一起的程式。我们经常使用的是将this_object()与this_player()通过object定义成简直的me或ob这样的符号。如果你要想在一个程序里制造成一件新的物品,则必须先定义一个变量,如:object obj;然后再obj = new(******)将这个obj实际上就clone了出来,括弧里的*****代表它的文件绝对路径名。
  二、int的意思,表明定义的变量是一个整数数字,可以为正负或0,定义出来的数字可以进行各种数字运算,但结果只保留小数点前的数字。比如:
  int i;
  i = this_player()->query_skill("force",1)/70;
  如果一个玩家的force最高只能到500级,那么这个i的结果只能是从0到7之间的这7个数之一。
  三、float相对于int来说可以是有小数的数字。比如i=10/3;如果前面是int i的话,i=3;而如果是float i的话,i=3.3333333。我查了一下外部函数表,对于我们使用的MUDOS来说,大部分的机器支持浮点值变量小数点后7位的精确度;
  四、string是说是一个字符串,你可以很简单地把它理解为一串字符号,这些字符不具有任何计算意义。一般来说,字符串的长度在理论上是没有限制的,在LPMUD里,限于网络响应,一般是在编译MUDOS时,在config.h文件里进行设置与限制的。对于字符串型变量的识别,我们有一个很简单的区别标准,就是要看它们有没有用双引号括起来,有则是string的变量,没有则看其是否整数而分辨为整数数值与浮点数值。因此在一些不严谨的语句中,如没有强制定义,也可将int、float与string区分出来。
    A、set("number",783);------->int型
    B、set("number",78.3);------>float型
    C、set("number","783");----->string型
    D、set("number","78.3");---->string型
  string型变量可以相加,但决非数字意义上的运算,而是一种合并,例如上面的C+D就是"78378.3";
  五、映射型变量是LPC独有的一种函数类型,据我的理解,好象是为了让程序更方便地实现一些小的数据库的功能。映射型变量里面有很多的小项,每一个小项都有与自己一一对应的参数。它们就好象是一个个独立的小变量一样,并使用 : 符号进行赋值。而且里面的这些小变量可以用前面的多种类型混用。 举例如下:
  mapping fam = (["a":2,"b":13,"c":2.333,"d":"一条小河","e":"158"]);
  这个fam里的a、b子变量是int型的,c是float型的,d、e是string型的。有一些LPC的说明文件里,a、b、c、d被叫做“关键字”,而:后面的象2、13、2.333、一条小河、158被叫做“内容值”。是不是有点类似于数据库的味道?映射型的变量可以用“变量名["关键字"]”的形式进行调用,并可以用“变量名["关键字"]=新内容值”的方式进行赋值。例如:
  fam["e"]的值就是"158" ,如果fam["e"]="400",那么再次调用时:fam["e"]的值就是"400"了。
  六、数组型变量实际上是很多的单个变量的集合,它的特征就是在定义变量名的时候,前面加一个*符号,前面可以object、可以int、也可以string,典型的数组型变量如下两种:
  string *num = ({"a","b","c","d","e"......});
  int *num = ({5,3,4,4,8......});
  object *obj = ({ob1,ob2,ob3,ob4});
  相同数型的不同数组型变量之间可以进行加减,加法时,则把两个数组里的项目合在一起,但是并不管里面有没有重复,一律列入。而减法则把被减变量里含有减变量里的项目统统去掉,举例说明:
  string *msg1 =({"a","b","d","d","e"});
  string *msg2 =({"b","b","d","f","g"});
  string *msg3 = msg1+msg2;
  string *msg4 = msg3-msg2;
  那么msg3 = ({"a","b","b","c","d","d","d","e","f","g"});
  而 msg4 = ({"a","c","e"});
  七、混合型变量一般用在一些特殊的地方,因为不能确定变量的类型,或者几个类型都有可能,就会用到它。不过一般的情况下,如果能确定的话还是要固定好。
  八、自定义型变量。(略。呵呵,因为我也不大掌握,基本上没用过。)
  另外象function (函数指针)用到的地方比较少,就不在入门手册中介绍了。还有一些可加在这些变量定义前面的进一步修饰的类型参数,比如象private、nomask这样的也不一定是新巫师所必须掌握的,还是留待更深一层的教材去讲述吧。


第二节 函数
  在LPC中,每一个函数被调用后,有时不需要返回任何值,有时则需要。我们就把不需要返回值的函数称为void(无返回值)型,其它的,则按照返回值的变量类型,区分为与此相互对应的类型。所以,参照上一节,我们就可以很容易地理解:函数也有着象那基本的八个变量、再加一个无返回的void,分为共九个基本类型。它们在函数开头的定义时就要写清楚了。
  所以新巫师们看到了这里后,就要使劲地想想,是否自己曾在某一个程序里,开头定义的是int ask_money(),结果在函数里面却是return "客官到底想要些什么?"这样返回是字符串的情况?反正我初写程序时常发生这样的错误。我记得在某些比较老的单机版的MUDOS里,对于函数的返回值检查并不是十分地严格,因此,在单机上测试往往很正常。但是到了LINUX下,尤其是新版本的MUDOS,对于这些检查十分地严谨,甚至在特殊的地方,还会导致宕机。
  前面我们讲过,LPC里,一个object就是一个很多变量的集合,那么这么多的变量是谁来控制它们呢,那就是函数了。在具体的编程中,每一个函数的设置都是要有其实际意义的,也就是说,要在运行中能被其它函数或其它object调用到。如果一个永远调用不到的函数,那就是没有任何意义的。在LPC中,有一些基本的函数是由系统,也就是底层的MUDOS自动调用的,我们也就无需去寻找它们的出处的。
  void create()
  前面也讲过,这是当一个object被载入内存时,对这个object进行最基本的初始状态设置用的函数。
  void init()
  当这个object本身进入一个新的object、或者有一个新的object进入了它所处的object、或者进入它自身里时这三种情况下将自动呼叫这一函数。
  然后还有一大堆由系统文件与总的继承文件所定义呼叫的大量函数,这些必须要了解,但是可以留待在实践中慢慢熟悉写与了解。
  再下来就是各个文件里自定义的函数了。其实所谓的自定义函数也只是相对的,最终说来,都是一个作者写的。只不过很多函数是由最早的巫师编写,并得到公认或约定俗成固定了下来。那么如何写一个函数呢?
  一、首先确定函数返回数据类型,比如是stirng还是int之类的;
  二、确定一个函数名,这个名字一般来说,首先你要熟悉你所工作的MUD里的函数命名规则或惯例,一是不要取一些与基本底层函数相同的名。比如die()、init()等等,其二是力求用简洁的英文或拼命取名,让人能够不看内容猜得出其用意;
  三、接下来就是一个()、()里放着这个函数执行时所需要的参数,这些参数可不是随便加的,它们的定义实际上是由调用这个函数的那段程序所提供的。
  四、写函数内容以一个{ 表示开始,最后当然是以一下 } 表示结束。函数的各种括号十分有意思,它们总是一对一对地出现。只要少了一个或多了一个,程序当然就会出错。
  五、函数一开始必须要对它所使用的变量进行声明,比如:
  string m,n;
  object ob1,ob2;
  这两句表示,在这个函数将要使用到两个分做m和n的字符串型变量与两个分别叫做ob1与ob2的对象型变量;
  六、下面就开始对变量进行赋值,计算指令的各种语句、表达式,也就是我们所看到的if、else、switch等等的语句。当然,就象别的函数调用你一样,你在这个函数里也可以调用别的函数。
  七、到了最后,再回到头来看看这个函数到底是什么类型的,只要不是 void,在最后结束的 } 前肯定要有一个 return ,并且返回和这个函数的数据类型一致的一个值。
  这里插一个与前面有关的话题,就是函数中所用到的变量问题。函数中的变量来自四个地方,第一个,当然是在函数一开始时声明并在之后直行赋值的;第二个就是在上面所说的第三步里在函数命名后面的()里面的,它是来自于调用这个函数的别的函数所提供的;第三个是来自于这个object()里的全局变量。一般是在整个文件扔程序开头的地方进行总的声明。我称它为小全局变量。这个变量可以在这个文件里所有的函数里进行调用;第四个是来自与整个MUDLIB所提供的全局变量。象我们的LPCMUD里经常会出现一些大写字母的变量名,比如象“USER_OB”“LOG_FILE”等等的变量名,在整个文件里甚至继承文件里也找不到,它一般是定义在/include目录下的全局变量声明文件里的。
第三节 符号
  编程要用到很多的符号。下面就要回到这一章开头讲的,到底那么多的符号怎么区别它们的用法。
  据我的体会,主要我们频繁使用的符号可以分出包括型与间隔型。
  
  包括型就是各种各样的括号。一共有四种,即()、{}、[]、"" 。这些括号可以掺在一起使用,但是一定要记住,在一个语句中,有几个(就必写会有几个)、同理,有几个[就必写会有几个]。所以在复杂的语句中,最好在检查时仔细数一数括号是否是前后对应的。
  一、回过头去看看第二章,就可以看到,()实质大多数是放函数执行时的参数或者是执行运算语句的。一个()前面必定会有一个函数名或者执行语词,当然有很大一部是由MUDOS提供的外部函数。比如象:write()、set()、init()或者是if()、else()、switch()等等。
  二、{}有三种用法,第一是用在函数的一开头与结尾,相互呼应。第二是用在一个程序表达式的开头与结尾。比如if(...){};第三便是被()包起来,表示数组,也就是({})。中间可以放入若干个项目;
  三、而[]也有三种用法,第一是被()包起来,表示映射函数。也就是([])。第二种是用函数名[关键字]这样的形式来表示映射里的某一关键字的值,比较常见的有在房间文件里的exits["south"];第三种是直接在一些string型或int型的变量后面跟上一个[],里面有一些参数,根据具体定义的返加值类型,返回不同的值。比如:
  string msg = "tims";
  (string)msg[0]就是t、(string)msg[3]就是s。
  而(int)msg[0]则会返回一组数字。具体数字的含义我也不太清楚,不过据我反复试验,发现这些数字的高低可以判断这个msg是英文字母、英文字符、中文字符或是全角字符。好象是各个字符的区域代码一样。
  四、""用在两个地方。一:在函数的具体项目名上要加。比如set("age",14);当然,如果这一个项目是一个变量或已经被一个变量所代替了,则不能加。二、在字符串上必须要加,尤其是表示字符串意义的数字。否则若没有定义的话,很容易被当作int型处理。而只要加了"",则必定被当作字符处理。
  间隔型符号主要只有两种:,与;与:
  一、逗号:,  逗号一般是表示前后的项目是平等并列的。它常被用在数组的各个数之间的分隔、映射中各个不同关键字的分隔,如:
  string *str = ({"A","B","C","D"})
  或者再如:
   mapping quest = (["A":4,"B":"大河","C":"15","D":31])。
  在一个函数的一变量声明中,它用于分隔同类不同名的变量名,在函数命名后()里的参数也是逗号相隔。当然这里有一处例外,就是在一些mapping型函数里,如果是采用set的方式,在总的映射名与后面的各项关键字之间也是用的是逗号分隔的,比较常用到的如:
  set("exits",([......]));
  二、分号:;  分号表示一个完整的语义讲完了、执行完毕。每一个分号前的话都有一定的独立的意思。因此,在某一个独立的变量内部是绝对不会出现分号的。
  三、冒号::,冒号一般用在三个地方,一是单独使用时,常常用在映射(mapping)里,表示将冒号右边的值赋给左边。左边的叫关键字,右边的叫做内容值。 二是与?合用,例如:
  A?b:c
  在这里,A是一个条件表达式,如果A成立的话、或者是真的话,就会返回冒号左边的b值,如果不成立,则返回冒号右边的c值。这种写法用在一些简单的判断里,可以省去很长的if else。
  第三种情况是在swtich()语句里,放在case <某一项>的后面,表示,如果swtich()里的可能是这某一项时的情况。例:
  swtich(random(10))
  {
    case 1:
  ....... ......
  最后再说一下,在程序中,象if() else() switch() 这样的判断语句后面直接跟着{},不需要加间隔符号。而且如果{}里面的内容只有一行的话,这对{}可以省略。例:
  if(me->query("age")>45)
  {
    write("it is good!\n");
  }
  就可以写成:
  if(me->query("age")>45)
    write("it is good!\n");
  
  再下来就是一些逻辑符号了,象&&表示并且、||表示或者、=表示赋值。
  运算符号,+-*/也就是我们四则运算了。
附录:常见编译出错信息
均以/u/llm/npc/test.c文件为例:
一、编译时段错误:/u/llm/npc/test.c line 13: parse error
  parse error一般表示错误出在基本的拼写上,多是象逗号、分号写错,或者是各种括号前后多写或漏写的情况,可以在提示的第13行或之前的几句里找一找;
二、编译时段错误:/u/llm/npc/test.c line 13: Undefined variable 'HIY'
  Undefined variable表示有一些未曾定义、不知其意义的东西存在。后面跟着的就是这个不明意义的字串。象这句就表示不知道第13行中的'HIY'是何意思。这个错误有三种可能,一是将一些变量的词拼错。比如,本来定义的是"HIT",结果写成"HIY"。二是因为这个变量未曾定义或者根本就没有声明,第三种情况是这个变量是定义在一些继承文件里,但在这个文件里却忘了继承。象这行就是最后一种情况,是这个文件前没有#include ,因为表示亮黄色的HIY是在/include/ahsi.h文件里定义的。
三、重新编译 /u/llm/npc/test.c:错误讯息被拦截: 执行时段错误:*Bad argument 1 to call_other()
  这句在开头,一般是指这个文件里在调用其它文件里的函数或者是对象时发生错误了。这时你可以接着往下看。一些与其它文件相关的错误信息全部跳过去,直接找有关这个 test.c文件相关的错误信息,然后找到比如象这样的信息:
  程式:/u/llm/npc/test.c 第 47 行 。那么就仔细查看第47行调用的东西有无问题。
四、重新编译 /u/llm/npc/test.c:错误讯息被拦截: 执行时段错误:F_SKILL: No such skill (froce)
  这个错误很明显的,肯定是在设置武功时把force写成了froce,系统当然找不到froce这样的skill了。
五、重新编译 /u/llm/npc/test.c:编译时段错误:/u/llm/npc/test.c line 75: Type of returned value doesn't match function return type ( int vs string ).
  这句表示在某一个函数里,返回值的类型与定义的不同,并指出是因为string与int的错误,到75行附近检查吧。
六、重新编译 /u/llm/npc/test.c:编译时段错误:/u/llm/npc/test.c line 72: Warning: Return type doesn't match prototype ( void vs int )
  这句也表示错在函数类型上了,只不过是因为函数与前面的声明相冲突,一个是int,一个是void。
七、重新编译 /u/llm/npc/test.c:编译时段错误: /u/llm/npc/test.c line 5: Cannot #include ansii.h
  很明显,在第5行处想要继承的文件并不存在。是不是自己写错了?
  
  后记:写完这篇《补遗篇》,这册《新巫师入门手册》就算结束了吧。相信你将这五章都真正看懂,并理解了之后,做一个日常维护的巫师也就可以了,而对于写一些简单的场景、NPC更不在话下了。有什么意见与想法将点击左下角的巫师信箱,给我来信。我们在以后的有关中级教材里再见面吧!
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|北大侠客行MUD ( 京ICP备16065414号-1 )

GMT+8, 2024-3-29 01:33 AM , Processed in 0.017738 second(s), 14 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表