Tech, Bio & Photography

Python TRPG制作实录 - EP.1 框架

EDIT: 删减标题,原来的太长了。

试试在这里转载一个我自己的小坑,看看开学前能不能把ep2憋出来.・゚゚・(/ω\)・゚゚・.

话说这个大分类写成“计算器与技术”而不是“计算机与技术”真的好吗?


我们这次将开始利用Python制作一款简易的TRPG,原载地址在百度贴吧,名字在很久以前已经想好——《Continuous Infinity》,这个名字出自刚立项时的想法——最终实现自动生成无限多的关卡。

为了使本文简洁明了,这个系列就不废话了 (希望如此)

Part I. 规划

从总体上看整个游戏流程,应该可以分成以下几步:

  1. 主页:进行游戏配置,选择进入或退出游戏;
  2. 主体:游戏主体部分,项目的核心
    1. 接受玩家操作,产生效果;
    2. 检查是否达到退出条件;
    3. 程序作出操作,产生效果;
    4. 检查是否达到退出条件;
  3. 退出游戏或回到1.。

游戏的主要形式即是玩家操纵角色与其他角色 (程序控制的敌人) 进行对战,互相攻击对方,直至一方血量归零,触发事件,执行后续操作——玩家死亡结算、敌方给出新角色、进入下一关等。由此可以归纳出游戏时需要定义的“具体事物”有角色 (mob) 、物品 (item) (包括装备、道具) 、技能 (skill) 等,稍抽象一点的是关卡 (level) (需要定义每个关卡的敌方角色的各种信息) 。

Part II. 源码结构

继而我们可以给出如下的源码结构作为参考:

.
│  run.py
│
├─game
│  │  system.py
│  │
│  ├─data
│  │  ├─items
│  │  ├─levels
│  │  ├─mobs
│  │  └─skills
│  ├─generator
│  │      levels.py
│  │
│  ├─loader
│  │      items.py
│  │      levels.py
│  │      mobs.py
│  │      skills.py
│  │
│  └─locales
└─test
        .keep

其中run.py是游戏启动的脚本;test文件夹存放单元测试的相关内容;game文件夹存放本游戏的主体内容,包括游戏的绝大多数逻辑、上文所说的具体事物的定义和设计等;game/data文件夹存放的即是所谓的“设计”;game/generator文件夹存放游戏的生成器,用以自定义一系列具体事物以及其自动生成算法;game/loader文件夹存放游戏的加载器,用以读取game/data定义的内容;game/locales文件夹存放游戏的多语言文件 (锦上添花而已,不重要)

Part III. 具体事物的信息存储和对战系统逻辑

前文提到了对战系统的主要形式是回合制,而较为精细的逻辑则需要涉及到一些具体事物的“使用——生效”机制。当开始某个角色 (不论是玩家还是敌方角色) 的回合时,这个角色可以选择出招 (skill) ,也可以选择使用道具 (item) 增益己方或损害对方,亦可以临时装备一些物品 (item),或是执行系统级操作——逃跑 (假设未来某天加入游戏中) 、投降 (同上) 、退出当前对局等。不过其中需要伤点脑子的是如何有效地存储一些具体事物的各种信息,以及如何让具体事物在对战时产生效果。

在早期的设计过程中,笔者写出了如下的代码来存储角色的信息:

class Mob(object): # Player and enemy
    def __init__(self, type, name, lvl, exp, hp, pp, hpm, ppm, atk, dfd, spd, pack, coin, skill, weapon, helmet, chestplate, leggings, boots):
        self.type = type                # 类型(职业)
        self.name = name                # 名字
        self.lvl = lvl                  # 等级
        self.exp = exp                  # 经验值
        self.hp = hp                    # 血量
        self.pp = pp                    # 技能点
        self.hpm = hpm                  # 血量上限
        self.ppm = ppm                  # 技能点上限
        self.atk = atk                  # 攻击力
        self.dfd = dfd                  # 防御力
        self.spd = spd                  # 迅捷度
        self.pack = pack                # 背包列表
        self.coin = coin                # 金钱
        self.skill = skill              # 技能列表
        self.weapon = weapon            # 武器
        self.helmet = helmet            # 头盔
        self.chestplate = chestplate    # 胸甲
        self.leggings = leggings        # 腿甲
        self.boots = boots              # 靴子

原来的代码中其实连中文注释都没有。但即便按照PEP 8进行格式化,加上了详细的注释,又添上了“docstring”,这段代码依然冗长,而且在定义一个角色的时候需要在参数中加上大量的None,十分不便。这是笔者联想到requests模块的get函数,get函数支持添加许多参数,但模块作者采用了一个较为聪明的方法:

In [2]: requests.get?
Signature: requests.get(url, params=None, **kwargs)
Docstring:
Sends a GET request.

:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary, list of tuples or bytes to send
    in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response

因而我们也可以让定义者显式地传入一个角色的个性化参数,如果没有给出就保持默认值,这样可以极好地便利游戏数据设计者。同时这样的设计可以在形式上将一个角色的所有信息平权,作为“字典型”参数传入时,没有明显的次序之分,总体上是优雅和谐的。

而对于产生效果的方法,一个问题是如何选择目标角色。一项技能和一个物品发挥作用,其目标可以是使用者自身,也可以是己方或对方所有角色,甚至是让使用者自选角色乃至随机角色。为了适应多种需求,我们可以借鉴游戏《Minecraft》上“目标选择器”的机制——利用简单的语法形成的字符串来选择目标。而第二个问题是施加效果,如果便利游戏数据设计者,可以封死自定义复杂函数的通道,只提供一个简单的列表保证一些必要数据 (譬如血量、技能点、装备、物品数量) 的增减;而如果提供充分的自由度,我们可以允许游戏数据设计者向loader提供一个含有指定接口的函数,loader只需利用提供的接口执行更复杂的操作即可。如果为了“双赢”,可以同时实现这两种方式,当然这样的话最累的就是游戏设计者 (其实就是笔者) 。

【完】

2 Likes

谢谢提醒,主题标题已修复。