第二章: LPMud driver

MudOS v21c2

中阶 LPC

 Descartes of Borg
 November 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.

    回到上一页