[轉載] [蟑螂賀新年] 基礎 LPC 簡介+1~7章

看板mud_sanc (Sanctuary - 聖殿)作者 (小太保)時間17年前 (2009/03/06 11:16), 編輯推噓0(000)
留言0則, 0人參與, 最新討論串1/2 (看更多)
作者 spock.bbs@bbs.csmc.edu.tw (打混的蟑螂史巴克), 看板 Mud 標題 [蟑螂賀新年] 基礎 LPC - 簡介 時間 中山醫學院BBS站 (Mon Jan 26 14:27:10 1998) ─────────────────────────────────────── 基礎 LPC 撰稿: Descartes of Borg 23 april 1993 簡介 如何使用此份手冊及使用的名詞 最近, 在 USENET 上面, 我看到許多人尋找 LPC 的使用手冊. 而且在我 mud 上的神族 (immortals) 也曾經告訴我 Nightmare 的架設文件有多好. 但 是在那些文件裡面, 並沒有適當地解釋 LPC 程式語言. 所以我決定試著撰寫一 份使用說明. 有些事情你必須謹記在心. LPC 是一種非常易於學習的程式語言, 而它真正的價值也如我們在現實世界 所知的一樣. 我從 1991 年開始玩 mud, 並於一個月內, 在名為 Orlith 的原 Bates College MUD 中, 創造出一個不起眼的區域和樂師公會. 之後, 我搬到洛 杉磯 (Los Angeles) 一年, 完全沒有碰電腦或玩 mud. 在 1992 年六月, 我回 到 Internet 並擔任 Igor 的巫師. 在 1992 年九月, 我開始撰寫 Nightmare mudlib 以符合我們的需要. 因為當時 MudOS 上並沒有任何 mudlib 能讓人直 接拿來跑, 所以後來決定把它公開出來 (當然, 現在可不是這樣 :)). 所以我只有不到一年的時間認真地撰寫程式. 如同主修資訊科學的人他們的 哲學, 我只想搞清楚, 要完全搞懂 LPC 程式寫作, 除了掛在 mud 裡頭以外, 並不需要拿你的電腦來作所有撰寫程式的事. 在此份使用手冊裡, 我們假設: 有人已經教過你最基本的 UNIX 命令, 例如: ls, cs, mkdir, mv, rm 等等. 你知道如何進入你的 mud 中的文字編輯程式, 並且儲存一個檔案. 除此以外沒 有其他的要求. 如果你熟悉 C 語言, 你反而會發現 LPC 雖然很像 C, 卻又不 是 C. 你以前對於模組化程式設計發展 (modular programming development) 的觀念還會扯你後腿. 如果你從來沒聽過 C 程式語言 (像我在 1991 五月那時 一樣) , 那你只缺基本的 C 結構, 像是程式執行的流程、邏輯運算子的規則等 等東西. 所以先前學的 C 對你而言並非有利, 因為能夠從 C 拿來用在 LPC 上的東西, 要學起來非常容易. 熟悉 C 跟 LPC 一點關係也沒有. 這份手冊分成若干章節, 表示你應該按順序閱讀它們. 先讀這份簡介, 再按照 chapter 那個檔案裡面所列的目錄依序閱讀. 每一章開始都有一個或兩個段落, 解釋你在該章節所應該了解的東西為何. 在這些簡介的章節之後, 就開始針對此 章節的主題作令人厭煩的詳細解釋. 在各章結尾, 會就你應該從此章中學到的東 西下個簡短的結論 (如果我寫的內容能讓你了解的話) . 接著, 也許會有一些跟 主題有關的註解, 而你並不需要去搞清楚註解內所講的東西. 如果你按照順序一章章研讀過來, 碰到某一章的簡介說你這時應該懂某些東西, 而你卻搞不懂的時候, 請寄信給我吧 ! 很顯然, 我在這裡寫的內容沒收到該有 的效果, 我得弄清楚我哪裡寫錯, 才能把錯誤的地方訂正過來. 如果某一章的總 結說你在看完該章後應該學會什麼而你沒學會, 一樣寄封信給我吧. 如果你的 mud 在 intermud 系統裡, 就寄給 descartes@nightmare. 不然, 寄給 borg@hebron.connected.com 即可. (譯按: 臺灣的 intermud 系統通常寄不到 美國的 mud. 而 borg@hebron.connected.com 是 1993 的地址, 已無法使用, 請改用 borg@imaginary.com ) 一些手冊中用的基本名詞: driver (驅動程式) - 這就是遊戲的 C 程式. 它接聽 socket (與其他電腦通訊) 、翻譯 mudlib 定 義的 LPC 程式碼、管理記憶體中的 mud 物件 (object) 、定期試著從記憶體 裡面把一些沒用的 mud 物件清掉、定期呼叫物件等等, 都由它負責. mudlib (mud 函式庫) - 定義 mud 世界的 LPC 程式碼. driver 本身不是遊戲. 它只是一個創造多人 環境的程式. 就某方面來說, driver 就像是一個 LPC 的編譯程式 (compiler) , 而 mudlib 像是編譯程式的函式庫 (library) (這樣比喻不太好). mudlib 定義一些基本的物件, 而創造 mud 世界的人重覆使用這些物件. 舉例來說, 像 是 /std/room (或 /room/room)、 /std/user.c (或 /obj/player.c) 等等物件 都是. area (區域) 或 castle (城堡): mud 創造者撰寫的某些物件, 這些物件用到 LPC「繼承」(inheritance) 的功能 , 具有基本 mudlib 物件的特性, 並轉換成玩家在遊戲裡面使用的物件. object (物件): 房間、武器、怪物、玩家、袋子等等所有的東西. 更重要的是, 每一個名字結尾 是 .c 的檔案都是一個物件. 每個物件有不同的用途. 像是 monster.c 和 user.c 這兩個物件繼承 /std/living.c 這個基礎物件. 而基礎物件以外的物 件則有的拿來複製, 也就是在記憶體中再載入一份相同的程式碼; 有的則只是載 入記憶體, 而被其他的物件拿來呼叫 (reference). native (原始) 及 compat (精簡): 這兩個名詞與最常用的兩種 driver 有關係. 原始模式的 mudlib 用於 LPMud driver 3.0 以後的版本. 精簡模式指的是: 你可以拿一個 2.4.5 型式的 mudlib 配合 3.0 的 driver. 原始模式的 mudlib 指的是 MudOS、CD、LPMud 列出的原始模式 mudlib. 而精簡模式的 mudlib 指的是 3.0 以前的 LPMud mudlib 和 3.* 精簡模式 mudlib. 我認為 Amylaar 的 mudlib 屬於精簡模式. 祝你順利 ! George Reese (Descartes of Borg) 12 july 1993 borg@hebron.connected.com (譯按: 已改為 borg@imaginary.com) 譯者: Spock of Final Frontier 97.Dec.21. 第一章: 程式撰寫環境的簡介 1.1 UNIX 檔案結構 LPMud 使用基本的 UNIX 命令及檔案結構. 如果你已經了解 UNIX 的命令, 請注 意 (除了幾個例外) 命令無法指定選項 (options). 跟 DOS 一樣, UNIX 也使 用階層式 (heirarchical) 的目錄結構. 所有的次目錄 (sub-directories) 都 附屬於根目錄 ( / , root) 之下. 而每個次目錄之下也可以有更多的次目錄. 一個目錄可以有兩種表示方法: 1) 用目錄的全名 (full name), 或稱作絕對名稱 (absolute name). 2) 使用相對名稱 (relative name). 絕對名稱就是從根目錄一路寫下來, 直到該目錄的名字為止. 舉例來說: /players/descartes/obj/monster 就是根目錄 (第一個 / 號) 之下的 player 目錄之下的 descartes 目錄的之 下的 obj 目錄之下的 monster 目錄. 相對名稱使用的是相對於其他目錄的名字. 以上面的例子來說, 相對於 /players/descartes/obj, 這個目錄叫作 monster; 對於 /players/descartes 來說, 這個目錄叫 obj/monster; 對 /players, 同一個目錄叫作 descartes/obj/monster; 最後, 對 / 來說, 此目錄叫作 players/descartes/obj/monster. 你可以看出來, 絕對名稱與相對名稱之間的 不同之處在於絕對名稱總是從 / 開始. 而你如果要知道一個目錄的相對名稱, 就得搞清楚是相對於哪個目錄. 一個目錄可以包括一些次目錄和檔案. LPMud 只使用 mudlib 裡面的文字檔案. 就如同目錄一樣, 檔案也有絕對與相對名稱. 最基本的相對名稱是該檔案的名字 .. 去掉檔案名字之後, 剩下的絕對名稱就是路徑 (path). 拿一個檔案舉例: /players/descartes/castle.c , 則 castle.c 是檔名, /players/descartes 則是其路徑. 在其他的 mud 裡, 用普通的檔案列表命令列出檔案時, 檔名開頭是 . 的檔案 (像是 .plan) 是看不到的. 1.2 UNIX 命令 跟 UNIX 檔案結構一樣, LPMud 也使用許多的 UNIX 命令. 大部份的 mud 中 , 使用的典型 UNIX 命令有: pwd, cd, ls, rm, mv, cp, mkdir, rmdir, more, head, cat, ed. 如果你從來沒見過 UNIX 命令, 你大概會覺得這些命令沒啥意義. 好吧, 它們的 確沒有意義, 但是你一定用得到它們. 在我們搞清楚它們是什麼東西之前, 先來 討論目前目錄 (current directory). 如果你熟悉 DOS, 那你就知道什麼是目前 工作目錄 (current working directory). 不管何時, 你一定在某個目錄裡面. 這表示, 你在 UNIX 命令裡面所給的任何相對檔案名稱或相對目錄名稱, 都相對 於你現在所處的那個目錄. 譬如說: 如果我的目前目錄是 /players/descartes , 而我輸入 "ed castle.c" (ed 是編輯檔案的命令), 那它就假設我指定的是 /players/descartes/castle.c 這個檔案. pwd: 顯示你目前所在的工作目錄. cd: 改變你目前的工作目錄. 你可以給它相對或絕對路徑名稱. 如果沒有指 定參數 (argument), 就切換到你自己的家目錄 (home directory). ls: 列出一個目錄裡面所有的檔案. 如果不指定目錄, 則列出目前工作目錄 的所有檔案. rm: 刪除指定的檔案. mv: 更改指定檔案的名字. cp: 複製指定的檔案. mkdir: 製作新的目錄. rmdir: 刪除一個目錄. 該目錄裡面的檔案必須先全部刪除才行. more: 分一頁一頁閱讀一個指定的檔案, 這樣你的螢幕上會一次顯示一頁. cat: 一次就把所有的檔案內容全部倒給你. head: 顯示檔案的前面幾行. tail: 顯示檔案的最後幾行. ed: 讓你能用 mud 的編輯程式編修一個檔案. 1.3 本章總結 UNIX 使用樹狀的階層式檔案結構, 而這棵樹的根部叫做 / (根目錄 root). 從 根目錄分支出去的目錄, 和這些目錄自己分出去的目錄就叫作次目錄 (sub-directory). 任何目錄都可以包含檔案及目錄. 目錄和檔案都能使用以 / 開頭的絕對名稱, 或相對於其他目錄的相對名稱. 你可以使用一些典型的 UNIX 命令來使用 UNIX 的檔案結構. 像是: 檔案列表、顯示目前工作目錄、等等命令. 在你的 mud 上, 上面的那些檔案都應該有詳細的命令說明, 讓你能搞懂那些命 令到底是做些什麼的. 另外, 也該有一份 mud 編輯程式的詳細說明檔案. 如果 你沒用過 ed, 你應該詳細閱讀那份說明檔. 譯者: Spock of Final Frontier 97.Dec.23. 第二章: LPC 程式 2.1 關於程式 這一章的名字取得不怎麼好, 因為沒有人用 LPC 寫程式. 寫 LPC 程式的人寫 的是物件 (objects). 這兩種說法有啥差別 ? 好吧, 就我們現在的目標來說, 差別在於兩者檔案執行的方式不同. 當你「跑」一個程式的時候, 都是從程式中 固定的地方開始執行. 換句話說, 就是所有的程式開始執行的時候, 一定有個地 方寫清楚要從那裡開始. 另外, 程式有一個固定的終止點, 所以執行程式只要執 行到該終止點, 程式就中止執行. 總之, 程式從固定的開頭跑到固定的結尾. LPC 物件就不是這麼一回事. 在 mud 裡面, LPC 物件只是遊戲 (driver) C 程式中, 顯而易見的部分. 換句 話說, mud 程式在 driver 裡面開始與結束執行. 但是實際上, 對於創造你玩的 mud 世界來說, driver 並沒有做多少事. 反之, driver 相當依賴 LPC 碼, 並 需要執行物件中的程式碼. 所以 LPC 物件不需要有起始點, 也不需要有固定的 終止點. 就像其他的程式語言, LPC 「程式」可以由一個或一個以上的檔案組成. 很簡單, 程式要先載入 driver 的記憶體. driver 會根據本手冊所教的結構, 讀取物件 中一行行的程式. 有一件重要的事你要先搞清楚, 就是 LPC 物件執行時沒有開 頭也沒有終止. 2.2 diiver-mudlib 之間的互動 我先前提過, driver 是在主機上執行的 C 程式. 它讓你連上遊戲, 並執行 LPC 碼. 注意, 這是 mud 程式設計的一個理論而已, 也不需要比其他的方法好. 整個 mud 遊戲可以全部用 C 來寫. 這樣遊戲的執行速度快上很多, 卻讓 mud 缺乏可塑性, 使巫師在遊戲正在執行的時候無法加入新東西. DikuMUD 就是全部 用 C 寫成的. 相反的, LPMUD 的理論就是 driver 不該決定遊戲內容, 而遊戲 內容應該決定於遊戲中的個別事物, 並能夠在遊戲執行時加上東西. 這就是為什 麼 LPMUD 使用 LPC 程式語言. 它能讓你用 LPC 定義遊戲內容, 交給 driver 依需要讀取並執行. 況且學 LPC 要比 C 容易得多, 這樣讓更多人能加入創造 世界的過程. 一旦你用 LPC 寫了一個檔案 (假設是用正確的 LPC), 它只是躺在你主機的硬 碟裡不動, 直到遊戲中有東西參考 (reference) 它. 當遊戲中有東西終於參考 到它時, 這個檔案就會被複製一份到記憶體裡面, 並且呼叫這個物件中一個特殊 的函式 (function). 呼叫這個函式的目的是初始化 (initialize) 這個物件中 的變數. 現在, 別管你腦袋裡才看到的上兩句話, 因為一個對程式設計完全陌生 的人來說, 哪裡會知道函式或變數到底是啥東西. 現在重要的是要知道 driver 讀取主機硬碟裡面的物件檔案, 複製一份之後扔進記憶體儲存 (既然是複本, 也 就可以有許多不同的版本 ). 你稍後會知道什麼是函式、什麼是變數, 並搞清楚 到底遊戲中的一些東西是怎麼參考你的物件的. 2.3 將一個物件載入記憶體 雖然一個物件裡面並沒有規定要從一個固定的地方開始執行程式, driver 卻要 先找到一個固定的地方並執行之, 才能初始化一個物件. 在精簡模式的 driver 上, 這是一個叫作 reset() 的函式. 在原始模式 mud 中, 則是 create(). LPC 物件是由變數 (variable) 所組成的 (會更改的值) 而函式是處理這些變數 的程式. 函式經由 LPC 語法結構來處理變數, 語法結構包括: 呼叫其他函式、 使用外部定義函式 (externally defined functions, efuns)、基本的 LPC 運 算式 (expression) 和流程控制 (flow control mechanism). 前面這些聽起來亂七八糟的吧 ? 讓我們從變數開始著手. 拿「等級」變數來說 吧, 等級可以隨情形不同而改變它的數值, 而不同的事物也使用玩家的等級數字 作出不同的事. 舉個例: 如果你是等級十九級的玩家, 則等級變數的數值就是 19 . 如果你的 mud 是舊的 LPMud 2.4.5 系統, 等級 1 到 19 級是玩家, 20 級以上是巫師, 則會有許多事物會詢問你的等級變數值, 判斷你能不能使用 巫師的動作. 基本上, 任何 LPC 物件就是一堆會隨時間不同而改變的變數組成 的. 發生在物件身上的事, 都基於該物件的各個變數裡頭的數值. 而常常也有許 多事會更改變數. 所以無論何時, 一個 LPC 撰寫的物件被其他在記憶體的物件拿來參考時, driver 就尋找這物件裡面所要找的值在哪裡 (但是現在還沒有任何數值) . driver 找 過之後, 就呼叫物件中的 reset() 或 create() 函式 (視不同 driver 而定) , 來設定該物件一開始的變數值. 就這樣, 經由「呼叫」「函式」處理變數. 雖然絕大多數的 LPC 程式碼都從 create() 或 reset() 開始執行, 此處卻不 是 LPC 程式碼開頭的地方. 事實上, 沒有這兩個函式也沒關係. 如果你的物件 一開始所有的值都是 NULL (虛無) 指標 (在此, 虛無指標我們先當它是 0 吧) , 那你就不需要 create() 或 reset() 函式. 所以, 每個物件開始執行程式碼 的地方都可能完全不同. 現在讓我們搞清楚這整章在講些什麼. 問題是: 一個完整的 LPC 到底是由哪些東 西組成的 ? 好, 一個 LPC 物件簡單來說, 就是一個或一個以上的函式組合起來 , 處理一個以上的變數 (或是不處理變數也行) . 各個函式之間完全不用管它們 擺的先後順序. 換句話說: ----- void init() { add_action("smile", "smile"); } void create() { return; } int smile(string str) { return 0; } ----- 跟底下的一樣: ----- void create() { return; } int smile(string str) { return 0; } void init() { add_action("smile", "smile"); } _____ 另外有個很重要的事提醒你, 下面這個物件只有: ----- void nonsense() {} ----- 這樣也可以, 但是這種微不足道的物件, 它大概不會與你的 mud 中的其他物件 作出正確的互動關係, 因為這樣的物件沒有重量、看不到......以此類推. 2.4 本章總結 LPC 碼沒有起點或終點, 因為 LPC 碼是用來創造 driver 程式使用的物件, 而 非單獨的程式. LPC 物件包括一個或多個函式, 其間先後順序毫無關係, 而這些 函式之中, 處理多個變數 (或根本沒有任何變數) . LPC 物件只是躺在主機的 硬碟裡面, 等著遊戲中其他的物件參考它 (換言之, 它們實際上不存在) . 一 旦一個物件被參考到, 它會被載入記憶體中, 並且它所有的變數都是零. 精簡模 式 mud 呼叫此物件的 reset() 而原始模式 mud 呼叫 create() (如果此物 件有這些函式的話 ), 讓這些函式來指定一些變數的初始值. 物件中的其他函式 由 driver 或遊戲中其他的物件使用之, 讓物件之間達到互動並處理 LPC 變數. reset() 和 create() 的說明: 只有原始模式的 mud 使用 create() (請見本手冊的 Introduction 一章, 有關 原始模式和精簡模式的介紹). 此函式僅用來初始化剛被參考的物件. 原始模式及精簡模式的 mud 都使用 reset() 函式. 在精簡模式 mud 中, reset() 有兩個功能. 第一, 它用來初始化剛被參考的物件. 第二, 在精簡模式 的 mud 中, reset() 用來重新設定物件. 也就是說, 讓物件回到最初的狀態. 這樣可以讓一個房間內的怪物重生, 或把一道門關回去......以此類推. 原始模 式的 mud 只用 reset() 作第二種功能 (就跟 reset 的意思一樣). 所以在 LP 式的 mud 中有兩件重要的事情讓 driver 呼叫物件中的函式. 第一 件事是創造物件. 此時, driver 呼叫物件中的一個函式來初始化物件的變數值 .. 在精簡模式的 mud 裡, 由 reset() 做此工作 (要加上 0 參數, 後面的章 節再討論參數是啥). 原始模式的 mud 下, 由 create() 做此工作. 第二件事是把房間重新設定回某些基本的狀況. 這些基本的設定可能會與一開始 的初始值不同, 也可能相同, 而你當然也不想花時間一直去重覆做某些事 (像是 重新設定一些不會更改的變數) . 精簡模式的 mud 用 reset() 函式來創造和 重新設定物件. 而原始模式的 mud 用 create() 創造物件, 用 reset() 重新 設定物件. 但是精簡模式也不會失去所有的變數值, 因為有個方法能區分是創造 物件還是重新設定物件. 在精簡模式要重新設定, 則 driver 傳入 1 或重新設 定的數字當作 reset() 的參數. 現在這個對你來說沒啥意義, 但是要記住, 你 在精簡模式實際上是可以把兩種情形區分開來的. 另外也要記住, reset() 在創 造物件時傳入的參數是 0, 而重新設定物件時傳入非零值. 翻譯: Spock of Final Frontier 98.Jan.16. 第三章: LPC 的資料型態 (data type) 3.1 你現在該知道的事 LPC 物件由零個或多個變數組合而成, 而這些變數由一個或多個函式組合而成. 在程式碼中, 這些函式的先後順序是無關緊要的. 當你寫的 LPC 第一次被參考 時, driver 把它複製一份到記憶體中. 之後, 還可藉此複製出更多相同的拷貝. 任何一份物件被載入記憶體時, 所有的變數一開始都指向「虛無值」. 精簡模式 mud 的 reset() 函式與原始模式的 create() 函式都都用於指定物件的初始變 數值. 物件載入記憶體之後, 會立刻呼叫創造的函式. 不過, 如果你讀這份課本 之前沒有寫過程式, 你大概不知道什麼是函式 (function) , 或函式是怎麼被呼 叫的. 就算你以前寫過程式, 你大概也想知道新創造的物件中, 函式之間互相呼 叫對方的過程是什麼. 回答以上這些問題以前, 你得多了解函式在處理什麼. 所 以你應該先徹底了解 LPC 資料型態背後的觀念. 說實在的, 在這份手冊裡頭最 無聊的主題, 也是最重要的主題, 90% 以上就是用錯 LPC 資料型態 (放錯 {} 和 () 不算在內). 所以說, 你得要耐心看完非常重要的這一章, 因為我覺得你 如果搞懂這一章, 可以讓你以後寫程式大大輕鬆不少. 3.2 與電腦溝通 你應該已經知道電腦不懂人類所使用的單字與數字. 電腦所說的「語言」由 0 與 1 的「字母」所組合而成. 當然, 你知道電腦不懂人類的自然語言. 但是實 際上, 它們也不懂我們寫給它們的電腦語言. 像是 BASIC、C、C++、Pascal 等等 , 這些電腦語言全都是過渡語言. 這些電腦語言讓你能把想法組織起來, 讓思考 更易轉換成電腦的 0 與 1 語言. 轉換有兩個方法: 編譯 (compilation) 和直譯 (interpretation) . 這兩個方 法的差別在於程式語言轉換成真正電腦語言的時候. 對編譯的程式語言來說, 程 式設計者撰寫程式碼之後, 使用編譯程式 (compiler) 把程式碼轉換成電腦真正 的語言. 程式在執行之前就已經轉換完畢. 而直譯的程式語言, 在程式執行的時 候才開始轉換. 因此直譯的程式語言所寫的程式執行起來要比編譯的慢上許多. 總而言之, 不管你用什麼程式語言撰寫程式, 最後都要轉變成 0 與 1 才能讓 電腦搞懂. 但是你儲存在記憶體中的變數並不是單純的 0 與 1. 所以你用的程 式語言要有個方法告訴電腦, 這些 0 和 1 到底要當作十進位數字、字元 (characters) 、字串 (string) 、還是當作其他的東西看待. 你可以靠著指定 資料型態來辦到. 舉例來說, 假設你有個變數叫做 x , 而你給它一個十進位的值 ── 65. 在 LPC 裡面, 你會寫出下面的敘述: ----- x = 65; ----- 你等一下再做像這樣的事: _____ write(x+"\n"); /* \n 符號代表在此換行 (carriage return) */ y = x + 5; ----- 第一行讓你送出 65 和換行到某個人的螢幕上. 第二行讓你把 y 設定為 70. 問題是你告訴電腦 x = 65; 時, 它不知道 65 到底是啥意思. 你認為是 65, 對電腦來說也許認為是: 00000000000000000000000001000001 而且, 對電腦來說, A 這個字母就是: 00000000000000000000000001000001 所以, 不管你什麼時候告訴電腦 write(x+"\n");, 電腦總要有個方法知道你想 看到 65 而不是 A. 電腦能透過資料型態了解 65 與 A 的不同. 資料型態只是說記憶體位置中儲存 的指定變數到底是屬於什麼型態的資料. 所以說, 每一個 LPC 變數都有變數型 態指導如何轉換資料. 在上面的範例裡, 你應該會在程式碼「之前」加上以下這 行: ----- int x; ----- 這一行告訴 driver 無論 x 指向何處, 都當作「int」 資料型態來使用. int 是整數 (interger, 或稱 whole number) 的縮寫. 現在我們已經初步介紹為什 麼要有資料型態. 這樣一來, driver 才能搞清楚電腦儲存在記憶體中的 0 與 1 到底是代表什麼意義. 3.3 LPC 的資料型態 所有的 LPMud driver 都有以下的資料型態: void (無), status (狀況), int (整數), string (字串), object (物件), int * (整數指標), string * (字串指標), object * (物件指標), mixed * (混合指標) 很多種 driver (不是全部) 有下列資料型態值得討論: float (浮點數), mapping (映射), float * (浮點數指標), mapping * (映射指標) 少數 driver 有下列罕用的資料型態, 並不值得討論: function (函式), enum, struct (結構), char (字元) (譯註: 目前台灣絕大多數的 LPMud 所使用的 driver 是 MudOS, 其資料型態 有些許不同之處. 請詳見參考譯者所翻譯之 MudOS 參考文件) 3.4 簡單的資料型態 這份簡介性質的課本會介紹 void, status, int, float, string, object, mixed 這幾種資料型態. 你可以在中階課本 (intermediate book, 譯註: 本作 者另外有寫一份中階 LPC 手冊, 譯者亦有翻譯) 找到像是 mapping (映射) 或 array (陣列) 這種更複雜的資料型態. 本章先介紹兩種最簡單的資料型態 (以 LPC 程式設計者的觀點來看) ── 整數 (int) 和字串 (string). int 表示任何整數. 所以 1, 42, -17, 0, -10000023 都是整數 (int) 型態. string 是一個以上的字元或數字. 所以 "a", "we are borg", "42", "This is a string" 都是字串. 請注意, 字串前後都要加上雙引號 "" , driver 才能分辨 int 42 和 string "42". 也才能區別變數名稱 (像是 x ) 與字串 (像是 "x" ). 當你在程式碼中使用變數, 你一開始要讓 driver 知道這個變數所指的是哪種變 數型態. 這種處理方式叫做「宣告」 (declaration). 你得在函式一開始的地方 宣告, 或是在物件程式碼的開頭之處 (在函式之外, 任何函式用到該變數之前). 要宣告變數型態的話, 只要像底下一樣, 把變數型態擺在變數的名字前便即可. ----- void add_two_and_two() { int x; int y; x = 2; y = x + x; } ----- 像這樣, 這是一個完整的函式. 函式的名稱是 add_two_and_two(). 函式一開始 宣告一個整數變數 x, 之後宣告一個整數變數 y. 所以, 在這裡 driver 有兩個 變數指向 NULL (虛無) 值, 而這兩個變數期待的變數值是整數型態. 關於虛無 (void) 和狀態 (status) 資料型態: 無 (void) 是一種很普遍的資料型態, 它不指向任何東西. 它並不是用在變數上面的 型態, 而是用於函式. 你稍後會了解這裡所說的事. 而現在, 你只需要知道 void 不指向任何值. 狀況 (status) 資料型態是布林 (boolean) 資料型態. 就是說, 它的值是 0 或 1. 這種值常常稱為真 (true) 或偽 (false). 3.5 本章總結 對變數來說, driver 需要知道電腦儲存在記憶體中的 0 與 1 要如何轉換成 你想使用的形式. 最簡單的 LPC 資料型態是 void, status, int, string. 變 數不使用 void 的資料型態, 但是這種資料型態用於函式. 另外, 資料型態用於 轉換格式, 決定 driver 應該使用哪種規則處理運算, 像是 +, - ......以此類 推. 舉例說, 運算式 (expression) 5+5, driver 知道 5 加上 5 的值是 10. 對字串來說, 對字串使用整數加法沒有意義. 所以, "a"+"b" 把 "b" 加在 "a" 的後面, 最後得出 "ab". 當你試著把 "5"+5 就會產生錯誤. 因為把整數加上字 串是無意義的, 所以 driver 會把第二個 5 轉換成 "5" 再加起來. 最後的結 果是 "55". 如果你想看的結果是 10 , 你最後只得到錯誤的程式碼. 請記住, 大多數的情況下, driver 不會像前面這樣產生 "55" 這種有用的結果. 它會產 生 "55" 是因為它早有一條規則處理整數加上字串的情況, 也就是把整數當成字 串看待. 在大多數的狀況中, 如果你在運算式或函式中使用資料型態並沒有事先 定義 (像是你試著把 "this is" 除以 "nonsense", "this is" / "nonsense") , driver 會嘔吐並回報錯誤給你. 翻譯: Spock of Final Frontier 98.Jan.22. 第四章: 函式 (functions) 4.1 回顧 現在, 你應該了解 LPC 物件由許多處理變數的函式所組成. 函式執行時就處理 變數, 而經由「呼叫」執行這些函式. 在一個檔案裡, 函式之間的前後順序是無 關緊要的. 變數在函式裡面被處理, 變數儲存在電腦的記憶體中, 而電腦把它們 當作 0 與 1 來處理. 利用定義資料型態這種方法, 這些 0 與 1 被轉換成 可使用的輸出及輸入結果. 字串 (string) 資料型態告訴 driver , 讓你看到或 你輸入的資料應該是許多字元及數字的形式. 整數 (int) 型態的變數對你來說 就是整數值. 狀況 (status) 型態對你來說就是 1 或 0. 無 (void) 資料型態 對你或對機器而言都沒有值, 並不是用於變數上的資料型態. 4.2 什麼是函式 ? 就像數學函式, LPC 函式獲得輸入值, 然後傳回輸出值. 像 Pascal 語言把程序 (procedure) 和函式 (function) 區分開來. 但是 LPC 不這樣做, 而知道這種 區分也是有用的. Pascal 稱為程序的東西, 在 LPC 就是無傳回值 (void) 型 態的函式. 也就是說, 程序或無傳回值函式沒有傳回輸出值. Pascal 稱為函式 的東西, 就是有傳回輸出值的. 在 LPC 裡, 最短的正確函式是: ----- void do_nothing() { } ----- 這個函式不接受輸入, 沒有任何指令, 也不傳回任何值. 要寫出正確的 LPC 函式有三個部分: 1) 宣告 (declaration) 2) 定義 (definition) 3) 呼叫 (call) 就像變數一樣, 函式也要宣告. 這樣一來, 讓 driver 知道: 1) 函式輸出的資 料是什麼型態 2) 有多少個輸入的資料以及它們的型態為何. 比較普通的講法稱 這些輸入為參數 (parameter). 所以, 宣告一個函式的格式如下: 傳回值型態 函式名稱 (參數 1, 參數 2, ..., 參數 N); 底下宣告一個 drink_water() 的函式, 它接受一個字串輸入, 而輸出一個整數: ----- int drink_water(string str); ----- str 是輸入的變數名稱, 會用於函式之中. 函式定義是描述函式實際上如何處理輸入值的程式碼. 呼叫則是其他函式之中, 呼叫並執行此函式的地方. 對 write_vals() 和 add() 兩個函式來說, 你可能會有這些程式碼: ----- /* 首先, 是函式宣告. 它們通常出現在物件碼的開頭. */ void write_vals(); int add(int x, int y); /* 接著是定義 write_vals() 函式. 我們假設這函式將會在物件以外被呼叫. */ void write_vals() { int x; /* 現在我們指定 x 為呼叫 add() 的輸出值. */ x = add(2, 2); write(x+"\n"); } /* 最後, 定義 add() */ int add(int x, int y) { return (x + y); } ----- 請記得, 哪一個函式定義在前都沒有關係. 這是因為函式並不是由前往後連續執 行的. 函式只有被呼叫時才會執行. 唯一的要求是, 一個函式的宣告必須出現在 函式的定義之前, 而且也必須在任何函式定義呼叫它之前. 4.3 外部函式 (efuns) 也許你已經聽過有人提過外部函式. 它們是外部定義的函式. 跟名稱一樣, 它們 由 mud driver 所定義. 如果你已經撰寫 LPC 程式碼很久, 你大概已經發現你 聽到的一些式子, 像是 this_player(), write(), say(), this_object()... 等等, 看起來很像函式. 這是因為它們是外部函式. 外部函式的價值在於它們比 LPC 函式要快得多, 因為它們早已經以電腦了解的二進位格式存在著. 在前面的 write_vals() 函式裡, 呼叫了兩個函式. 第一個是 add() 函式, 是 你宣告及定義的函式. 第二個, 則是稱做 write() 的外部函式. driver 早就 幫你宣告並定義這個函式. 你只需要呼叫它. 創造外部函式是為了處理普通的、每天都用得到的函式呼叫、處理 internet socket 的輸出與輸入、其他用 LPC 難以處理的事. 它們是在 game driver 內以 C 寫成的, 並與 driver 一起編譯在 mud 開始之前, 讓它們執行起來快 得多. 但是對你來說, 外部函式呼叫就像對你的函式呼叫一樣. 不過, 任何外部 函式還是要知道兩件重要的事: 1) 它的傳回值是什麼, 2) 它要什麼參數. 外部函式的詳細資料, 像是輸入參數和傳回值, 常常可以在你的 mud 中的 /doc/efun 目錄找到. 我沒有辦法在這裡詳細介紹外部函式, 因為每種 driver 的外部函式都不相同. 但是, 你常常可以藉由「man」 或「help」指令 (視 mudlib 而定) 找到詳細的資料. 例如指令「man write」 會給你 write 外部 函式的詳細資料. 如果都不行, 「more /doc/efun/write」也可以. 看過 write 的詳細資料之後, 你應該找到 write 是宣告成這樣: ----- void write(string); ----- 這樣告訴你, 要正確呼叫 write 不應該期待它有傳回值, 而且要傳入一個字串 型態的參數. 4.4 定義你自己的函式 雖然在檔案中, 你的函式次序誰先誰後都沒有關係, 但是定義一個函式的程式碼 的先後順序就非常重要. 當一個函式被呼叫時, 函式定義中的程式碼按照出現的 先後順序執行. 先前的 write_vals() 中, 這個指令: ----- x = add(2, 2); ----- 如果你想看到 write() 使用正確的 x 值, 就必須把它放在 write() 呼叫之前. 當函式要傳回一個值時, 由「return」指令之後跟著與函式相同資料型態的值所 完成. 在先前的 add() 之中, 指令「return (x+y);」 把 (x+y) 的值傳回給 write_vals() 並指定給 x. 在更普通的層次上來說, 「return」停止執行函式 , 並傳回程式碼執行的結果給呼叫此函式的函式. 另外, 它將跟在它後面任何式 子的值傳回呼叫的函式. 要停止執行失去控制的無傳回值函式, 使用 return; 而後面不用加上任何東西. 請再次記得, 使用「return」傳回任何式子的資料型 態「必須」與函式本身的資料型態相符合. 4.5 本章總結 定義 LPC 物件的檔案是由函式所組成的. 函式依次由三個部分組成: 1) 宣告 2) 定義 3) 呼叫 函式宣告通常出現在檔案的最前面, 在任何定義之前. 不過函式只要求在函式定 義之前以及任何函式呼叫它之前宣告它. 函式定義可以任何順序出現在檔案裡, 只要它們都放在宣告之後. 另外, 你不可 以再一個函式裡面定義另一個函式. 函式呼叫則出現在其他任何函式中, 任何程式碼想執行你的函式的地方. 呼叫也 可以出現在自己的函式定義中, 但是這種做法並不建議給新手去做, 因為它很容 易變成無窮迴圈. 函式定義依序由底下的部分所組成: 1) 函式傳回值型態 2) 函式名稱 3) 一個左小括號 ( 接著列出參數再加上一個右小括號 ) 4) 一個左大括號 { 指示 driver 從這裡開始執行 5) 宣告只用在這個函式中的任何變數 6) 指令、式子、視需要呼叫其他函式 7) 一個右大括號 } 描述函式碼在此結束. 對於無傳回值函式來說, 如果 在此還沒有碰到「return」指令 (只適用於無傳回值函式) , 會如同有 碰到「return」指令一樣回到原來呼叫的函式執行. 最短的函式是: ----- void do_nothing() {} ----- 因為這個函式不接受任何輸入, 不做任何事, 也不傳回任何輸出. 任何無傳回值型態以外的函式「必須」傳回一個與函式資料型態相同的值. 每一種 driver 都有一套早已經幫你定義好的函式, 它們叫做外部函式. 你不需 要宣告或定義它們, 因為它們早已經幫你做好這些事. 更深入一點, 執行這些函 式比起執行你的函式要快得多, 因為外部函式是 driver 的一部份. 再者, 每一 個 mudlib 都有特殊函式像是外部函式一樣, 早已經為你宣告並定義好. 但是不 同的是, 它們用 LPC 定義在 mudlib 裡面. 它們叫做模擬外部函式 (simul_efuns, 或 simulated efuns). 在大多數的 mud 裡, 你可以在 /doc/efun 目錄底下找到關於它們的詳細資料. 另外, 很多 mud 有稱作 「man 」或「help」的命令, 讓你可以方便地叫出這些資料檔案. 程式風格的註解: 有些 driver 可能不會要求你宣告函式, 有些不會要求你指定函式的傳回值型態. 無論如何, 底下有兩個理由勸你不要省略以上這些動作: 1) 對其他人來說 (還有你自己過了一段時間之後) , 會比較容易讀懂你的 程式碼並了解程式碼的意義. 這對除錯時特別有用, 有很多錯誤 (除了 放錯地方的各種括號) 發生在資料型態上 (有沒有碰過「Bad arg 1 to foo() line 32」? (程式第三十二行, 呼叫 foo() 時的第二個參數有錯) ). 2) 大家認為這樣子寫程式是個好習慣. 翻譯: Spock of Final Frontier 98.Jan.25. 第五章: 基礎的繼承 (inheritance) 5.1 回顧 你現在應該了解函式基本的功能. 你應該可以宣告並呼叫一個函式. 另外, 你應 該能認識函式定義, 雖然你可能是第一次接觸 LPC. 你現在並不見得能定義你自 己的函式. 函式是 LPC 物件的基石. 函式中的程式碼, 要別的函式呼叫它們的 時候才會執行. 呼叫一個函式時, 作出呼叫的函式要給它輸入值, 才能執行被呼 叫的函式. 被呼叫的函式執行其程式碼, 並傳回某種資料型態的傳回值給呼叫它 的函式. 沒有傳回值的函式屬於無傳回值 (void) 型態. 仔細看過你自己的工作室程式碼之後, 它看起來大概像這樣 (視 mudlib 而定): ----- inherit "/std/room"; void create() { ::create(); set_property("light", 2); set_property("indoors", 1); set("short", "Descartes 的工作室"); set("long", "此處是 Descartes 工作的地方.\n這裡是一個立方體.\n"); set_exits( ({ "/d/standard/square" }), ({ "square" }) ); } ----- 如果你到目前為止, 所有的課本內容都了解的話, 你應該能認出以下的程式碼: 1) create() 是函式的定義. (嘿 ! 他沒有宣告它) 2) 它呼叫 set_property() 、set()、set_exits(), 沒有一個函式在這段 程式碼中曾有宣告或定義. 3) 最上面有一行, 不是宣告變數或函式, 也不是函式定義 ! 這一章會找出這些問題的解答, 你現在應該腦中應該有這些問題: 1) 為什麼沒有宣告 create() ? 2) 為什麼 set_property() 、set() 、set_exits() 已經宣告並定義過了 ? 3) 檔案最上面那一行到底是啥東西 ? 5.2 物件導向程式設計 (object oriented programming, OOP) 繼承 (inheritance) 是定義真正物件導向程式設計的特性之一. 它讓你創造通 用的程式碼, 能以多種用途用於許多不同的程式中. 一個 mudlib 所作的, 就是 創造這些通用的檔案 (物件) , 讓你用來製造特定物件. 如果你必須把定義前面工作室全部所需要的程式碼寫出來, 你大概必須要寫 1000 行程式碼才能得到一個房間所有的功能. 當然, 那根本是浪費磁碟空間. 再者, 這種程式碼與玩家和其他房間的互動性很差, 因為每一個創造者都寫出自己的函 式以作出一個房間的功能. 所以, 你可能使用 query_long() 寫出房間的長敘述 , 其他的巫師可能使用 long() . 這就是 mudlib 彼此不相容最主要的原因, 因 為它們使用不同的物件互動協定. OOP 克服了這些問題. 前面的工作室中, 你繼承已經定義在 "/std/room.c" 檔案 中的函式. 它擁有普通房間所需要的全部函式定義其中. 當你要製造一個特定的 房間, 你拿這個房間檔案中定義好的通用函式功能, 並加上你自己的函式 create() 以製造一個獨特的房間. 5.3 繼承如何作用 你現在大概猜得出來, 這一行: ----- inherit "/std/room"; ----- 讓你繼承 "std/room.c" 的函式功能. 藉由繼承函式功能, 它代表你可以使用 "/std/room.c" 裡面已經宣告並定義好的函式. 在 Nightmare Mudlib 中, "/std/room.c" 裡面有許多函式, 其中有 set_property() 、set() 、 set_exits() 函式, 都已經宣告並定義過. 在你的 creat() 函式裡, 你呼叫那 些函式來設定你房間一開始的值. 這些值讓你的房間不同於別的房間, 卻保留與 記憶體中其他房間互動的能力. 實際的寫作中, 每一個 mudlib 都不同, 所以要你使用不同一套的標準函式來達 到相同的功能. 說明有哪些函式存在和它們是作什麼用的, 已經超出了這本課本 的範圍. 如果你的 mudlib 有自己詳細的說明資料, 你會找到教你如何使用各種 繼承檔案的說明文件以創造物件. 這些說明應該會告訴你有哪些函式、它們需要 哪些輸入、它們輸出的資料型態、以及它們的功能. 5.4 本章總結 本章距離完整解釋繼承如此複雜的主題還有一大段距離. 本文的目的只是讓你能 了解如何使用繼承來創造你的物件. 以後的課本將對此會有完整的討論. 現在你 應該已經了解底下幾點: 1) 每一個 mudlib 都有一套通用物件庫, 有它們自己的通用函式. 創造者 透過繼承使用它們, 讓撰寫物件程式碼這件工作更輕鬆, 並與其他物件之間能良 好互動. 2) 可被繼承的檔案裡頭的函式, 每個 mudlib 都不一樣. 你的 mud 裡應 該有說明文件解釋如何使用這些可被繼承的檔案. 如果你還不知道有哪 些函式可用, 那你就沒有辦法用它們. 任何時候, 都請你特別注意輸入 和輸出的資料型態. 3) 你藉由底下這行繼承函式的功能: ----- inherit "filename"; ----- filename 是被繼承的物件檔案名稱. 這行放在你程式碼的開頭. 註解: 你可能看到有幾處地方有 ::create() 或 ::init() 或 ::reset() 語法. 你現 在不需要完全了解這個, 但是應該告訴你一點線索, 知道它到底是什麼. 「::」 運算子是一種特殊的方法來呼叫繼承物件的函式 (叫做範圍解析運算子 scope resolution operator). 例如, 大多數 mud 的 room.c 都有叫做 create() 的 函式. 當你繼承 room.c 並設定 create() 時, 你所作的事稱為超越 (override) room.c 的 create() 函式. 這表示不管任何東西呼叫你房間的 create() , 它 會呼叫「你的」版本, 而不是 room.c 裡面的那一個. :: 運算子讓你能呼叫 room.c 裡的 create() 而不是你的 create(). 一個例子: ----- #1 inherit "/std/room"; void create() { create(); } ----- ----- #2 inherit "/std/room"; void create() { ::create(); } ----- 第一個例子是個恐怖的例子. 當它被載入時, driver 呼叫 create() , 之後 create() 再呼叫 create(), create() 又呼叫 create(), 這時 create() 又 呼叫 create()......換句話說, 所有的 create() 就一直呼叫自己直到 driver 偵測到太深的遞迴 (recursion) 並跳出來. 第二個例子基本上只是浪費記憶體, 它的功能跟 room.c 沒有兩樣. 對它而言, driver 先呼叫它的 room.c , 然後呼叫 ::create() , 也就是 room.c 裡的 create() . 其他的地方就跟 room.c 的功能一樣. 譯者: Spock of Final Frontier 98.Jan.25. 第六章: 變數 (variable) 處理 6.1 回顧 現在你應該能利用你 mud 的標準物件庫, 撰寫一些簡單的物件. 繼承能讓你使 用那些物件中已經定義好的函式, 而不用自己去定義. 另外, 你應該知道如何宣 告你自己的函式. 這一章將教你 LPC 的基本元素, 讓你能藉由處理變數來定義 你自己的函式. 6.2 數值與物件 基本上, mud 裡頭的物件都不一樣的原因有兩個: 1) 有的物件擁有不同的函式 2) 所有的物件都有不同的數值 現在, 所有的玩家物件都有同樣的函式. 它們不一樣的地方在於它們自己所擁有 的數值不同. 舉例來說, 名字叫做「Forlock」的玩家跟「Descartes」「至少」 他們各自的 true_name 變數值不同, 一個是 "descartes", 另一個是 "forlock". 所以, 遊戲中的改變伴隨著遊戲中物件值的改變. 函式名稱就是用來處理變數的 過程名稱. 例如說, 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/monster"))-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; i<x; i++) write((string)tmp[i]->query_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 敘述會判斷 () 之中的運算式是真還是偽, 所以如果你把 = 錯當成 == , 你就會得到永遠為真的運算式. 你會扯掉許多根頭髮, 也搞不清楚到底是為什麼 出錯 :) 譯者: Spock of Final Frontier 98.Jan.26. 第七章: 流程控制 (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); x<y; x++) { write(x+"\n"); } 在此, 我們初始化 x 和 y 兩個變數, 我們把它們用逗號分開來. 你可以在 for() 三個部分的運算式中如此使用. 7.6 敘述: switch() 原型: switch(運算式) { case 常數: 一些指令 case 常數: 一些指令 ...... case 常數: 一些指令 default: 一些指令 } 這樣有點像 if() 運算式, 而且對 CPU 也好得多, 但是 switch() 很少有人使 用它, 因為它看起來實在很複雜. 但是它並非如此. 第一點, 運算式不是測試條件. case 才是測試. 用普通的話來讀: 1 int x; 2 3 x = random(5); 4 switch(x) { 5 case 1: write("X is 1.\n"); 6 case 2: x++; 7 default: x--; 8 } 9 write(x+"\n"); 就是: 設定變數 x 為一個 0 到 4 的隨機數字. x = 1 的 case 中, 顯示 x 的值, 將 x 加上 1 之後再將 x 減 1. x = 2 的 case 中, 將 x 加上 1 之後再減 1. 其他情形下, x 減 1. 顯示 x 的值. switch(x) 基本上告訴 driver, 變數 x 的值是我們想配合各個 case 的情形. 當 driver 找到一個能配合的 case 時, 這個 case 「以及所有在它之後」的 case 都會執行. 你可以使用 break 指令, 在執行一個 case 之後跳出 switch 敘述, 就像其他流程控制敘述一樣. 稍後會解釋這一點. 只要 switch() 流程還沒中斷, 任何 x 值都會執行 default 敘述. 你可以在 switch 敘述中 使用任何資料型態: string name; name = (string)this_player()->query_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 有關建造房間的文件) 、簡 單的怪物、其他簡單的物件. 譯者: Spock of Final Frontier 98.Feb.1. -- Boldly go where no mudder has gone before... Spock (roach admin) 蟑螂管理員 homepage: http://bbs.csmc.edu.tw/spock/ From The Final Frontier 140.128.136.12 4000 ※ 來源:‧中山醫學院BBS -- 絮情小站 bbs.csmc.edu.tw‧[FROM: localhost] -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 218.170.228.91 ※ 編輯: laechan 來自: 218.170.228.91 (03/06 11:16)
文章代碼(AID): #19i9M6gL (mud_sanc)
文章代碼(AID): #19i9M6gL (mud_sanc)