MUSH 风格游戏的基本教程

本教程让你在 Evennia 中编码一个小而完整的 MUSH 风格游戏。MUSH 是一种以角色扮演为中心的游戏类型,专注于自由形式的叙事。即使你对 MUSH 不感兴趣,这仍然是一个很好的第一款游戏类型,因为它不那么重视代码。你将能够使用相同的原则来构建其他类型的游戏。

本教程从头开始。 如果你已经完成了 第一步编码 的教程,你应该对如何做某些步骤已经有了一些想法。

以下是我们将实现的(非常简化和删减的)功能(来源于 MUSH 用户对 Evennia 的功能请求)。在这个系统中,角色应该:

  • 拥有一个从 1 到 10 的“力量”分数,表示他们的强度(代表属性系统)。

  • 拥有一个命令(例如 +setpower 4)来设置自己的力量(代表角色生成代码)。

  • 拥有一个命令(例如 +attack),允许他们投掷力量并生成一个“战斗分数”,介于 110*Power 之间,显示结果并编辑他们的对象以记录此数字(代表 +actions 代码)。

  • 拥有一个命令,可以显示房间中的每个人及其最近的“战斗分数”投掷(代表战斗代码)。

  • 拥有一个命令(例如 +createNPC Jenkins)来创建一个具有完整能力的 NPC。

  • 拥有一个命令来控制 NPC,例如 +npc/cmd (name)=(command)(代表 NPC 控制代码)。

在本教程中,我们假设从空数据库开始,没有任何之前的修改。

服务器设置

为了模拟 MUSH,默认的 MULTISESSION_MODE=0 就足够了(每个帐户/角色一个唯一会话)。这是一种默认设置,因此你无需更改任何内容。你仍然可以操纵/取消操纵你有权限的对象,但没有现成的角色选择功能。

我们将假设我们的游戏文件夹称为 mygame。你应该可以使用默认的 SQLite3 数据库。

创建角色

首先要选择我们的角色类如何运作。我们不需要定义一个特殊的 NPC 对象——NPC 毕竟只是一个当前没有帐户控制的角色。

mygame/typeclasses/characters.py 文件中进行更改:

# mygame/typeclasses/characters.py

from evennia import DefaultCharacter

class Character(DefaultCharacter):
    """
     [...]
    """
    def at_object_creation(self):
        "这是只在对象首次创建时调用。"
        self.db.power = 1
        self.db.combat_score = 1

我们定义了两个新的 属性powercombat_score,并将它们设置为默认值。如果你已经运行服务器,请确保 @reload 服务器(每次更新 Python 代码时都需要重新加载,不用担心,没有帐户会被断开连接)。

请注意,只有 角色才能看到你的新属性(因为 at_object_creation 钩子在对象首次创建时被调用,现有角色不会拥有它)。要更新自己,请运行

@typeclass/force self

这将重置你的类型类(/force 开关是一个安全措施,以防止意外执行),这意味着 at_object_creation 会重新运行。

examine self

在“持久属性”标题下,你现在应该可以看到新属性 powercombat_score。如果没有,首先确保你已经 @reload 了新代码,然后查看服务器日志(在终端/控制台中),以检查是否有任何语法错误阻止新代码正确加载。

角色生成

在此示例中,我们假设帐户首先连接到一个“角色生成区域”。Evennia 还支持完整的 OOC 菜单驱动角色生成,但对于此示例,一个简单的起始房间就足够了。当在这个房间(或房间)中时,我们允许角色生成命令。实际上,角色生成命令将 在这样的房间中可用。

注意,再次这样做是为了便于扩展到完整的游戏系统。在我们简单的示例中,我们可以简单地在账户上设置一个 is_in_chargen 标志,并让 +setpower 命令进行检查。然而使用这种方法,方便后续添加更多功能。

我们需要以下内容:

  • 一个角色生成 命令 用于设置角色的“力量”。

  • 一个角色生成 CmdSet 来保存这个命令。我们称之为 ChargenCmdset

  • 一个自定义的 ChargenRoom 类型,使这个命令集能够在这样的房间中提供给玩家。

  • 一个这样的房间来进行测试。

+setpower 命令

为了本教程,我们将将所有新命令添加到 mygame/commands/command.py 中,但如果你希望,你可以将命令拆分到多个模块中。

在本教程中,角色生成将仅包括一个 命令,用于设置角色的“力量”属性。它将使用以下类似 MUSH 的形式调用:

+setpower 4

打开 command.py 文件。它包含基于默认命令和 Evennia 中使用的“MuxCommand”类型的空模板。我们将使用普通的 Command 类型,MuxCommand 类提供了一些额外功能,例如去除多余的空格,如果需要,请从那里导入。

command.py 文件末尾添加以下内容:

# end of command.py
from evennia import Command  # 仅为清晰起见;已在上方导入

class CmdSetPower(Command):
    """
    设置角色的力量

    用法:
      +setpower <1-10>

    这将设置当前角色的力量。这只能在角色生成期间使用。
    """

    key = "+setpower"
    help_category = "mush"

    def func(self):
        "执行实际命令"
        errmsg = "你必须提供一个介于 1 和 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(f"你的力量已设置为 {power}。")

这是一个相当简单的命令。我们进行了一些错误检查,然后设置自己的力量。我们使用 help_category 为“mush”,以便使所有命令易于查找并在帮助列表中分开。

保存文件。现在,我们将其添加到一个新的 CmdSet 中,以便可以访问(在完整的角色生成系统中,你当然会在这里有多个命令)。

打开 mygame/commands/default_cmdsets.py,在顶部导入你的 command.py 模块。我们还为下一步导入默认的 CmdSet 类:

from evennia import CmdSet
from commands import command

接下来,向下滚动并定义一个新的命令集(基于我们刚才在此文件末尾导入的基本 CmdSet 类),仅保存我们的角色生成特定命令:

# end of default_cmdsets.py

class ChargenCmdset(CmdSet):
    """
    这个命令集在角色生成区域使用。
    """
    key = "Chargen"
    def at_cmdset_creation(self):
        "这在初始化时被调用"
        self.add(command.CmdSetPower())

将来,你可以将任何数量的命令添加到此命令集中,以扩展你希望的角色生成系统。现在,我们需要将该命令集放在某个地方,以便为用户提供访问权限。我们可以直接将其放在角色上,但这样会使其随时可用。将其放在房间上更清晰,因此当玩家在该房间时,才可用。

角色生成区域

我们将创建一个简单的房间类型作为所有角色生成区域的模板。接下来编辑 mygame/typeclasses/rooms.py

from commands.default_cmdsets import ChargenCmdset

# ...
# 在 rooms.py 文件末尾

class ChargenRoom(Room):
    """
    这个房间类用于角色生成房间。它使
    ChargenCmdset 可用。
    """
    def at_object_creation(self):
        "这仅在首次创建时调用"
        self.cmdset.add(ChargenCmdset, persistent=True)

注意,使用此类型类创建的新房间将始终在自身上启动 ChargenCmdset。不要忘记 persistent=True 关键字,否则在服务器重新加载后将丢失命令集。有关 命令集命令 的更多信息,请参见各自的链接。

测试角色生成

首先,确保你已经 @reload 了服务器(或使用终端中的 evennia reload),以将新 Python 代码添加到游戏中。检查你的终端并修复您看到的任何错误——错误回溯会准确列出错误所在——查看您已更改文件的行号。

在没有角色生成区域的情况下,我们无法测试。登录游戏(此时你应该使用新的自定义角色类)。让我们挖掘一个角色生成区域进行测试。

@dig chargen:rooms.ChargenRoom = chargen,finish

如果您阅读 @dig 的帮助,您会发现这将创建一个名为 chargen 的新房间。冒号后面的部分是你想要使用的类型类的 Python 路径。由于 Evennia 将自动尝试进入游戏目录的 typeclasses 文件夹,因此我们只需指定 rooms.ChargenRoom,这意味着它将在 rooms.py 模块中查找名为 ChargenRoom 的类(这就是我们上面创建的)。在名称后面的 = 中给出的名称是从当前地点到房间的出口名称。你还可以为每个名称附加别名,例如 chargen;角色生成

因此,总结一下,这将创建一个 ChargenRoom 类型的新房间,并打开一个到它的出口 chargen 和一个出口回到这里,名称为 finish。如果在此阶段看到错误,你必须修复代码中的错误。@reload 在修复之间。请勿继续,直到创建似乎正常工作为止。

chargen

这应该会将你带到角色生成房间。在那里,你应该现在拥有 +setpower 命令,因此测试一下。当你离开(通过 finish 出口)时,该命令将消失,尝试 +setpower 现在应该会给你一个命令未找到错误。使用 ex me(作为特权用户)来检查 Power 属性 是否已正确设置。

如果事情不工作,请确保你的类型类和命令没有错误,并确保你输入的各种命令集和命令的路径是正确的。检查日志或命令行,寻找回溯和错误。

战斗系统

我们将将我们的战斗命令添加到默认命令集中,这意味着它将始终对所有人可用。战斗系统由 +attack 命令组成,以确定我们攻击的成功程度。我们还将更改默认的 look 命令,以显示当前战斗分数。

使用 +attack 命令进行攻击

在这个简单的系统中,攻击意味着投掷一个受 power 属性影响的随机“战斗分数”:

> +attack
你 +attack 的战斗分数为 12!

返回到 mygame/commands/command.py,在文件末尾添加命令,如下所示:

import random

# ...

class CmdAttack(Command):
    """
    发起攻击

    用法:
        +attack

    这将根据你的力量计算新的战斗分数。
    你的战斗分数对同一位置的所有人可见。
    """
    key = "+attack"
    help_category = "mush"

    def func(self):
        "计算 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_template = "{attacker} +attack{s} 的战斗分数为 {c_score}!"
        caller.msg(message_template.format(
            attacker="你",
            s="",
            c_score=combat_score,
        ))
        caller.location.msg_contents(message_template.format(
            attacker=caller.key,
            s="s",
            c_score=combat_score,
        ), exclude=caller)

我们在这里做的只是使用 Python 内置的 random.randint() 函数生成一个“战斗分数”。然后我们存储这个结果并将其回显给所有在场的人。

要让 +attack 命令在游戏中可用,请返回到 mygame/commands/default_cmdsets.py 并向下滚动到 CharacterCmdSet 类。在适当的位置添加此行:

self.add(command.CmdAttack())

@reload Evennia,+attack 命令应对你可用。运行并使用例如 @ex 确保 combat_score 属性正确保存。

让“look”显示战斗分数

玩家应该能够查看房间中的所有当前战斗分数。我们可以通过简单地添加一个名为 +combatscores 的第二个命令来实现,但我们将让默认的 look 命令为我们做繁重的工作,并将我们的分数显示为其正常输出的一部分,像这样:

> look Tom
Tom(战斗分数:3)
这是一个伟大的战士。

然而,我们并不需要实际修改 look 命令。要理解原因,请查看默认的 look 实际定义。它位于 evennia/commands/default/general.py

你会发现,实际返回文本是通过 look 命令调用一个名为 return_appearance钩子方法 实现的。所有 look 做的就是回显由此钩子返回的内容。因此,我们需要做的是编辑我们自定义的角色类型类,并重载它的 return_appearance,以返回我们想要的内容(这就是拥有自定义类型类的实际好处)。

返回到在 mygame/typeclasses/characters.py 中的自定义角色类型类。return_appearance 的默认实现位于 evennia.DefaultCharacter

如果你想进行更大的更改,你可以将整个默认内容复制并粘贴到我们重载的方法中。然而在我们的例子中,更改很小:

class Character(DefaultCharacter):
    """
     [...]
    """
    def at_object_creation(self):
        "这是只在对象首次创建时调用。"
        self.db.power = 1
        self.db.combat_score = 1

    def return_appearance(self, looker):
        """
        来自此方法的返回内容是
        观察者在查看该对象时所看到的。
        """
        text = super().return_appearance(looker)
        cscore = f"(战斗分数:{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 将调用父类的同一方法)。然后我们将这段文本的第一行分出,附上我们的 combat_score,并重新组合在一起。

@reload 服务器,你应该能够查看其他角色并看到他们当前的战斗分数。

注意:更有用的一种方式是重载整个 Roomreturn_appearance,并修改它们列出内容的方式;这样一来,人们在查看房间时就可以同时看到所有在场角色的战斗分数。我们将此作为练习留给你。

NPC 系统

这里我们将通过引入一个可以创建 NPC 对象的命令来重用角色类。我们还应该能够设置其力量并指挥它。

定义 NPC 类有几种方法。理论上,我们可以为其创建一个自定义的类型类,并在所有 NPC 上放置一个特定于 NPC 的自定义命令集。这个命令集可以包含所有操作命令。然而,由于我们期望 NPC 操作在用户群体中会是常见的,因此我们将把所有相关 NPC 命令放在默认命令集中,并通过 权限和锁 限制最终访问。

使用 +createNPC 创建 NPC

我们需要一个用于创建 NPC 的命令,这是一个非常简单的命令:

> +createnpc Anna
你创建了 NPC 'Anna'。

command.py 的末尾,创建我们的新命令:

from evennia import create_object

class CmdCreateNPC(Command):
    """
    创建一个新的 NPC

    用法:
        +createNPC <name>

    创建一个新的、命名的 NPC。 NPC 将以 1 的力量开始。
    """
    key = "+createnpc"
    aliases = ["+createNPC"]
    locks = "call:not perm(nonpcs)"
    help_category = "mush"

    def func(self):
        "创建对象并命名"
        caller = self.caller
        if not self.args:
            caller.msg("用法:+createNPC <name>")
            return
        if not caller.location:
            # 不允许在 OOC 时创建 NPC
            caller.msg("你必须有一个位置来创建 NPC。")
            return
        # 姓名始终以大写字母开头
        name = self.args.strip().capitalize()
        # 在调用者的位置创建 NPC
        npc = create_object("characters.Character",
                      key=name,
                      location=caller.location,
                      locks=f"edit:id({caller.id}) and perm(Builders);call:false()")
        # 宣布
        message_template = "{creator} 创建了 NPC '{npc}'。"
        caller.msg(message_template.format(
            creator="你",
            npc=name,
        ))
        caller.location.msg_contents(message_template.format(
            creator=caller.key,
            npc=name,
        ), exclude=caller)

在这里,我们定义了 +createnpc+createNPC 也可以),其可由未拥有 nonpcs权限 的每个人调用(在 Evennia 中,“权限”也可以用于阻止访问,这取决于我们定义的锁)。我们在调用者的当前位置创建 NPC 对象,使用我们自定义的 Character 类型类来做到这一点。

我们在 NPC 上设置了一个额外的锁条件,我们将在稍后用于检查谁可以编辑 NPC——我们允许创建者这样做,以及任何拥有构建者权限(或更高权限)的人。有关锁系统的更多信息,请参见

请注意,我们只是向对象提供默认权限(通过不指定 permissions 关键字来调用 create_object())。在某些游戏中,人们可能希望给 NPC 赋予与创建它们的角色相同的权限,但这样做可能会有安全风险。

像以前一样将此命令添加到你的默认命令集中。@reload 并且它将可以进行测试。

使用 +editNPC 编辑 NPC

由于我们重用了自定义角色类型类,新的 NPC 已经有了 力量 值—默认值为 1。我们如何更改这个?

我们可以选择几种方法。最简单的办法是记住 power 属性仅仅是一个简单的 属性,存储在 NPC 对象上。因此,作为构建者或管理员,我们可以立即使用默认的 @set 命令设置它:

@set mynpc/power = 6

不过,@set 命令过于强大,因此仅限于工作人员。我们将添加一个自定义命令,仅更改我们希望玩家能够更改的内容。原则上,我们可以重新设计旧的 +setpower 命令,但让我们尝试一些更有用的东西。我们将创建一个 +editNPC 命令。

> +editNPC Anna/power = 10
将 Anna 的属性 'power' 设置为 10。

这是一个稍微复杂一点的命令。它将放在 command.py 文件末尾,如前所述。

class CmdEditNPC(Command):
    """
    编辑现有 NPC

    用法:
      +editnpc <name>[/<属性> [= 值]]

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

    此命令用于编辑现有 NPC。你必须有
    编辑该 NPC 的权限才能使用此命令。
    """
    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):
        "进行编辑"

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

        caller = self.caller
        if not self.args or not self.name:
            caller.msg("用法:+editnpc name[/propname][=propval]")
            return
        npc = caller.search(self.name)
        if not npc:
            return
        if not npc.access(caller, "edit"):
            caller.msg("你无法更改此 NPC。")
            return
        if not self.propname:
            # 这意味着我们只是列出了值
            output = f"{npc.key} 的属性:"
            for propname in allowed_propnames:
                propvalue = npc.attributes.get(propname, default="N/A")
                output += f"\n {propname} = {propvalue}"
            caller.msg(output)
        elif self.propname not in allowed_propnames:
            caller.msg("你只能更改 %s。" %
                              ", ".join(allowed_propnames))
        elif self.propval:
            # 为新属性值分配
            # 在此示例中,属性都是整数...
            intpropval = int(self.propval)
            npc.attributes.add(self.propname, intpropval)
            caller.msg("将 %s 的属性 '%s' 设置为 %s" %
                         (npc.key, self.propname, self.propval))
        else:
            # 设置了属性名,但没有属性值 - 显示当前值
            caller.msg("%s 的属性 %s = %s" %
                         (npc.key, self.propname,
                          npc.attributes.get(self.propname, default="N/A")))

这个命令示例展示了更高级解析的用法,但其余部分主要是错误检查。它在同一房间中搜索给定的 NPC,并检查调用者是否确实具有“编辑”权限,然后才继续。没有适当权限的帐户将无法查看给定 NPC 的属性。对每个游戏来说,这是否适合如此处理由你决定。

像以前一样将此添加到默认命令集中,你应该能够尝试它。

注意:如果你希望玩家使用这个命令来更改 NPC 的某个属性名称(即 key 属性),你需要修改命令,因为 key 不是属性(不能通过 npc.attributes.get 检索,而是通过 npc.key 直接检索)。我们将这作为可选的练习留给你。

让 NPC 做事 - +npc 命令

最后,我们将创建一个命令来指挥我们的 NPC。目前,我们将将此命令限制为仅可由对 NPC 拥有“编辑”权限的人使用。如果任何人可以使用 NPC,则可以进行更改。

由于 NPC 是从我们的角色类型类中继承的,因此可以访问大多数玩家的命令。它没有权限访问基于会话和玩家的命令集(这意味着,除了聊天时不能在频道上发言,但如果你添加了这些命令,他们也可以做到)。因此,+npc 命令很简单:

+npc Anna = say Hello!
Anna 说:“你好!”

再次将其添加到 command.py 模块的末尾:

class CmdNPC(Command):
    """
    控制 NPC

    用法:
        +npc <name> = <命令>

    这使得 NPC 作为其自身执行命令。它将以
    自己的权限和访问执行此操作。
    """
    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):
        "运行命令"
        caller = self.caller
        if not self.cmdname:
            caller.msg("用法:+npc <name> = <command>")
            return
        npc = caller.search(self.name)
        if not npc:
            return
        if not npc.access(caller, "edit"):
            caller.msg("你无法命令此 NPC 做任何事情。")
            return
        # 发送命令
        npc.execute_cmd(self.cmdname)
        caller.msg(f"你告诉 {npc.key} 去做 '{self.cmdname}'。")

请注意,如果你给出错误命令,你将不会看到任何错误消息,因为该错误将返回给 NPC 对象,而不是返回给你。如果你希望玩家看到这些内容,可以将调用者的会话 ID 传递给 execute_cmd 调用,如下所示:

npc.execute_cmd(self.cmdname, sessid=self.caller.sessid)

不过,需要记住的是,这是一种非常简单的控制 NPC 的方法。Evennia 支持完整的操纵。一个帐户(假设“操纵”权限设置正确)可以简单地执行 @ic mynpc 并能够“作为”该 NPC 玩游戏。这实际上与帐户控制其正常角色时发生的情况是相同的。

总结评论

本教程到此为止。看起来文本很多,但你需要编写的代码实际上相对较短。此时你应该拥有游戏的基本框架,并了解到编写游戏涉及的内容。

从这里开始,你可以构建更多的 ChargenRooms 并将其链接到更大的网格。+setpower 命令可以基于或伴随许多其他命令,以获得更复杂的角色生成。

简单的“力量”游戏机制应容易扩展为更成熟和有用的东西,战斗分数原则也是如此。+attack 可以通过针对特定玩家(或 NPC)并自动比较他们的相关属性来生成结果。

如需进一步学习,你可以查看 教程世界。有关更多具体想法,查看 其他教程和提示 以及 Evennia 组件概述