第 五 章


補 遺 篇

第一節:變量


  首先,我發現新巫師們編程結束后,一旦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更不在話下了。有什么意見與想法將點擊左下角的巫師信箱,給我來信。我們在以后的有關中級教材里再見面吧!
                  叮當(2000年7月)