脚本


角色外的脚本和角色内的物体如同一对兄弟。“脚本”这个名字可能会暗示它们只能用于游戏脚本,其实这只是它们用途的一部分(可我们只能从众多用途中选出一个来作为它的名字)。脚本是完整的类型类数据库实例,就和物体一样,继承了所有优点。同样的,它们也可以存储任意属性。

脚本可以用在Evennia的许多不同事物上:
  • 它们可以添加到物体上以不同的方式影响物体,也可以独立于游戏中的任何实体而存在。
  • 它们可以用作定时器、计时器等任何会随时间变化的东西,但也可以完全不依赖时间。
  • 它们可以描述状态的变化。
  • 它们可以用作数据仓库,在数据库中持久存储游戏数据。
  • 它们可以作为角色外的数据仓库在物体之间共享数据。
脚本最常见的用途是作为定时器或事件触发器。想像一下在老摆钟上运行着一个脚本,这个脚本有一个计时器,每隔一小时会触发一次,这让时钟可以定时报时。这个脚本甚至可以规定时钟必须多少时间上一次发条才不会停下来。

脚本可以用来控制状态的变化。比如创建一个“黑暗的”房间,给它加上两个脚本:黑暗状态脚本和明亮状态脚本。当一些角色进入黑暗的房间,它会给这些角色添加自定义的命令集。这个命令集会定义它们所描述的状态。在这个例子中由于黑暗,脚本会限制角色的动作。在他们跌跌撞撞了一阵之后,有人举起了火炬。由于现在房间里有了光源,黑暗状态脚本作出了回应,它关闭自己并将控制权转交给明亮状态脚本,明亮状态脚本把命令集恢复正常。最后,当带着火把的角色离开房间,明亮状态脚本检测到了它,于是又乖乖地把控制权送还给黑暗状态脚本,那些剩下的可怜虫们又陷入黑暗之中。

通过将状态变化和定时器结合在一起,你可以创建一个白天和晚上看起来不一样的房间,天气和季节也可以变化。你还可以实现更复杂的东西,比如带有AI的系统可以让小怪到处走动,可以跨越房间追赶其它角色(当需要控制大量物体时,出于效率考虑,你可能需要让脚本分时间段执行。具体实现方式请见分时段脚本的教程)。

脚本还是存储与角色无关的游戏数据的绝佳的地方,一组物体可以通过它们共同指向的同一脚本对象共享数据。

总而言之,脚本可以用来做很多事情。


如何创建和测试自己的脚本类型

在游戏中你可以使用 @script 命令来测试脚本。请输入以下命令:
> @script self = examples.bodyfunctions.BodyFunctions
这会隔一段随机时间显示一些随机信息。
> @script/stop self = examples.bodyfunctions.BodyFunctions
这会去除脚本。你可以用 @scripts 命令列出游戏中所有活动的脚本。其中有几个是Evennia默认创建的。

自定义的脚本模块通常存储在 game/gamesrc/scripts 中。你可以很方便地从 ev.Script 继承脚本。

如果你给物体添加了脚本,脚本就可以操纵物体了。脚本是添加到名为 scripts 的脚本处理程序上的。处理程序会为你处理所有与脚本初始化和启动相关的事宜。
    # 将脚本添加到 myobj 的脚本处理程序上
    myobj.scripts.add("game.gamesrc.scripts.myscripts.CoolScript")
    # 另一种方法
    from ev import create_script
    create_script("game.gamesrc.scripts.myscripts.CoolScript", obj=myobj)
脚本也可以不连接到游戏中的物体,这样的脚本称为全局脚本。想创建全局脚本只要在创建时不提供存储的物体就行了:
    # 添加全局脚本
    from ev import create_script
    create_script("game.gamesrc.scripts.globals.MyGlobalEconomy",
                   key="economy", obj=None)
假定脚本 game.gamesrc.scripts.globals.MyGlobalEconomy 已存在,这会把它作为全局脚本创建并启动。


定义在脚本上的属性和函数

脚本拥有类型类对象的所有属性,如 db 和 ndb(见类型类)。设置 key 对管理脚本很有用(比如可以通过名字删除它们等)。以下内容通常在脚本的类型类中设置,但也可以通过 ev.create_script 的关键字参数直接设置。
  • desc —— 可选的脚本说明信息,在脚本列表中可以看到它。
  • interval —— 脚本运行的频率。如果 interval == 0(默认值),它不是反复运行,而是会永远运行下去(它不接受负值)。
  • start_delay —— (布尔值),是否要在第一次运行前等待指定的间隔(interval)时间。
  • repeats —— 重复执行多少次(假定 interval > 0),如果设置 repeats <= 0,脚本会一直反复运行。
  • persistent —— 脚本是否要在服务器重置或关闭时保留。(如果你只想让脚本在正常重启时保留,那就不需要设置这个值,系统重新加载时脚本会暂停,重启结束后会自动重新开始)。

还有一个特殊属性:
  • obj —— 这是脚本添加到的物体(如果有的话)。你不需要手动设置它,比如你用 myobj.scripts.add(myscriptpath) 将脚本添加到物体上,或将 myobj 作为 utils.create.create_script 函数的参数,obj 属性会自动设为 myobj。

了解钩子函数也很重要。通常,你在定制自己的脚本时只需要重载它们就可以了。你可以在 src/scripts/scripts.py 中找到更详细的说明。
  • at_script_creation() —— 通常可以在这里设置控制脚本运行的东西,如 interval 和 repeats 属性。它只会在脚本初次创建时被调用一次。
  • is_valid() —— 判断脚本是否仍然在运行。执行 obj.scripts.validate() 时就会调用它,你也可以手动调用它,Evennia 会某些情况下,如重新加载时调用它。它还可以用在脚本状态管理上。如果函数返回 False,说明脚本已经停止并被清除了。
  • at_start() —— 当脚本启动或恢复时会被调用。对于持续性的脚本,在服务器启动时至少会被调用一次。请注意,即使设置了 start_delay 为 True,它也会被立即调用。
  • at_repeat() —— 每间隔 interval 秒会被调用或根本不被调用。它会在启动时被立即调用,除了start_delay 为 True 的时候,在这种情况下,系统会等待间隔秒数然后再调用它。
  • at_stop() —— 不管因为什么原因脚本被停止,它都会被调用。这是做自定义清理工作的好地方。
  • at_server_reload() —— 这会在服务器热重启时(比如执行 @reload 命令时)被调用。这是在重启前保存非持久性数据的好地方。
  • at_server_shutdown() —— 这会在系统重置或关闭时被调用。

与运行相关的方法(通常由系统自动调用,但也可以手动调用)
  • start() —— 这会启动脚本。不论何时你将新脚本加入处理程序都会自动调用它。at_start()将被调用。
  • stop() —— 这会终止并删除脚本。从处理程序中删除脚本时会自动终止脚本的运行。at_stop()将被调用。
  • pause() —— 这会暂停脚本的运行,停止它活动,但不会删除它。所有属性都被保留和计时器也可以恢复。当系统重新加载时它会被自动调用,它不会触发 at_stop() 钩子函数。它只是将脚本挂起,不会改变状态。
  • unpause() —— 恢复之前暂停的脚本。at_start()钩子函数会被调用,让脚本可以恢复内部状态。计时器等都会恢复到暂停之前的状态。服务器在重新加载完成后会自动恢复所有暂停的脚本。
  • force_repeat() —— 这会强制重复执行脚本,而不管它是否应该在其它时间执行。计时器会被复位,at_repeat() 钩子函数会被正常调用。如果限制了重复次数,它会增加已重复的总数。
  • time_until_next_repeat() —— 用于定时脚本,它会返回从现在到下一次执行的秒数。如果 interval==0 则返回None。
  • remaining_repeats() —— 如果脚本的执行次数有限制,它会告诉我们当前剩余的次数。


定义新脚本

有两种创建新脚本类型的方法:
  1. 使用 ev.create_script() 创建脚本实例。这个函数将所有的重要脚本属性(如 interval、locks、start_delay 等)都作为关键字参数。它会返回一个新创建的(已启动)的脚本对象。如果设置了关键字 persistent=True,那返回的脚本也可以在重新加载时保留。
  2. 定义一个新的脚本类型类。你应该在 game/gamesrc/scripts 中的文件里做这事(你可以从 examples/ 文件夹中把脚本模版复制出来,这样你就可以有地方着手了)。下面是一个脚本类型类的例子。
    import random
    from ev import Script

    class Weather(Script):
        "显示天气信息。需要添加到房间上。"
        def at_script_creation(self):
            "在初始化时会被调用一次"
            self.key = "weather_script"
            self.desc = "给出随机的天气信息。"
            self.interval = 60 * 5 # 每5分钟
            self.persistent = True
        def at_repeat(self):
            "每 self.interval 秒被调用一次。"
            rand = random.random()
            if rand < 0.5:
                weather = "微风拂面"
            elif rand < 0.7:
                weather = "云卷天空"
            else:
                weather = "毛毛细雨"
            # 将这条消息发送给该脚本附加到的物体(很可能是房间)中的每一个人
            self.obj.msg_contents(weather)
这是一个可以添加到其它物体上的简单天气脚本,每5分钟它会告诉物体内的每一个人天气如何。

要激活它,只需将它添加到房间的脚本处理程序(scripts)上。在上面例子中,房间就成了 self.obj。现在我们把它添加到一个名为 myroom 的房间上:
myroom.scripts.add(weather.Weather)
如果你写了类型类,可以把你的类型类直接作为 create_script 函数的参数:
    from ev import create_script
    create_script('game.gamesrc.scripts.weather.Weather', obj=myroom)
请注意,如果你给 create_script 添加了关键字参数,它会覆盖你类型类中的默认值。例如:
    create_script('game.gamesrc.scripts.weather.Weather', obj=myroom,
                   permanent=False, interval=10*60)
对于天气脚本的这个特殊实例,运行间隔是10分钟,而且它无法在服务器重新加载时保留。

在游戏中你可以像往常一样用 @script 命令给你自己的类型类添加脚本:
 @script here = weather.Weather
在游戏中你可以用 @scripts 命令很方便地查看、删除运行中的脚本。


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