背景:
阅读文章

《巫师入门教程》

补遗篇

[日期:2007-09-10] 来源:谁与争锋/叮当  作者:非凡公子 [字体: ]

    一共四章的《新巫师入门手册》写出去以后,叮当一直有一种诚惶诚恐的感觉。因为我无论在接触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 <ansi.h>,因为表示亮黄色的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更不在话下了。有什么意见与想法将点击左下角的巫师信箱,给我来信。我们在以后的有关中级教材里再见面吧!


 

【内容导航】
第1页: 第2页:观念篇
第3页:上手篇 第4页:理解篇
第5页:见习篇 第6页:补遗篇
收藏 推荐 打印 | 录入:sbso | 阅读:
相关内容      
本文评论   [发表评论]   全部评论 (0)
内容推送
52mud提供
一起回忆泥巴游戏QQ群68186072
52mud官方微信公众平台
热门评论