类型类


你如何表示游戏中的不同物体?是什么让熊有别于石头,让角色有别于房屋,或让AI脚本有别于处理亮暗的脚本?如何将这些差异存储到数据库中?一种方式是为每一种类型创建一张表,于是熊有“爪子”字段,而石头有指定它重量和颜色的字段......很快你就会因为制作无穷无尽的表而抓狂。

与此相反,Evennia使用的是简单通用的数据库模型,然后通过具备特定功能的普通Python类对它们加以“包装”。使用Python类意味着你可以使用Python对象的灵活管理方式。

Evennia中的四种主要游戏“实体”已经有类型类了,它们是玩家物体脚本频道。它们基本上是普通的Python类,它们的行为、继承性等都和普通Python类一样。但当存储数据时,它们会将这些数据自动存到数据库中。

类型类说明图

在上面图中的三种类型类实例是与数据库模型相连的。它们会为你处理所有的数据库交互,你不必再为此操心。数据库模型类不会变化,但连接到它们的类型类可以。类型类的基类(物体、脚本和玩家)可以有任意数量的子类描述各种各样的对象,以上有一些默认分配的例子。

好处是你只需要关心到类型类这一层就可以了,不用管幕后数据库发生的事情。

类型类用起来很简单,只要创建一个继承它的新类就行了:
    from ev import Object

    class Furniture(Object):
        # 在这里定义 Furniture 是什么

对所有类型类实例都适用的属性(玩家、物体、脚本和频道)
所有类型类实例都有一些非常有用的属性和方法。
  • key —— 实例的主要标识符,比如“Rose”、“myscript”或“Paul”。名称也可以使用别名。
  • date_created —— 创建对象的时间。
  • locks —— 控制访问权限的锁处理程序。可用 locks.add()、locks.get() 等。
  • tags —— 标签处理程序。可用 tags.add()、tags.get() 等。
  • attributes —— 管理对象属性的处理程序。可用 attributes.add() 等。
  • nattributes —— 非持久性属性的处理程序,管理不需要保存在数据库中的属性。
  • dbref —— 对象的数据库id(数据库索引)。这是一个唯一的整数,通常你也可以使用 id。
  • is_typeclass(typeclass, exact=False) —— 检查对象的类型类是否与指定的类型类相符。如果exact为False,则父类相符也会返回True。
  • delete() —— 删除这个对象。
  • swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) —— 这会将对象转为另一个类型类。你可以选择是否清除所有的旧属性(如果新类型类和旧的有很大相同就比较有必要这样做)。no_default 用来指定如果转换失败了该怎么办:如果为True,在转换失败时会将其恢复到转换前的类型类,为False则会将其设为 settings 文件中设定的默认类。

此外还有三个属性需要特别注意:
  • db (数据库) —— 这是指向属性处理程序的快捷方式,在存储新属性时你可以使用便捷形式 obj.db.attrname = attrvalue 。
  • ndb (非数据库) —— 这和 db 的功能类似,但会在非持久性的属性处理程序上存储非持久数据 (即数据会在服务器重启时丢失)。
  • typeclass —— 指向类型类自己。可以用它来确保你使用的是类型类对象而不是数据库对象。
  • dbobj —— 指向与类型类相连的数据库对象(相应的,dbobj.typeclass 会指回这个类型类)。

各种类型类的实例会再用自己的属性来扩展这个列表。可阅读物体脚本玩家频道的文档获取更多信息。

重载钩子
在类型类实例中,用户可以重载两类钩子方法。Evennia的大部分钩子只定义在类型类级别上,如at_traverse()。也有一些重要的基本钩子(尤其是调用其它钩子的钩子),它们直接定义在数据库模型上,如objectdb.msg()。下面会说明重载是如何生效的(在以下例子中,object是类型类的实例,dbobject是数据库模型):
  • 对于只定义在类型类上的钩子(如 at_traverse),执行 dbobject.at_traverse() 就和执行 object.at_traverse() 一样,这是最常见的情况。
  • 对于 msg() 这类最初定义在数据库对象上的方法,用起来需要小心一点。如果你重载过类型类上的 msg() ,调用 object.msg() 会执行你重载的版本,但如果调用 dbobject.msg() 则会执行数据库模型中没有重载的原始版本。通常这也不是什么大问题,除非你无法确定对象的类型是类型类还是数据库模型。在这种情况下,你需要明确使用 obj.typeclass.msg() 或 obj.dbobj.msg() 来保证调用的是你想要的 msg() 版本。

使用类型类时的注意事项
类型类基本上是普通的Python类,你可以添加/重载自定义的方法、从它们继承自己的类等等,能对Python类做的大多数事情都可以对它做。但有些事情你需要记住:
  • 使用 ev.create_* 来创建类型类的新实例,而不是仅仅初始化类型类。因此要使用 ev.create_object(MyTypeclass, ...) 来创建 MyTypeclass 类型的新对象,执行 obj = MyTypeclass() 会无法正常工作。
  • 在不同情况下,Evennia会寻找并调用类型类上具有特定名字的钩子方法。只要在类上定义具有正确名字的方法,Evennia就会在适当的时候调用它。钩子是你与服务器进行交互的主要方法。可用的钩子方法列在 game/gamesrc/ 的相应基础模块中。
  • 不要使用普通的 __init__() 来初始化你的类型类,它是 Evennia 用来构建类型类系统的。可以改用预设的钩子方法,如 at_object_creation()、 at_player_creation() 或 at_script_creation() 等。
  • 不要重新实现 Python 的特殊类方法 __setattr__()、 __getattribute__() 和 __delattr__(),它们在类型类系统中被广泛使用。
  • 某些属性名称不能分配给类型类的实例,因为它们已被用于类型类的内部操作。如果你这么做会产生错误。这些属性的名字是 id、dbobj、db、ndb、objects、typeclass、attr、save 和 delete。
  • 即使没有被明确保护,你也不应该重新定义上述属性的“类型”(比如将整型数字存入 key 属性中)。这些属性经常被引擎调用,并需要有特定的返回值类型,有的甚至直接和数据库关联,如果给了不正确的数据类型会导致错误。
  • 进阶注意事项:如果你正在做高级编码,你也许会发现,重载 __init__、_setattr__ 等可以实现一些只靠钩子无法实现的功能。如果你明白你在做些什么,你可以这么去做,但你必须记住还要同时使用 Python 的内置函数 super() 来调用父类的方法,否则你会造成服务器崩溃!已经提醒过你了。


类型类是如何实际工作

这部分是进阶内容。你可以在第一次阅读时跳过它,除非你真的对幕后发生的事情很感兴趣。

所有的类型类实例实际上都由两个(或三个)部分组成:
  • 类型类(普通Python类,带有自定义的 get/set 方法)
  • 数据库模型(Django模型)
  • 属性

类型类基本上是普通的Python类,具有类的所有灵活性。以下是这个类的特殊地方,其中一些上面已经提到过了:
  • 它继承自 src.typeclasses.typeclass.TypeClass。
  • __init__() 方法被各种自定义的初始化程序保留。
  • 它会初始化一个属性 dbobj 指向数据库模型。
  • 它重新定义了自身的 python 通用方法 __getattribute__()、 __setattr__() 和 __delattr__,它们会将自身的所有数据在 dbobj 上同步存取(即在数据库模型上存取)。

相关的数据库模型会相应地在数据库中存取数据。数据库模型有以下(与类型类相关的)特点:
  • 它继承自 src.typeclasses.models.TypedObject(这实际上实现了id映射类型的模型。如果你不明白这是什么也没关系)。
  • 它有一个字段 typelclass_path,给出了与这个模型实例相关连的类型类的Python路径。
  • 它有一个属性 typeclass 会从 typeclass_path 动态导入并加载类型类,并将自己赋给类型类的 dbobj 属性。
  • 为了避免循环,它重新定义了 __getattribute__() 来搜索它对应的类型类。这意味着不管你搜索的是数据库模型还是类型类,都可以找到存储在另一个对象上的数据。

本质上,属性并不是类型类系统的真正组成部分,但它们对于不用改变数据库就保存数据非常的重要。相关内容在这篇单独的文档中。

为什么要这样划分?
数据库模型(Django模型)可以将数据保存到数据库中,这是持久存储数据的最佳地方,对象可以在多次登录中使用这些数据。但它不适合存储游戏所需要的所有数据。你不希望重新定义数据库,仅仅因为汽车对象的外观和行为与椅子对象不一样。因此,我们尽可能保持数据库模型的“通用性”,添加的字段必须是所有对象都需要的(或是为了能快速、规范地搜索数据库)。比如字段"key"和"location"。

进入类型类。由于找不到更合适的词,我们暂且说类型类“包装”了一个Django的数据库模型。通过重新定义类的 get/set 方法,该类型类在幕后与Django模型不断地通信。这样做的好处是这些细节都对你这个程序员隐藏了,只要你不去重载上述的几个强大方法,你几乎可以像对待普通Python类一样对待类型类。你可以扩展它、继承它等等,而不用关心隐藏着的具备完整持久性的数据库实现。所以,你可以创建一个花卉的类型类,然后从它派生出一堆其它的类型类,如玫瑰、郁金香、向日葵等。你的类在实例化时,它们会自动建立指向数据库模型的索引,所有数据实际都存放在那里。而我们可以把类型类和数据库模型当成是一体的。

以下是数据库/类型类的结构示意图。

类型类的详细结构

让我们举个例子看看创建对象的过程是什么样子的。
  1. 我们已在 game.gamesrc.objects.flower.Rose 中定义了一个名为 rose 的类型类。它继承自game.gamesrc.objects.baseobjects.Object,这是 src.typeclasses.typeclass.TypeClass 的孙子类。所以rose和设计的一样,是一个类型类对象。
  2. 我们用命令创建一个 rose 的新实例 !RedRose(比如用 @create redrose:flowers.Rose)。
  3. 一个新的数据库模型被建立,赋以关键字 !RedRose。由于这是一个物体的类型类(不是脚本或玩家的类型类),所以使用的数据库模型是 src.objects.models.ObjectDB,它直接继承自src.typeclasses.models.TypedObject。
  4. 新的Django模型实例收到 Rose 类型类的Python路径,并把它作为字符串存在自己身上(放入数据库字段 typeclass_path 中)。当服务器重启时,数据库模型可以以此为起点重新创建。
  5. 接下来数据库模型会从存储的路径导入类型类,并在内存中创建它的新实例。它将指向这个实例(!RedRose)的引用保存在属性 typeclass 中。
  6. 由于 Rose 被实例化,它的 __init__() 方法被调用。它会将指回Django模型的引用保存到新的 Rose 实例中。这个引用的名字为 dbobj。
  7. 创建方法接着会运行类型类上和启动相关的钩子,如at_object_creation()。

使用类型类 .db 操作符可以将正确类型的属性存储到数据库中。所以 RedRose.db.thorns = True 会创建一个名为 "thorns" 的属性,并存入布尔值 True。

另一方面,存储 RedRose.thorns 只会将数据存为一个普通属性(实际上类型类会透明地传递数据,总是将它存储到数据库模型中)。由于缓存的缘故,同时也为了清晰和可读,我们强烈建议你使用 ndb 存储临时变量,如 RedRose.ndb.newly_planted=True。

反过来,读取属性也可能会去访问你重载的方法。比如 ObjectDB 数据库模型有一个 msg 方法,而你可能想用自己的程序重载它。

所以访问 RedRose.msg 会首先搜索 RedRose 类型类看它是否有自定义的 msg 方法,只有没有搜索到才会继续搜索数据库对象上的属性。这里有一个类型类重载 msg 方法的例子。这是另一个使用 db/ndb 来操作的理由,它们可以表明你想创建/读取的是属性,而不是类中的方法。

下面是访问属性的示意图:

属性访问


类型类系统的注意事项

虽然使用类型类系统而不是直接操纵Django模型有许多优势,但也有一些注意事项需要记住。

使用非Evennia的搜索方法和创建方法时要小心。几乎所有Evennia的代码(包括默认命令)都假设搜索方法或创建方法返回的是类型类而不是Django模型(即两者中的前者)。如果你使用任何模块的管理方法,或 src.utils.create 和 src.utils.search 的创建/搜索功能,都会得到这样的结果。Django大师们会觉得使用Django内建的数据库查询方法很有诱惑力,比如用 ObjectDB.objects.filter() 来获取数据。这可以工作,但返回的结果当然不会是类型类,而是Django模型对象(查询)。你可以用 dbobj.typeclass 和 typeclass.dbobj 轻松地转换它们,但你必须要知道这种区别。
    #自定义的evennia管理方法,这会返回类型类。
    obj = ObjectDB.objects.get_id(1)
    #标准的Django,返回Django模型对象。
    obj = ObjectDB.objects.get(1)
Django迷们更要注意,Evennia的方法返回的是列表,而通常Django方法返回的是 Query 对象(例如 filter() 方法)。只要你没被返回值类型弄糊涂(例如,你不能像“连接”Querysets一样“连接”列表),应该不会有什么事。

阅读 src/ 下每个相关文件夹中的 manager.py 文件,可以看到哪些数据库访问方法是可用的。


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