中阶 LPC
Descartes of Borg
Novermber 1993

			第二章: 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 of the Final Frontier	98.Jul.22.
