mud_sanc 板


LINE

作者 [email protected] (打混的蟑螂史巴克), 看板 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. 不然, 寄给 [email protected] 即可. (译按: 台湾的 intermud 系统通常寄不到 美国的 mud. 而 [email protected] 是 1993 的地址, 已无法使用, 请改用 [email protected] ) 一些手册中用的基本名词: 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 [email protected] (译按: 已改为 [email protected]) 译者: 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)







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:BuyTogether站内搜寻

TOP