MUSH风格游戏的基础教程


这是一篇有点长的教你如何在Evennia中构建简单的MUSH风格游戏的教程。即使你对MUSH不感兴趣,你也可以将它作为首个试着构建的游戏,因为它不需要很多的编码工作。而且相关概念还可以作为制作其他类型游戏的参考。

本教程会一步步地教你如何实际建立必要的游戏系统,最终你会拥有一个虽小但功能齐备的游戏。教程会从零基础开始指导,如果你已经做过“最初的编码”教程(这是好主意,但不是必需的),那么有一些配置应该已经完成了,如果是这样,可以跳过这些步骤(也可以顺便检验你是否了解它们所做的事)。

以下是我们需要实现的(非常简略的)特性列表(这是从Evennia的新MUSH用户的功能需求中挑选出来的)。系统中的角色应该:
  • 拥有“Power(力量)”属性,值从1到10,描述他们的强壮程度。 (用来代表属性系统)
  • 拥有命令(如 +setpower 4),可以设置他们的力量。 (用来代表生成角色的代码)
  • 拥有命令(如 +attack),让他们可以按自己的力量生成一个在1到10*Power之间的战斗得分,显示出结果并且将这个数字记录在自己的目标对象上。 (用来代表行动的命令代码)
  • 拥有命令可以显示房间中的每个人,以及他们最近一次的战斗得分。 (用来代表战斗代码)
  • 拥有命令(如 +createNPC Jenkins)可以创建具有完整属性的NPC
  • 拥有命令可以控制NPC,如 +npc/cmd (name)=(command) (用来代表NPC的代码)

在本教程中,我们假定你是从空的数据库开始的,之前没做过任何修改。


服务器设置

要模拟MUSH,使用默认的 MULTISESSION_MODE=0 设置就行了(每个玩家/角色有一个唯一的会话)。这是默认的设置,所以你不需要做任何修改。你仍然可以操纵你拥有控制权的对象,但在这个模式下,默认是没有角色选择功能的。

你用默认的 SQLite3 数据库就可以了。


创建角色

首先要决定我们角色类该如何工作。我们不需要定义专门的NPC对象,归根到底,NPC只是目前没有玩家操纵的角色。

在这个简单例子中,Evennia默认角色类的几个基本命令就已经够用了。之后我们只需要再添加 power 属性就行了。同样的,“战斗得分”也可以在需要的时候随时加入。

不过我们还是要在这里更进一步介绍类型类的概念,只是为了能接下去做更高级的修改(之后我们会用它来修改 look 命令)。使用类型类可以让角色在启动时就默认带上 power 属性和战斗得分。更高级的角色生成系统可能需要添加或删除默认值,而不只是简单地用一句命令来设置它的值。

如何更改默认的类型类在物体类型类教程中有介绍,如果需要更多信息可以参考它,以下是简介(只需要这样做一次)。
  1. 前往 game/gamesrc/objects/examples
  2. 那里应该有一个名为 character.py 的模板文件。将这个文件复制到上一级目录中(即 game/gamesrc/objects/)
  3. 编辑复制的 character.py 文件,添加属性“Power”。见下文。
  4. 使用 @create/drop npc:game.gamesrc.objects.character.Character 创建一个该类型类的新对象做测试,修正相关的bug。
  5. 当你确定新的类可以正常工作了,将以下内容添加到 game/settings.py 中:
    BASE_CHARACTER_TYPECLASS="game.gamesrc.objects.character.Character"
    
  6. 使用 @reload 重启Evennia (这不会将任何人踢出游戏)。

以下是对 character.py 文件的简单修改:
    from ev import Character as DefaultCharacter

    class Character(DefaultCharacter):
        """
         [...]
        """
        def at_object_creation(self):
            "This is called when object is first created, only."   
            self.db.power = 1         
            self.db.combat_score = 1
我们定义了两个新属性 power 和 combat_score,并给它们设置了默认值。请注意,只有新建的角色能看到你的新属性(因为 at_object_creation 钩子函数只在对象初次创建时被调用,现有的角色不会有新的属性)。要更新自己的角色可以执行
 @typeclass/force/reset self=game.gamesrc.objects.character.Character


新的默认命令

我们创建的大部分代码会以命令的形式出现。

首先要告诉Evennia,我们要使用自定义的默认命令集。这在添加命令教程中有详细论述,本例中的主要内容也就是该教程的第一部分内容(你只需要这样做一次):
  1. 前往 game/gamesrc/commands。
  2. 那里有一个名为 examples 的子文件夹,将 examples/cmdset.py 复制到当前目录中(game/gamesrc/commands)。
  3. 编辑 game/settings.py,加入以下内容:
    CMDSET_CHARACTER="game.gamesrc.commands.cmdset.CharacterCmdSet"
    
  4. 使用 @reload 重启Evennia (这不会将任何人踢出游戏)。

此后Evennia就会将这个模块中的 CharacterCmdSet 类作为角色的命令(模板会导入所有默认值)。打开这个文件,看看 CharacterCmdSet 是在什么地方导入、添加新命令的(那里还有一些注释掉的例子)。


生成角色

在这个例子中我们假设玩家会首先连接到“角色生成区”。Evennia支持完整的基于菜单的角色产生系统,但在本例中,有一个简单的起始房间就够了。在这个(些)房间中我们要允许使用角色生成命令,而且角色生成命令也只能在这样的房间中使用。

请注意,本例中的做法可以很方便地扩展成完整的系统。在这个简单的例子中,我们可以给玩家添加一个 is_in_chargen(是否正在生成角色)标志,并且让 +setpower 命令检查它。使用这种方法可以便于以后添加更多的功能。

接下来我们需要以下东西:
  • 一个角色生成命令,可以在角色上设置“power”
  • 一个包含该命令的角色生成命令集,让我们把它命名为 ChargenCmdset。
  • 一个自定义的 ChargenRoom 类型,让处于该类型房间中的玩家可以使用这个命令集。
  • 一个该类型的可供测试的房间。

+setpower 命令

在 game/gamesrc/commands/examples/ 中有一个名为 command.py 的模板文件。将它复制到上一级目录(game/gamesrc/commands),为了便于识别,将它重命名为 mushcommands.py。我们要将所有的命令添加到该文件的末尾。

在本教程中,角色生成只包含一条MUSH类型的命令
 +setpower 4
打开新的 mushcommands.py 文件。它包含了基本命令的模板以及Evennia默认使用的“MuxCommand”类型。对于这些简单的MUSH类命令,我们只需要用到基本类型。

将以下内容添加到 mushcommands.py 文件的末尾:
    # end of mushcommands.py
    from ev import Command    

    class CmdSetPower(Command):
        """
        set the power of a character

        Usage: 
          +setpower <1-10>

        This sets the power of the current character. This can only be 
        used during character generation.    
        """

        key = "+setpower"
        help_category = "mush"

        def func(self):
            "在这里实际执行命令"
            errmsg = "You must supply a number between 1 and 10."
            if not self.args:
                self.caller.msg(errmsg)      
                return
            try:
                power = int(self.args)  
            except ValueError:
                self.caller.msg(errmsg)
                return
            if not (1 <= power <= 10):
                self.caller.msg(errmsg)
                return
            # 至此参数已经通过了验证,现在来设置它。
            self.caller.db.power = power
            self.caller.msg("Your Power was set to %i." % power)
这是一个非常简单的命令。我们做了一些错误检查,然后设置自己的 power 属性。我们将命令的 help_category 都设成“mush”,这只是为了能在帮助列表中更容易地找出它们。

保存该文件。现在,我们将它添加到新的命令集中,这样才能使用它(当然,对于完整的角色生成系统,你可以在这里添加更多的命令)。

回到 game/gamesrc/commands/cmdset.py,在模块顶部导入新的 mushcommands 模块:
    from game.gamesrc.commands import mushcommands
接着向下滚动到文件末尾,在那里定义一个新的命令集,其中只包含我们的角色生成命令:
    # end of cmdset.py

    class ChargenCmdset(CmdSet):
        """
        This cmdset it used in character generation areas.
        """
        key = "Chargen"
        def at_cmdset_creation(self):
            "This is called at initialization"
            self.add(mushcommands.CmdSetPower()
以后你可以在这个命令集中添加任意数量的命令,按你所希望的方式扩展角色生成系统。现在,我们需要将命令集实际添加到某样东西上,让它可以被用户使用。我们可以把它直接放在角色上,但是这样一来在任何时候都可以使用它了。把它添加到房间上会更好,这样只有处在这个房间中的玩家可以使用它。

角色生成区域
我们要创建一个简单的房间类型类,作为我们所有角色生成区域的模板。前往 game/gamesrc/objects/examples ,那里应该有一个模板文件 room.py。将它复制到上一级目录(game/gamesrc/objects)。你可以按你的想法给它重命名,但在这里我们假设你没有这么做。编辑这个文件,在它的末尾添加一个新类型类 ChargenRoom:
    # end of room.py

    from game.gamesrc.commands.cmdset import ChargenCmdset
    class ChargenRoom(Room):
        """
        This room class is used by character-generation rooms. It makes
        the ChargenCmdset available.
        """
        def at_object_creation(self):
            "this is called only at first creation"
            self.cmdset.add(ChargenCmdset, permanent=True)
注意一下该类型类的新房间是怎样在一开始就加载 ChargenCmdset 的。不要忘了 permanent=True 关键字参数,否则在服务器重启之后会失去这个命令集。有关命令集命令的详细信息请参阅相关链接。

测试角色生成系统
如果没有可用的角色生成区域是无法做测试的。登录进入游戏(现在你使用的应该是新的自定义角色类)。让我们创建一个角色生成区域做测试。
 @dig chargen:room.ChargenRoom = chargen,finish
这会创建一个 ChargenRoom 类型的新房间,并且打开名为 chargen 的出口通向那里,返回这里的出口名为 finish。如果你在这个一步看到有报错信息,必须修正它们的代码,然后调用 @reload,直到可以正常工作了才能继续下一步。
 chargen
在角色生成房间中,你应该可以使用 +setpower 命令了,所以开始测试吧。在你离开后(通过 finish 出口),该命令就会消失。(作为特权用户)使用 ex me 命令检查 power 属性是否被正确设置了。

如果没有正常工作,请检查你的类型类和命令中是否有bug以及指向命令集和命令的路径是否输入正确。检查日志或命令行,看是否有回溯或报错。


战斗系统

我们要将战斗命令添加到默认命令集中,因为所有人在任何时间都会使用到它。

战斗系统要包含能够生成随机结果的方法(+attack),需要保存这个值用它来生成“战斗得分”。其他人应该可以列出房间中的所有当前战斗得分。要做到这一点,一种方法是添加一个名字类似 +combatscores 的命令,但我们要用另一种方法,让默认的 look 命令来做这些事,在它输出的内容中增加显示我们的得分。

用 +attack 命令攻击
前往 game/gamesrc/commands/mushcommands.py 在末尾添加以下命令:
    import random

    class CmdAttack(Command):
        """
        issues an attack 

        Usage: 
            +attack 

        This will calculate a new combat score based on your Power.
        Your combat score is visible to everyone in the same location.
        """
        key = "+attack"
        help_category = "mush"

        def func(self):
            "Calculate the random score between 1-10*Power"
            caller = self.caller
            power = caller.db.power
            if not power:
                # 如果命令的调用者不是我们自定义的 
                # 角色类型类对象,则会出现这种情况
                power = 1
            combat_score = random.randint(1, 10 * power)
            caller.db.combat_score = combat_score

            # 显示信息
            message = "%s +attack%s with a combat score of %s!"
            caller.msg(message % ("You", "", combat_score))
            caller.location.msg_contents(message % 
                                         (caller.key, "s", combat_score),
                                         exclude=caller)
我们在这里所做的只是使用Python内置的 random.randint() 函数生成“战斗得分”。然后保存它,并向所有相关的人显示结果。

为了让你在游戏中可以使用 +attack 命令,前往 game/gamesrc/commands/cmdset.py。你在添加 +setpower 命令时应该已经在顶端导入过 mushcommands 了。

向下滚动到 CharacterCmdSet 类,并且在正确的位置添加以下内容:
    self.add(mushcommands.CmdAttack)
在Evennia中执行 @reload 重启,+attack 命令应该就可以使用了。执行它,然后使用 @ex 之类的命令检查属性 combat_score 是否被正确保存了。

让“look”显示战斗得分
下一步,我们希望能从指定角色身上看出他们的战斗得分。
 >  look Tom
Tom (战斗得分: 3)
这是一个伟大的战士。
其实我们不需要修改look命令。要知道为什么,就要看看默认的 look 命令是如何定义的。它位于 src/commands/default/general/ 中(你也可以在这里在线浏览它)。你会发现,look 命令会调用观看对象上的 return_appearance 函数,而它会生成实际显示的文本。look 命令所做的全部事情就是显示这个函数返回的东西。因此,我们要做的就是编辑自定义的角色类型类,重载它的 return_appearance 函数,让它返回我们想要的东西(这是自定义类型类的实际优势之一)。

回到 game/gamesrc/objects/character.py 中的自定义角色类型类。默认的 return_appearance 实现可以在 src/objects/objects.py 中找到(或点击这里在线浏览)。如果你想做比较大的改动,可以将所有默认的东西都复制粘贴到我们重载的方法中。但本例中要做的改动很小:
    class Character(DefaultCharacter):
        """
         [...]
        """
        def at_object_creation(self):
            "This is called when object is first created, only."   
            self.db.power = 1         
            self.db.combat_score = 1

        def return_appearance(self, looker):
            """
            The return from this method is what
            looker sees when looking at this object.
            """
            text = super(Character, self).return_appearance(looker)
            cscore = " (combat score: %s)" % self.db.combat_score
            if "\n" in text:
                # 文本有多行,将得分加在第一行之后
                first_line, rest = text.split("\n", 1)
                text = first_line + cscore + "\n" + rest
            else:
                 # 文本只有一行,将得分加在最后
                text += cscore
            return text
我们所做的只是让默认的 return_appearance 做它该做的事(super 会调用父类)。然后分离出文字的第一行,添加战斗得分,再把它重新组合起来。

用 @reload 重启服务器,你应该可以看到其他角色上的当前战斗得分了。

注:还有一种方法可能用处更大,就是重载房间的整个 return_appearance 方法,修改它展示内容的方式,这样在察看房间的时候就能同时看到所有在场角色的当前战斗得分了。我们把这个留作练习。


NPC系统

在这里,我们要复用 Character 类并加入创建 NPC 对象的命令。我们要给它设置 power 属性,并且要让它动作起来。

有几种方法可以定义 NPC 类。理论上我们可以为它创建一个自定义类型类,并且给所有 NPC 添加自定义的 NPC 专用命令集,这个命令集可以带上所有的操纵命令。但由于我们希望操纵 NPC 是用户上的常见事件,我们会将所有与 NPC 相关的命令放入默认命令集中,通过权限和锁来控制对它的访问。

使用 +createNPC 创建 NPC
在 mushcommands.py 的末尾创建新命令:
import ev

class CmdCreateNPC(Command):
    """
    create a new npc

    Usage:
    +createNPC 

    Creates a new, named NPC. The NPC will start with a Power of 1.
    """ 
    key = "+createnpc"
    aliases = ["+createNPC"]
    locks = "call:not perm(nonpcs)"
    help_category = "mush" 

    def func(self):
        "creates the object and names it"
        caller = self.caller
        if not self.args:
            caller.msg("Usage: +createNPC ")
            return
        if not caller.location:
            # 不能在脱离角色的状态下创建NPC
            caller.msg("You must have a location to create an npc.")
            return
        # 名字的首字母必须大写
        name = self.args.capitalize()
        # 在调用者当前所在的位置创建NPC
        npc = ev.create_object("game.gamesrc.objects.character.Character", 
                               key=name, 
                               location=caller.location,
                               locks="edit:id(%i) and perm(Builders)" % caller.id)
        # 显示信息
        message = "%s created the NPC '%s'."
        caller.msg(message % ("You", name)) 
        caller.location.msg_contents(message % (caller.key, name), exclude=caller)        
在这里,我们定义了 +createnpc(用 +createNPC 也一样),每个没有 nonpcs 权限的人都可以调用它(在Evennia中,“权限”也可以用来阻止访问,这取决于我们定义的锁)。我们在调用者当前所在的位置用我们自定义的角色类型类创建 NPC 对象。

我们还在 NPC 上设置了另外的锁,之后可以用它来判断什么人可以编辑 NPC:我们允许创建者以及任何具有建造者或更高权限的人编辑。关于锁系统的更多信息请见锁的文档

请注意,我们只给对象默认的权限(没有在 create_object() 中指定 permissions 关键字参数)。在某些游戏中可能会给予 NPC 和它的创建者一样的权限,但这可能有安全隐患。

像之前添加 +attack 命令的方法一样,将这个命令添加到默认命令集中。调用 @reload 重启,然后它就可以使用了。

用 +editNPC 命令编辑 NPC

因为我们复用了自定义的角色类型类,我们的新NPC已经有 power 值了,它默认为1。我们如何修改它呢?

对此有几种方法可做。最简单方法是考虑到 power 属性也只是存储在 NPC 对象上的简单属性,所以建造者或管理员可以直接用默认的 @set 命令设置它:
 @set mynpc/power = 6
但 @set 命令太通用太强大了,因此只提给工作人员使用。我们要添加一个自定义命令,只能让玩家修改允许的值。原则上我们可以在这里使用前面的 +setpower 命令,但让我们来做一些更有用的东西,我们来创建 +editNPC 命令。

这是一个稍复杂一些的命令。像之前一样将它添加到 mushcommands.py 文件的末尾。
    class CmdEditNPC(Command):
        """
        edit an existing NPC

        Usage: 
          +editnpc [/ [= value]]

        Examples:
          +editnpc mynpc/power = 5
          +editnpc mynpc/power    - 显示 power 的值
          +editnpc mynpc          - 显示所有可编辑的属性和变量

        This command edits an existing NPC. You must have 
        permission to edit the NPC to use this.
        """
        key = "+editnpc"
        aliases = ["+editNPC"]
        locks = "cmd:not perm(nonpcs)"
        help_category = "mush" 

        def parse(self):
            "我们需要在这里解析命令"
            args = self.args
            propname, propval = None, None
            if "=" in args: 
                args, propval = [part.strip() for part in args.rsplit("=", 1)]     
            if "/" in args:
                args, propname = [part.strip() for part in args.rsplit("/", 1)]
            # 保存结果,这样我们在之后调用的func()中就可以使用它们了
            self.name = args
            self.propname = propname
            # 只有属性值而没有属性名是无意义的
            self.propval = propval if propname else None

        def func(self):
            "do the editing"

            allowed_propnames = ("power", "attribute1", "attribute2")

            caller = self.caller
            if not self.args or not self.name:
                caller.msg("Usage: +editnpc name[/propname][=propval]")            
                return
            npc = caller.search(self.name)
            if not npc:
                return 
            if not npc.access(caller, "edit"):
                caller.msg("You cannot change this NPC.")
                return         
            if not self.propname:
                # 这表示要列出属性值
                output = "Properties of %s:" % npc.key
                for propname in allowed_propnames: 
                    propvalue = npc.attributes.get(propname, default="N/A")
                    output += "\n %s = %s" % (propname, propvalue)
                caller.msg(output)
            elif self.propname not in allowed_propnames: 
                caller.msg("You may only change %s." % 
                                  ", ".join(allowed_propnames))
            elif self.propval:
                # 赋新的属性值
                # 在本例中,所有的属性都是整型的……
                intpropval = int(self.propval)  
                npc.attributes.add(self.propname, intpropval) 
                caller.msg("Set %s's property '%s' to %s" %
                             (npc.key, self.propname, self.propval))
            else:
                # 有属性名而没有属性值,显示当前值
                caller.msg("%s has property %s = %s" % 
                                 (npc.key, self.propname, 
                                  npc.attributes.get(propname, default="N/A")))
这个命令的例子展示了如何使用更高级的解析,而不只是用于错误检查。它会在当前房间内搜索指定的 NPC,并在继续操作之前检查调用者是否真的拥有“edit”权限。没有适当权限的玩家甚至不能查看指定 NPC 的属性。每个游戏可以按各自的需求做出这样的规定。

像之前一样将它添加到默认命令集中,然后你就可以使用它了。

注:如果你想让玩家使用这个命令修改对象的内建属性,如NPC的名字(属性 key),你需要修改命令,因为“key”不是通用属性(它不是通过 npc.attributes.get 获得的,而是直接通过 npc.key)。我们把它留作可选的练习。

让NPC动起来 —— +npc命令
最后,我们要创建一个命令来指挥NPC做动作。目前,我们会限制这个命令只能由具有“edit”指定NPC权限的人使用。如果想让所有人都可以指挥NPC,也可以对它做修改。

由于NPC继承了我们的角色类型类,它可以使用大部分的命令,但它不能使用基于会话和玩家的命令集(因此它们不能在频道中聊天,但如果你添加了这些命令,它们就可以这么做了)。

这使得 +npc 命令很简单。同样的,将一下内容添加到 mushcommands.py 模块的末尾:
    class CmdNPC(Command):
        """
        controls an NPC

        Usage: 
            +npc  = 

        This causes the npc to perform a command as itself. It will do so with its
        own permissions and accesses. 
        """
        key = "+npc"
        locks = "call:not perm(nonpcs)"
        help_category = "mush"

        def parse(self):
            "只是按 = 号做分割"
            name, cmdname = None, None
            if "=" in self.args:
                name, cmdname = [part.strip() for part in self.args.rsplit("=", 1)]
            self.name, self.cmdname = name, cmdname

        def func(self):
            "Run the command"
            caller = self.caller
            if not self.cmdname:
                caller.msg("Usage: +npc  = ")
                return
            npc = caller.search(self.name)   
            if not npc:
                return
            if not npc.access(caller, "edit"):
                caller.msg("You may not order this NPC to do anything.")
                return
            # 执行命令
            npc.execute_cmd(self.cmdname)
            caller.msg("You told %s to do '%s'." % (npc.key, self.cmdname))
需要注意的是,如果你给出了错误的命令,你不会看到任何的出错信息,因为出错信息是返回给NPC的,而不是你。如果你希望玩家能看到它,可以给将调用者的会话ID传给 execute_cmd 函数,如下所示:
    npc.execute_cmd(self.cmdname, sessid=self.caller.sessid)
另外需要记住的是,除了通过这个方法非常方便地控制NPC,Evennia还支持以另一种非常简单的操纵方式。一个玩家(假定具有正确地“操纵”权限)可以简单地调用 @ic mynpc,然后就能在游戏中以NPC的身份进行游戏了。实际上玩家就是以这种方式控制他们的角色的。


结束语

教程这就结束了。它看起来文字很多,但你要写的代码其实是比较少的。现在你应该拥有一个基本的游戏框架,并且对编写游戏代码有点感觉了。

之后,你可以建立更多的 ChargenRooms 并将它们连接成更大的结构。+setpower 命令可以扩展,还可以添加更的多类似命令,以建立更复杂的角色生成系统。

简单的“power”游戏机制应该可以很容易地扩展成更丰富、更有用的系统,战斗得分系统也一样。“+attack”可以改成针对特定玩家(或NPC),并且会自动比较两者的相关属性以确定攻击的结果。

在此之后,你可以看看教程世界。如果想要了解更具体开发思想,可以看看其他的教程和提示,包括开发者中心