命令的持续时间


注意:本文属高级教程,在充分理解命令如何工作之前,不要轻易尝试。

某些类型的游戏中,一个命令的执行和结束不应当是瞬时的。装填十字弓也许得花上点儿时间——而当敌人正急速冲向你时根本没有这点儿时间。同样地,制作护甲也不能瞬间完成。对某些类型的游戏而言,真实地移动或改变姿势都要求附带特定的时间开销。

下面是为某个命令的执行过程添加持续时间的简单例子。
    from evennia import default_cmds, utils

    class CmdEcho(default_cmds.MuxCommand):
        """
        wait for an echo

        Usage: 
          echo <string>

        Calls and waits for an echo
        """
        key = "echo"
        locks = "cmd:all()"

        def func(self):
            """
             This is called at the initial shout
             If we return a deferred from this the execution of the command
             handler process will not happen until the deferred has fired.
            """
            self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
            # this waits non-blocking for 10 seconds
            utils.delay(10, callback=self.echo) # call echo after 10 seconds

        def echo(self):
            "Called after 10 seconds"
            shout = self.args
            string = "You hear an echo: %s ... %s ... %s"
            string = string % (shout.upper(), shout.capitalize(), shout.lower())
            self.caller.msg(string)
将这个新命令(echo)导入到默认命令集并重载服务器。你将发现要等待10秒钟才能听到自己的回声。你还将发现这是一个非阻塞的效果;在等待期内还可以执行其他命令,而游戏照常继续。回声将按它自己的计时传回给你。

关于utils.delay()

utils.delay(delay, callback=None, retval=None) 是个有用的函数。它会等待 delay 秒,然后调用你给定的函数 callback() 或者 callback(retval)。

(带 retval 参数的 callback() 被限制使用;你还需要把延迟添加到Twisted inlineCallbacks yield call。如果你想那么做,请参阅Twisted手册)。

你可能觉得 utils.delay(10) 在上面的代码中看起来只是像又一个 time.sleep(10) 的翻版。这绝不是实情。如果你执行 time.sleep(10),事实上你将冻结整个服务器时间10秒钟!utils.delay() 函数是Twisted Deferred的一个轻量级封装, 它会延迟10秒执行,但是以异步方式进行,不会影响到任何其他的东西(甚至不会影响调用者自身——你可以继续照常做事情直到延迟事件的发生)。

这里需要记住的是 delay() 不会“暂停”在它被调用的那个点上。事实上调用 delay() 之后的语句会立即执行。你必须做的是告诉 delay() 时间到后需要调用哪个函数(它的“回调函数”)。乍听起来有点古怪,不过这就是异步系统的工作方式。你也可以像下面这样把这些调用连在一起使用:
    from evennia import default_cmds, utils

    class CmdEcho(default_cmds.MuxCommand):
        """
        wait for an echo

        Usage: 
          echo <string>

        Calls and waits for an echo
        """
        key = "echo"
        locks = "cmd:all()"

        def func(self):
            "This sets off the chain of delayed calls"

            self.caller.msg("You shout '%s', waiting for an echo ..." % self.args)            

            # wait 2 seconds before first return
            utils.delay(2, callback=self.echo1)

        # callback chain, started above
        def echo1(self):
            "First echo"
            self.caller.msg("... %s" % self.args.upper())
            # wait 2 seconds for the next one
            utils.delay(2, callback=self.echo2)
        def echo2(self):
            "Second echo"
            self.caller.msg("... %s" % self.args.capitalize())
            # wait another 2 seconds
            utils.delay(2, callback=self.echo3)
        def echo3(self):
            "Last echo"
            self.caller.msg("... %s ..." % self.args.lower())
上面这个版本会使这些echo命令一个接一个出现,每个都间隔几秒钟。
> echo Hello!
... HELLO!
... Hello!
... hello! ...

阻塞命令

utils.delay() 函数的伟大之处在于它的延迟不会造成阻塞。它只是在后台运行而你可以在此期间自由行动。不过在某些场景下这可能不是你想要的。有些命令在运行时应当能简单地“阻塞”其他命令——例如你正在制作头盔,你不应该能同时制作盾牌,或者你刚刚拿着武器使出了“超级挥砍”,你不应该能立即再次使用该技能。

实现阻塞的最简单做法是使用类似于命令冷却时间中提到的那些技术,绝大部分在这里也适用。

这里以使用调用者的某个属性为例:
    from evennia import utils, default_cmds

    class CmdBigSwing(default_cmds.MuxCommand):
        """
        swing your weapon in a big way

        Usage:
          swing <target>

        Makes a mighty swing. Doing so will make you vulnerable
        to counter-attacks before you can recover. 
        """
        key = "bigswing"
        locks = "cmd:all()"

        def func(self):
            "Makes the swing" 

            if self.caller.ndb.off_balance:
                # we are still off-balance.
                self.caller.msg("You are off balance and cannot swing again yet!")
                return

            self.caller.msg("You swing big! You are off balance now.")   

            # set the off-balance flag
            self.caller.ndb.off_balance = True

            # wait 8 seconds before we can recover. During this time we won't be able
            # to swing again due to the check at the top.        
            utils.delay(8, callback=self.recover)

        def recover(self):
            "This will be called after 8 secs"
            del self.caller.ndb.off_balance
            self.caller.msg("You regain your balance.")
如果你希望一个给定的命令阻塞其它全部命令,你需要稍微修改命令类,使它支持寻找该命令所作的标记。最简单的是编写你自己的命令类,添加检查项到恰当的钩子,然后自此让其余所有命令都继承它。

可被中止的命令

可以想象你希望在一个耗时的命令执行完毕之前结束它。例如当你正在制作护甲而一个怪物突然闯进你的铁匠铺时,你肯定很希望停止“制作”命令。

你可以像上面实现“阻塞”命令那样做到这一点,只不过反过来。下面是一个例子,一旦触发战斗,“制作”命令立即被中止:
    from evennia import utils, default_cmds

    class CmdCraftArmour(default_cmds.MuxCommand):
        """
        Craft armour

        Usage:
           craft <name of armour>

        This will craft a suit of armour, assuming you
        have all the components and tools. Doing some
        other action (such as attacking someone) will 
        abort the crafting process. 
        """
        key = "craft"
        locks = "cmd:all()"

        def func(self):
            "starts crafting"

            if self.caller.ndb.is_already_crafting:
                self.caller.msg("You are already crafting!")
                return 

            # [Crafting code, checking of components, skills etc]

            # set up the steps
            if self.check_abort(): return True

            self.caller.ndb.is_already_crafting = True
            self.caller.msg("You start crafting ...")
            utils.delay(60, callback=self.step1)

        def check_abort(self):
            "checks abort condition and returns True if aborting."
            if self.caller.ndb.crafting_aborted:
                del self.caller.ndb.crafting_aborted
                if self.caller.ndb.is_already_crafting:
                    del self.caller.ndb.is_already_crafting 
                return True

        def step1(self):
            "first step of armour construction"
            if self.check_abort(): return True
            self.msg("You create the first part of the armour.")
            utils.delay(60, callback=self.step2)
        def step2(self):
            "second step of armour construction"
            if self.check_abort(): return True
            self.msg("You create the second part of the armour.")            
            utils.delay(60, callback=step3)
        def step3(self):
            "last step of armour construction"
            if self.check_abort(): return True          

            # [code for creating the armour object etc]

            del self.caller.ndb.is_already_crafting
            self.msg("You finalize your armour.")


    # example of a command that aborts crafting

    class CmdAttack(default_cmds.MuxCommand):
        """
        attack someone

        Usage:
            attack <target>

        Try to cause harm to someone. This will abort
        eventual crafting you may be currently doing. 
        """
        key = "attack"
        aliases = ["hit", "stab"]
        locks = "cmd:all()"

        def func(self):
            "Implements the command"

            if self.caller.ndb.is_already_crafting:
                self.caller.ndb.crafting_aborted = True
                del self.caller.ndb.is_already_crafting

            # [...]
上述代码创建了一个具有延迟效果的“制作”命令,允许逐渐制作出护甲。如果attack命令被执行,调用方将会设置一个标记,不论“制作”命令的持续时间何时将更新,它立即静默中止。

杂记

这些例子中我们只使用了 utils.delay(),这是Twisted中 reactor.callLater() 函数的一个非常简单的封装。如果你熟知Twisted,你可能想使用更多复杂特性,例如使用callback/errback链来更有效地处理多个命令的状态和条件。