作者laechan (小太保)
看板mud_sanc
标题[转载] [蟑螂贺失恋] 中阶LPC第1~7章
时间Fri Mar 6 15:30:44 2009
作者
[email protected] (打混的蟑螂史巴克), 看板 Mud
标题 [蟑螂贺失恋 ?] 中阶 LPC - 第一章
时间 中山医学院BBS站 (Wed Jul 22 21:43:06 1998)
───────────────────────────────────────
中阶 LPC
Descartes of Borg
Novermber 1993
第一章: 简介
1.1 基础 LPC
阅读此课本的人应该读过基础 LPC 课本或是够熟悉 mud 世界的程式写作.
不只是能建造房间和撰写区域内有关的物件而已, 也该清楚自己写出来的程式在
执行的时候到底在做什麽. 如果你觉得你的程度还不到, 就回去看完基础 LPC
再来看中阶 LPC 课本. 如果你达到要求, 你会发现你在此读到的东西对你比较
有意义.
1.2 中阶 LPC 课本的目标
此份介绍性的课本是为了对 LPC 一无所知的人, 让他们有能力在任何 LPMud
写出好的游戏世界. 对 LPC 和建设 LPMud 来说, 自然要比建造房间、护甲、
怪物、武器来得艰深. 当你进入更复杂的概念, 例如公会; 或想更深入你的游戏
世界, 你会发现基础 LPC 详细说明的概念里面没有这些东西. 中阶 LPC 的设
计是把你从简单的世界建造过程, 带到完全了解 LPC 作为 LPMud 世界建造者
的知识. 撰写 mudlib 本身的工作留到後面再讲. 读完这份课本, 并实际撰写一
些实验性的程式码之後, 你们读者应该能写出合乎任何设计或想法的游戏物件,
只要这样我的目的就达到了.
1.3 概观
到底多了什麽东西 ? 呃, 你们大部份都知道 LPC 支援映射 (mapping) 和阵
列 (array), 也曾问过我基础 LPC 为什麽没有详细说明它们. 我觉得那些概念
超出我在基础 LPC 课本里面尝试讲述的范围, 而比较适合放在这份课本里. 不
过, 新工具都棒极了, 而重要的是, 你可以用新工具做啥. 基础 LPC 课本的目
标是让你能建造够格的 LPMud 区域, 不需要映射 (mapping) 和阵列 (array)
就可以办到. 这份课本的目标是让你能在你的区域中实现任何点子, 这样就需要
映射和阵列的知识.
LPMud 中, 任何你想写的点子都可能实现. 令人惊讶的是, LPC 语言非常适合实
现这些目标. 你无法写出你的点子的原因是: 你对 LPC 的知识、不合的 mudlib
、你的 Mud 主题、mud 管理的政策. 这份课本没办法把你工作的 mudlib 变得
更好, 也无法改变你的 Mud 主题或 mud 的管理政策. 千万别认为 LPC 没办
法做到你想做的事. 如果你的点子不容於 mud 主题或是管理政策, 那就是你的
点子无法在你现在的 mud 实现. 如果 mudlib 不合, 告诉负责 mudlib 的人,
在 mudlib 层面上可以做哪些事来增进它. 你会很惊讶, mudlib 中实际上有很
多你不知道的东西. 更重要的是, 读过此课本後, 你应该能读懂你 mud 的
mudlib 程式码, 了解每一行程式在做啥. 你目前应该还没有能力自己写出这种
程式码, 但是至少你可以了解 mudlib 层次在做什麽.
这份课本开始时, 讨论 LPMud driver 到底在做些什麽. 这份课本的一个好处是
, 里面所讲的东西跟 driver 和 mudlib 无关 (除了 Dworkin Game Driver).
对於游戏 driver 的章节不会深入实际的 driver 程式架构, 而是所有游戏
driver 跑 mud 基本上所要做的事.
接着, 我讨论每个人都想多了解一些的题目 ---- 阵列 (array) 和映射
(mapping). 要了解映射这种资料型态, 大概是最简单也是最困难的事. 大致上
, 映射算是一种复杂的阵列, 要讨论映射之前, 你应该先了解阵列. 一旦你了解
它们, 映射实际上要比阵列好用得多. 无论快慢, 用你最多的的时间来学习这一
章, 因为本章大概是这份课本里头最困难, 也是最有用的一章.
之後, 有一小章讨论 LPC pre-compiler, 在你的程式码送给 compiler 之前,
你可以用它来编排你的程式码. 虽然我在这里介绍得很可怕, 这章恐怕是这份课
本中最简单的一章. 所以我把它摆在介绍映射和阵列的那一章之後.
接着重新介绍字串 (string) , 更详细地教你如何处理字串, 像是分断字串这种
高级的命令处理技巧. 只要你很了解阵列, 这章应该很简单.
再下一章是这本课本第二重要的部分. 如果你想超越中阶的水准、深入 mudlib
程式码, 这可能是最重要的部分. 这章深入介绍 LPC 继承 (inheritance) 的复
杂观念. 既然这份课本的目标并不是教你设计 mudlib 的程式, 这章不会详细讨
论物件导向程式设计. 了解本章, 会让你体会一些物件导向程式设计的内涵, 也
让你能够僭越 (overriding) 其他函式以写出更复杂的物件, 并定义出你自己的
角色基础职业.
最後, 课本以简略地讨论程式码除错作结. 这不是很重要的一章, 但是这样也表
示此章不只是补充你目前所学的知识而已.
1.4 此课本没有的东西
对某些人来说, 此份课本最大的、也是政策性的遗漏就是「投影」(shadow). 我
从来没有看过使用投影是最好或最有效率的例子. 不过, 这样也不代表投影一无
是处. 我在这份课本里不介绍投影的理由是, 学习 LPC 的人, 最好在碰上投影
以前, 先从此课本学得一些观念, 并花上时间去熟悉这些观念. 这样一来, 我觉
得学习 LPC 的人会有能力决定是否要使用投影. 我会在以後的课本里讨论投影.
如果你经常使用很多的投影, 请别认为上面这段文字是针对你的批评. 我也曾经
看过投影有很多优秀的用途. 不过, 投影并不是一个完成工作的好方法, 所以投
影并不适合这份中阶课本的目标.
我也删掉了讨论系统安全和物件导向程式设计的部份. 这两者很明显都是讨论
mudlib 方面的题目. 不过很多人大概会反对我不讨论物件导向程式设计的作法.
我决定把这个课题留到以後再说, 因为大多数区域设计者是为了创作而撰写程式
码, 而不是为了资讯理论. 在中阶和基础的课本里, 我决定只在实际 LPC 程式
设计上可以直接应用的地方讨论物件导向程式设计的理论. 对於想撰写一个庞大
mudlib 的 LPC 老手而言, 理论可能要实用得多. 不过以这份课本的目标来说
, 讨论物件导向程式设计只是个让人打瞌睡的题目. 我计划在下一份课本里多讨
论这个理论.
1.5 总结
LPC 不难学习. 虽然比不上其他大部分电脑语言所常做的工作, 令人惊异的是它
非常强大, 在建造 MUD 这种游戏的工作上, 也没其他语言比得上它. 对初学者
来说, 它让你易於学习, 甚至在你还不知道在做什麽的时候, 就能写出有用的物
件. 对中阶的人来说, 它让你的任何点子变成文字化的虚拟实境. 对高阶的人来
说, 它的物件导向特点, 可以让你建造一个 internet 上最受喜爱的游戏. 你唯
一所受到的限制, 是你了解多少东西. 而进一步学习并不需要资讯学位.
Copyright (c) George Reese 1993
译者: Spock @ FF 98.Jul.19.
第二章: LPMud driver
2.1 回顾基本的 driver/mudlib 间的互动
在基础 LPC 课本里, 你学到很多 mudlib 工作的方式, 尤其是关於你为了建造
区域所撰写的物件. 而 mudlib 和 driver 间的互动讨论得并不多. 不过, 你应
该知道 driver 做了以下的事:
1) 当一个物件第一次被载入记忆体, 原始模式 mud 的 driver 会呼叫
create(), 而精简模式 mud 会呼叫 reset(). 创作的人使ꔠcreate() 或
reset() 给予物件初始值.
2) 每到游戏管理者设定的周期, driver 呼叫 reset() 函式. 这样让物件能
重新产生怪物之类的东西. 请注意, 在精简模式的 mud 中, 同一个函式不
但用於重新设定房间, 也用於设定初始值.
3) 任何时候, 一个活物件 (living object) 遇到另一个物件时, driver 呼
叫新遇到物件的 init() 函式. 这样可以让新遇到的物件透过 add_action()
外部函式 (efun) 给予活物件可以执行的命令, 同样也可以执行其他的动作,
而这些动作是一个活物件碰到此一物件时所该发生的事.
4) driver 定义了一套称为外部函式的函式, 在游戏中所有的物件都可以使用
它们. 举例来说, 常用的外部函式有: this_player(), this_object(),
write(), say(), 以此类推.
2.2 driver 周期 (cycle)
driver 是执行游戏的 C 程式. 它的基本功能是接受外界的连线, 让人能登录
(login) 、解译定义 LPC 物件和它们在游戏中作用的 LPC 程式码、接受使用
者的输入并呼叫适当的 LPC 函式以配合事件发生. 它最简单的要素就是, 它是
一个永不终止回圈 (loop).
一旦游戏启动, 并且正确地执行功能 (以後会在高阶 LPC 课本中讨论启动程序)
, driver 就进入一个回圈. 除非合法呼叫 shutdown() 外部函式, 或碰上臭虫
让 driver 崩坏 (crash), 此回圈不会终止. 一开始, driver 控制任何新进的
连线, 并把连线交给登录物件 (login). 之後, driver 把所有使用者输入的命
令放入一个命令表 (table of commands), 此时已是 driver 的最後一个周期.
在组合命令表之後, 所有从 driver 最後一个周期排定要送给连线的讯息, 就送
给使用者. 此时, driver 依序执行命令表中的命令, 并执行每个物件放在命令
表中的各套命令. driver 在周期结束时, 呼叫每一个有 heart_beat() 函式的
物件, 执行其中的 heart_beat() 函式. 最後, 执行所有等待的延迟呼叫
(call out). 本章不讨论连线控制, 本章焦点放在 driver 如何控制使用者命令
、心跳 (heartbeat)、延迟呼叫.먊
2.3 使用者命令
如同 1.2 中所提, driver 在每个周期中, 把每一个使用者要执行的命令储存
在命令表里. 命令表里头有执行此命令的活物件名称、给予活物件此一命令的物
件、要执行此命令时所执行的函式. driver 把输入命令的物件当作是给予命令
者. 大多数的时候, 这就是 this_player() 所传回的给予命令者.
driver 由有延迟命令的活物件表的头端开始, 接着执行命令, 呼叫这些活物件
输入的命令相关的函式, 并传入给予命令者给函式的任何参数. 当 driver 由新
的活物件所给的命令开始时, 给予命令者变数就改为新的活物件, 这样在命令开
始依序执行函式时, this_player() 外部函式才能传回给予命令的物件.
来看看一个玩家的命令暂存区范例. 在一个叫做 Bozo 的玩家执行最後一个命令
时, 他输入 "north" 和 "tell descartes 下次重新开机是什麽时候 ?".
"north" 命令与 Bozo 所在房间里的 "Do_Move()" 函式相关 ("north" 命令由
此房间的 set_exits() 外部函式自动设定). "tell" 命令并没有特别列在玩家
所可以使用的命令中, 而在玩家物件中有一个叫做 "cmd_hook()" 的函式, 比对
玩家可能输入的命令.
当 driver 处理到 Bozo, 给予命令者的变数就设定为 Bozo 这个物件. 然後,
看到 Bozo 输入 "north", 也看到与 "north" 相关的函式, 则 driver 呼叫
Bozo's_room->Do_Move(0) (Bozo 所在房间的 Do_Move() 函式). 因为 Bozo
只输入 "north" 命令, 没有加上参数, 所以用参数 0 传入此函式. 此房间
平常会呼叫一些它需要的函式, 此时 this_player() 外部函式所传回的物件
就是 Bozo. 最後, 此房间物件会呼叫 Bozo 中的 move_player(), 之後呼叫
move_object() 外部函式. 这个外部函式负责改变一个物件的环境.
当一个物件的环境改变时, 会删除前一个环境中其他物件和前一个环境中对它
加上的可用命令. 删除之後, driver 呼叫新环境和新环境中每一个物件的
init() 外部函式. 每一次呼叫 init() 时, Bozo 物件仍然是给予命令者.
所以此次移动所有的 add_action() 外部函式会加在 Bozo 身上. 完成所有的
呼叫後, 控制权从 move_object() 交给 Bozo 的 move_player() 区域函式
.. move_player() 将控制权交回给旧房间的 Do_Move(), Do_Move() 传回 1
给 driver, 以表示此命令的动作完成. 如果 Do_move() 因为某些原因传回
0, 则 driver 会对 Bozo 显示 "什麽?" (或是你的 driver 所预设的错误命
令讯息).
一旦第一个命令传回 1, driver 就继续处理 Bozo 的第二个命令, 过程就跟
第一个一样. 请注意, driver 把 "tell descartes 什麽时候重新开机 ?"
的 "descartes 什麽时候重新开机 ?" 当作参数传给跟 tell 相关的函式. 这
个函式决定要如何处理这个参数. 这个命令之後传回 1 或 0, driver 再继
续处理下一个有延迟命令的活物件, 然後以同样的步骤处理全部有延迟命令的
活物件, 执行它们的命令.
2.4 set_heart_beat() 和 call_out() 外部函式
一旦有延迟命令的物件其全部的命令执行完成後, driver 就继续呼叫所有
driver 列为有心跳之物件中的 heart_beat() 函式. 只要一个物件以非零参数
呼叫 set_heart_beat() 外部函式 (视你的 driver 而定, 非零的数字也许很重
要, 但是在大多的情况下为整数 1 ), set_heart_beat() 外部函式把呼叫
set_heart_beat() 的物件加在有心跳物件的列表上. 如果你以 0 为参数呼叫
它, 它就把此物件从有心跳物件的表上删除.
心跳在 mudlib 里最常见的用途是治疗玩家和怪物、执行战斗. 一旦 driver 处
理完命令列表, 它就开始看心跳列表, 呼叫表上每一个物件的 heart_beat().
所以举例来说, 对玩家而言, driver 会呼叫玩家里面的 heart_beat() 以执行
以下功能:
1) 让玩家变老
2) 依照治疗速率治疗玩家.
3) 检查四周是否有任何被人猎杀、正在猎杀人、或正在攻击人的物件
4) 如果第三点成立, 开始攻击.
5) 其他需要每秒钟自动发生的事.
请注意, 有心跳的物件越多, mud 每个周期需要处理的时间也就越久. 有心跳的
物件已知是 mud 贪求 CPU 时间最主要的因素.
call_out() 外部函式用於执行不需要像心跳一样常常发生、或只发生一次的计
时函式呼叫. 延迟呼叫 (call out) 让你指定呼叫一个物件中的某个函式. 一般
延迟呼叫的公式为:
call_out( func, time, args );
第三个指定参数的参数并非必要. 第一个参数是一个字串, 代表被呼叫的函式名
称. 第二个参数是经过几秒之後才呼叫函式.
实际上来说, 当一个物件呼叫 call_out() 时, 它就被加到一个延迟呼叫的物件
表中, 此表中记有延迟呼叫的总延迟时间, 和欲呼叫的函式名称. driver 的每
一个周期, 就会进行倒数, 直到呼叫函式的时间. 时间一到, driver 把此物件
从延迟呼叫表上删除, 并执行呼叫延迟呼叫函式, 传入原本延迟呼叫函式所指定
的参数.
如果你想在一个延迟呼叫执行前将其删除, 你需要用 remove_call_out() 外部
函式, 传入延迟呼叫的函式名称. driver 会删除下一次延迟呼叫的这个函式.
这表示如果同一个函式有一个以上的延迟呼叫, 就会出现模拟两可的情况.
要让一个延迟呼叫循环执行, 你必须在你延迟呼叫的函式中再使用 call_out()
外部函式, 因为 driver 执行完延迟呼叫後, 会自动把函式从延迟呼叫表中删除.
举例:
void foo() { call_out("hello", 10); }
void hello() { call_out("hello", 10); }
在 foo() 第一次被呼叫後, 每 10 秒呼叫 hello() 一次. 在此有几件事要注
意. 第一, 你必须要小心, 确定你的延迟呼叫不会造成任何不正确的递回方式.
第二, 比较 set_heart_beat() 和 call_out() 所做的事有何不同.
set_heart_beat():
a) 将 this_object() 加在心跳物件列表中
b) 每一次 driver 周期呼叫 this_object() 中的 heart_beat() 函式
call_out():
a) 将 this_object() 、this_object() 中的函式名称、延迟时间、一组参数,
加在延迟呼叫函式的列表上
b) 指定名称的函式只呼叫一次, 在延迟一段指定的时间後, 执行此次呼叫
你可以看到, 延迟呼叫的 (a) 部分有很庞大的记忆总量 (memory overhead),
而心跳的 (b) 部分则有更庞大的 CPU 总量, 假设延迟呼叫的延迟时间要比一
次 driver 周期来得长.
很明显, 你不会执行延迟一秒的延迟呼叫, 否则你会拖垮两者. 同样, 你也不希
望应该使用比一秒钟长的延迟呼叫周期来达成的功能出现在心跳中. 我个人听过
一种论点, 认为你应该多使用延迟呼叫. 我最常听到的是, 单一呼叫或比十秒长
的周期最好使用延迟呼叫. 十秒以内的周期性呼叫, 你最好使用心跳. 我并不知
道这种说法是否正确, 但是我也不认为遵照这种作法会造成任何损害.
2.5 总结
基於更深入了解 LPC, 和了解 driver 和 mudlib 间的互动. 你现在应该知道
driver 执行函式的顺序, 并了解有关 this_player()、add_action()、
move_object() 外部函式和 init() 区域函式更多的细节. 另外, 根据以往你
从基础 LPC 课本学得的知识, 本章以 driver 如何控制延迟呼叫和心跳来介
绍它们. 你现在应该对延迟呼叫和心跳有基本的认识, 并可以在你的程式码中
实验一下.
Copyright (c) George Reese 1993
译者: Spock @ FF 98.Jul.22.
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 218.170.228.153