定时触发器处理程序


实现动态MUD的一种方法是使用“定时触发器”,也被称为“心跳”。定时触发器是每隔指定时间就会触发一次的时钟。定时触发更新的机制在各种游戏系统中广泛使用。

在其它mud代码库中定时触发器很常见,甚至是不可或缺的。某些代码库强制要求必须依赖全局的“定时触发”机制。Evennia没有这样的规定,是否使用定时触发器完全看你游戏的需要。“定时触发器方案”只是系统运作方式中的一种。

想以最精细的方式来管理时间流动,当然要靠脚本。许多类型的操作(典型的例子是天气)需要按规则的时间间隔对多个对象做相同的事,但为此就将单独的脚本添加到各个对象上是效率低下的做法。要做到这一点,合适的方法是使用带有“注册模块”的定时触发器,如果物体想被定时触发就注册进来,不再需要定时更新时就注销掉。

Evennia为注册模块提供了优化的解决方案 —— 定时触发器处理程序(源代码)。 类型类对象将自己添加(“注册”)到该处理程序。然后,处理程序会使用你指定的参数定时调用注册对象的钩子方法(默认调用 at_tick 方法)。这会一直进行,直到对象从定时触发器注销掉。该处理程序会在服务器重新启动时保留,而且非常节省资源。

以下是使用它的例子:
    # 我们假设 obj 已经定义了钩子“at_tick”
    from ev import tickerhandler

    ticker_handler.add(obj, 20)
就是这样,从现在开始每20秒会调用一次 obj.at_tick()。
    ticker_handler.remove(obj, 20)
请注意,在注销时你也必须提供时间间隔以确定要删除的注册。这是因为在处理程序中有一个定时触发器池,只要指定的时间间隔不同,一个对象可以注册任意多次。

你还可以指定定时触发器处理程序调用其它的钩子:
    tickerhandler.add(obj, 10, hook_key="at_ten_seconds")
请记住,处理程序只按对象和间隔时间来识别注册的内容,因此一个对象在指定的间隔时间上只能注册一个钩子,不能有多个(如果你想要那么做,可以让一个钩子来处理所有事务)。

你在类型类中定义的钩子方法的形式应该是 hookname(*args, **kwargs),其中的参数和关键字是通过 tickerhhandler.add() 传送过来的。请注意,这些参数应该是文本格式的,这是为了能在系统重启后保留。所以你不能将数据库对象作为参数或关键字,这会导致无法预料的结果。

在测试时,你可以调用 tickerhandler.clear() 清除整个游戏中的所有定时触发器。你也可以用 tickerhandler.all() 查看当前注册的对象。

使用定时触发器处理程序的例子请参见天气教程

什么场合不应使用定时触发器处理程序
定时触发器处理程序听起来很有用,但知道什么时候不应该使用它也很重要的。即使在使用其它代码库时你已经习惯于让一切都依赖定时触发器,你也应该停下来想一想是否真的需要它。这是最重要的一点:

你不应该用定时触发器来捕捉变化。

想想看,为了及时捕捉变化你可能需要每秒钟触发一次,但很有可能在大部分的时间内什么都没有改变。所以你在做无谓的调用(因为调不调用结果都一样)。鉴于系统的复杂性,检查是否发生变化的运算开销可能会很大,更不用说这可能需要检测数据库中的每个对象。每一秒钟,只是为了维持现在的状态。

与其反复检查极小概率才会发生的变化,还不如考虑更积极的措施。你也许可以让很少发生变化的系统在有变化时自己报告出来?如果你能等到“有需求”了才做事情,应该会更节省、更高效。Evennia自身广泛使用钩子方法的原因就是这个。在默认设置中,唯一真正使用“定时触发器”的地方是保存运行时间(当然,每次调用时它都会变化)。

所以,如果你觉得一个定时触发器会频繁调用,但预计在99%的时间内不会产生效果,那你可以想想是否能用其它方式处理这些事务。即使是快速变化的属性,采用按需报告的策略通常仍然会开销更小。还要记住,有些东西不需要一直更新,当有人要实际检测或使用它们时才做更新,对在此之前发生的临时变化作计算是浪费时间。

需要定时触发器的主要原因是,你想在同一时刻对多个对象做某些事情,而且不想由其它对象触发操作。

扩展定时触发处理程序
这是进阶内容。

定时触发器处理程序可以在同一时刻高效地更新大量对象。而定时触发器处理程序所做的一切都可以用脚本来实现,而且更加灵活。

比如出于某种原因,你想要创建定时触发器处理程序的子类,以下是相关做法。

主要的 TickerHandler 类在 src/scripts/tickerhandler.py 中。它实际上没有使用脚本,而是直接使用了 Twisted 的原语。它由三部分组成。上层是定时触发器处理程序类自身,它提供了面向用户的API。它使用了定时触发器池,池会随定时触发器动态增大或缩小。当用户添加新注册和更改间隔时间时,池会确保将注册的对象添加到定时触发器中,或按指定间隔创建新的定时触发器。当一个定时触发器不再有人注册,它会被自动删除掉。

如果想让定时触发器处理程序实现一些其他功能(比如不是调用数据库对象的钩子方法,而是其它对象的方法),三个组件中的一个或全部都可以被重载,以改用自定义版本的组件。改编的例子请参见 OOBTicker

在默认情况下,你自定义的定时触发器处理程序不会在重启服务器后保留。要让它做到这点,你必须按以下方式做:
  • 给处理程序的数据设置唯一的保存名称。这是实例化处理程序对象的唯一参数:myhandler = MyTickerHandler("mytickerhandler_storage") 。设置之后,处理程序就会在每次更新时保存其数据(不需要专门调用 myhandler.save())。
  • 确保在服务器启动时调用 myhandler.restore()。为此,你需要设置插件模块 at_server_startstop.py 让它执行调用。


(原文:https://github.com/Evennia/evennia/wiki/TickerHandler    翻译:卢铱俊)