    這篇說明是為了新上任的巫師所寫的, 我假設讀著這篇說明文件的
新巫師已經讀過 help new_wiz 中的內容, 並對巫師專有的指令如 clone,
update, cd, ls, cp... 等能熟練地加以使用, 但對如何開始寫作自己
的區域感到茫然, 不知所措的新進巫師。

[在開始製作之前]
    讓我們大略看一下在 LP MUD 中, 世界的構成方式。這個世界是由
一個個的物件 (object) 所組成, 每個物件有一個對應的程式檔案來描
述它的特性。我們可以藉由寫作一段程式來創造出一個全新的物件, 可
以利用 update 來更新物件所屬的程式, 用 clone  來實際造出一個可
用的物件。
    在這裡, 我們有各式各樣的物件, 但是可以將之區分為三大類: 房
間 (ROOM), 物品 (OBJECT), 與生物 (LIVING)。 在我們製作區域的慣
例上, 我們習慣將房間的檔案直接擺在區域的目錄下, 生物與物品則擺
在這個區域中名為 monster 及 obj (也有人喜歡用 object 或 item
為名, 看個人習慣。) 的子目錄中。
    以下我將以這三大類物件來分別說明該如何實作出區域。但在這之
前先提出一個忠告: 最好讓你的每個檔案都 #include 一個自定的 .h
檔, 然後在這個 .h 檔內做一些 #define 來定義檔案的絕對目錄。例
如

#define EGA="/u/e/ega/"
#define MOB="/u/e/ega/monster/"

如此一來在有需要用到房間或是怪物檔名時, 可以用 EGA"chat_room"
或是 MOB"troll" 的形式來表示。這除了可以讓你少打不少字以外, 對
於以後要將整個區域搬家搬到某個目錄下時會產生相當大的便利。

[如何製作房間]
    一個房間必定繼承了 ROOM (inherit ROOM), 這是在
<mudlib.h中被 #define 好的一個檔案, 它表示一個特別的檔案, 所
有的房間都必須繼承它. 才能擁有屬於房間的一切特性。

    一個房間有三個非常重要的函式: create(), init(), 與 refresh()
這三個函數會在某些特定的時機被系統所呼叫, 並且可以由你自行改寫
, 以達成千變萬化的效果。

[create 函數]
。 create() 是房間在一被創造出來時必定要呼叫的一個函數, 通常我
們都在裡面做一大堆設定初值的動作。隨便找一個房間來看, 我們可以
發現 create() 函數中總是有一大堆的 set("something", somevalue);
這些 set 的意義在此不詳述, 你可以自己猜,  也可以問問較資深的巫
師。
    有時你的房間並不直接繼承 ROOM。而是繼承了一個有繼承 ROOM 的
特別房間, 像是商店或是公會房間什麼的。這時候你所寫的 create()
函數會覆蓋掉原先繼承來的房間中的 create() 函數, 而導致不能正常
的動作。這時候你最好在你的 create() 函數中加入 ::create();  這
行指令, 它表示要去執行原先繼承來的那個 ROOM 中的 create() 函數
。
    如果你有寫 refresh() 函數, 那麼在 create()  函數的最後加上
一行 refresh(); 來呼叫它會是個好主意。

[init 函數]
    init() 函數被呼叫的時機在於有生物 (怪物及玩家)  進入這個房
間的時候。這時有個常用的函數 this_player()  會傳回走進房間的這
個人, 或是怪物。this_player() 的概念容後再談, 你現在只要記住這
個函數在每個生物走進來時都會被呼叫一次就可以了。

    在 init() 中最常見的的函數莫過於 add_action("function", "action");
了。它的作用是在進來的生物身上添加上一個指令 (注意, 系統只認指
令的第一個字), 並在玩家下達這個指令時去呼叫 "function"  中所給
定名稱的函數。舉例而言, 如果我們寫了這樣的 init():

init() {
        add_action("do_climb", "climb");
}

當玩家走進這個房間時, 系統會幫他多出 climb 這個指令。當他下達
了 climb tree 這個指令時, 系統會去尋找 do_climb() 這個函數, 並
加以執行。同時, 系統會將玩家所輸入的 "climb"  這個指令後的所有
文字以字串型別的引數傳給 do_climb()。 你可以將 do_climb 這個函
數宣告為 int do_climb(string s)。 這樣一來, 當玩家下達 climb
tree, 或是 climb the red wall 這種指令時, "tree" 或是 "the red
wall" 就會被存進字串變數 s 之中供你處理。
    由 add_action() 所宣告的函數必定要是一個整數型別的函數, 因
為系統會根據這個函數的傳回值採取不同的動作。如果你傳回的是 0,
那麼系統會認定這個命令與你這個處理函數無關, 而對其他也有 climb
命令的函數一個一個嘗試著去執行, 直到所有的 climb 命令都傳回 0
時, 系統會當這個指令不合法, 而丟出一個錯誤訊息(what?) 給玩家。
    若你的函數傳回值為 1, 表示這個指令已經由你所寫的函數處理掉
了, 系統不會再嘗試著往下面繼續尋找其他的 climb 指令。
    在你的函式偵測到玩家輸入的引數有問題時 (例如你要他們 climb
 tree, 但他們卻輸入了一些錯誤的指令如 climb three 之類的)。 想
給他們一些特別的錯誤訊息時, 你可以用 notify_fail(string errormsg)
來寫這個訊息, 如 notify_fail("climb what?\n");   notify_fail()
這個函數也是 int 型別, 固定會傳回 0, 所以我們最常用的寫法是:

if (條件不合)
    return notify_fail(錯誤訊息);
if (另一個條件不合)
    return notify_fail(另一個錯誤訊息);
.............................
<所有可能導致錯誤的輸入都過濾光了>
開始真正幹活的部份....
return 1;


[refresh() 函數]
    refresh 呼叫的時機是系統定時 (約每半個小時一次) 呼叫。主
要的用途在於房間中怪物、物品的再生。如果你改寫了 refresh()
函數, 千萬記得要串接 ::refresh(), 否則可能導致嚴重的後果 (門
一打開就不會自動關上, 怪物打死後也不會再生...
    由於有 set("objects", (["name1" : "file1", "name2" :
"file2",... ]) ); 這種寫法的存在 (在 create() 裏面這麼寫) 所
以 refresh() 被用到的機會不多了。 (因為 set("objects", ) 這
個寫法可以幫你作出自動定時 refresh 怪物、物品)  但是在製作一
些必須定時回復原始狀態的小機關時, 仍然有必要用到這個函數。

有關房間的部份就寫到這裡, 接下來是物品。

[物品的製造]
    要製作物品, 首先必須 inherit OBJECT;。理由與做房間時必須
inherit ROOM 一樣。OBJECT 是最基本的物品, 如果你要做的東西是武
器、防具、地圖等, 你必須 inherit WEAPON, ARMOR, MAP 等等才能獲
得這種類別的物品所擁有的特性。現在請注意一下, 我們現有的系統在
這裡有些小臭蟲, 在你 inherit WEAPON 或其他子類別時, 請你把 inherit
OBJECT 這行消掉, 否則會造成錯誤。同時, 你不可以同時繼承 WEAPON,
又繼承 ARMOR, 這也會造成錯誤, 理由跟上面的錯誤相同。
    物品的重要函數只有 create() 與 init(), 作用與 ROOM  中的同
名函數大致相同。

[create() 函數]
    要寫 create() 函數, 最好的方法是拿現成的同類物品來修改。因
為不同類的物品往往可以 set 不同的屬性, 而且特性極多, 有重量、
價格、攻擊力(武器)、防禦能力(防具)、使用壽命(火把)等等。很難記
得完整, 所以我勸你找一個較完整檔案來修改。

[init() 函數]
    與 ROOM 中的 init 函數類似, 但是被呼叫的時機多了許多, 共有
下列的幾種情況:
1. 物品擺在房間中, 有一個玩家走進來。
2. 一個物品突然出現在某個玩家所在的房間中。
3. 一個物品突然出現在某個玩家的物品欄中。
物品的 init 函數大多還是用在寫 add_action 上面, 這些 action 會
生效的場合歸結起來很簡單, 就是:

    「玩家用 l 或是 i 指令看得到這個物品的時候」

同一個房間中他人或怪物身上的東西時不算, 裝在袋子的東西不算。這
點要注意一下。

[怪物]
    簡單的怪物很好做, 連 init 都不用寫, 只需要寫 create(),  唯
一的問題是屬性太多了, 要一一理解得花上相當的時間才行。

    會做複雜動作的怪物則需要相當的技巧, 並且了解有哪些變數可以
被攔截下來改寫利用。等你有一定的程度時, 再來找個怪物參考參考較
好。

    怪物要 inherit MONSTER; 它也沒有 refresh() 這個函數。

[程式必須的概念]
    你必須了解, 在 LPC 中最重要的一個概念是物件(object) 當你想
做任何動作時, 都要考慮到這個動作是哪一個 object 所做的, 不然很
容易導致錯誤。LPC 的語法並不嚴謹, 有些場合為了省事可以將函數是
由哪個物件所作的省略掉, 例如我們在 create() 函數中最常看到的
set(), 事實上最嚴謹的寫法應為 this_object()->set()。write() 則
為 this_player()->write()。
    說這麼多只是為了強調一件事: 你能抓出一個物品的 object 變數
就能讓他幹一切他所能做的事。

[this_object() 與 this_player()]
    這兩個函數是系統所提供的函數, 也是最最好用的兩個函數。在你
寫作一個物件 (房間、物品...etc.)時, this_object() 表示自己這個
物件。
    this_player() 則比較複雜, 它會傳回一個屬於玩家型別的物件。
這個玩家在 init 中就是觸發 init 的那個玩家。this_player() 會跟
著函數呼叫一直傳遞給所有被 init 呼叫的函數, 包括 add_action 中
所定義出來的函數, 在這些函數中, this_player() 就是表示做動作的
那個人。

[present() 函數]
    常常, 我們只知道一個物件的名字, 卻不能用個 object 型別的變
數指向它。 object present(string "id",object env) 函數在此時就
可以派上用場, 你給定你要找的物件的名字, 與它的所在地 (某個房間
或某個人), 函數就會傳回他所找到的物件。
    簡單的想, 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 的下一個物品, 在同一個 environment 中。
    all_inventory(object ob) 類似於 first_inventory(),  但是它
所傳回的是包含了所有物品的一整個陣列。

[更進一步的提示]
    LPC 的函數群有三個, efun, lfun, simul_efun。它們提供了絕大
部分的功能。有空不妨多多 help efun, help lfun... 看看, 這會給你
帶來非常大的收穫。

[關於輸出輸入訊息的各個函數的提示]
can_read_chinese
printf, sprintf
scanf, sscanf
write, say, shout
tell_object, tell_room

[關於物件操作的函數]
clone, new
destruct, remove
move, move_player, move_around

講解就到此結束, 如果有不對的地方以後再修正。
--
───────────────────────────────
朦朦迷霧中, 一帆船影帶著詭異的氣氛緩緩地靠岸。
而後兩盞魔火飄了過來, 在你面前輕輕一點, 領著你往船走去。
該接受它的邀請嗎?
E.G.A.                          E-mail u7911008.cc.nctu.edu.tw

--
發信人: Lazyrat.bbs@csie.nctu (懶鼠), 信區: mud
標  題: 給 ES 的新巫師們[5](完)
發信站: 交大資工鳳凰城資訊站 (Mon Jun  6 10:34:46 1994)
轉信站: star!bbsroute!phoenix
※ 轉寄:‧台北科技大學紅樓資訊站 redbbs.cc.ntut.edu.tw‧
