游戏开发论坛

 找回密码
 立即注册
搜索
查看: 10775|回复: 1

游戏《蔚蓝山》教我的编程道理

[复制链接]

8364

主题

8525

帖子

1万

积分

版主

Rank: 7Rank: 7Rank: 7

积分
14833
发表于 2020-8-18 10:49:13 | 显示全部楼层 |阅读模式
如果有这么一款游戏,你操作的角色平均每 20 秒就会死亡一次,正常通关一次,总共需要死掉超过 2000 次。你猜这是一款神作还是垃圾?

《Celeste》(译名:“蔚蓝山”)就是这么一款游戏。在游戏里,你扮演一个名为 Madeline 的女孩,通过跳跃、抓墙、冲刺等动作,去努力登顶一座名为 “Celeste” 的高山。

celete_gameplay_1.jpg
图:《蔚蓝山》游戏画面,它是一款点阵画风 2D 平台动作游戏

正如我在开头说的,这款游戏的难度高到令人发指,玩家平均得死上千次才能通关。但奇怪的是,这款游戏获得的成就似乎和它的难度一样高。在 2018 发售那年,它获得了 TGA “年度游戏”提名并成功拿下了“最佳独立游戏”奖项。截止到 2018 年底,它总共卖出了超过 50 万份。

极低的犯错成本

让《蔚蓝山》大获成功的原因有很多。精妙的关卡设计、出色的动作手感、令人惊艳的游戏配乐,以及剧情里流露出的真诚人文关怀,都是非常关键的因素。但除开这些,我在玩游戏时,还注意到了一个有意思的细节:在游戏里,玩家的犯错成本非常低。

假如你操作跳跃的时机不对,角色掉入坑里死掉了。然后,在 不到 3 秒钟 内, Madeline 就会在房间入口处复活。你可以对自己的打法稍作调整,马上进行下一次尝试。

并非所有游戏都给予了玩家这种快速试错能力。比如在 PS4 游戏《血源诅咒》里,一次死亡可能代表你过去一小时获得的资源全都化为乌有。

注解:
在《血源诅咒》中,玩家死亡后会丧失当前拥有的所有血之回响(一种游戏内资源)。如果要找回它们,你需要从一个又一个怪物堆里穿过,回到你的死亡地点。如果你在路上再次死掉,那么那些血之回响就会全部消失。

所以,在《蔚蓝山》里,游戏设计者给了玩家一种可以 “低成本犯错” 的能力。有了它,我们可以快速从错误中学习,更好的完成挑战。那么,如果用编程来类比,我们在写代码时的犯错成本又如何呢?

编程时的“犯错成本”


假设我在开发一个新闻稿管理系统,系统里目前只有一种用户:“管理员”。但因为需求变更,我现在得给系统加上两个新角色:“编辑”和“主编”。

每类角色能做的事是有区别的:

  • 编辑:可以提交稿件、修改自己的稿件
  • 主编:在编辑的权限上,增加刊登稿件的功能
  • 管理员:可以做任何事以及管理所有人的权限

为了支持不同的角色,我需要改进现有的用户权限体系。首先,我得把和权限控制相关的所有功能点整理出来,然后开始写权限控制相关的代码。

没人能一次写出不出错的代码,所以写代码,其实就是一个在不断重复 “开发” -> “试错” -> “修改” 的过程:

  • 修改后端代码,增加新角色:“主编”
  • 在“主编”相关的功能点,增加权限保护代码片段
  • 保存代码,等待本地服务器重启加载改动 (5-10 秒)
  • 打开浏览器,点击各个功能页面,确认我的改动是否生效 (10 秒以上)
  • 如果测出问题,回到步骤 2,重复整个过程

在很长一段时间里,我在工作时的开发流程就是上面这样。我总是在接到需求后就马上对代码修修改改,然后打开浏览器,点点这里、点点那里,用肉眼观察一切是否正常。

使用这种开发方式,假如我某次写的代码有问题,那么从我每次改完代码,到一直走完步骤 3、4、5,整个过程至少得花费超过 30 秒。

如果你不觉得 30 秒很多,请你想想《蔚蓝山》吧。在《蔚蓝山》里,角色每次死亡到下次重试的时间间隔是不到 3 秒钟,二者相差 10 倍。所以,上面这种开发模式的“犯错成本”太高了。

如何降低“犯错成本”

其实,在开发这类 web API 时,我们完全没有必要傻乎乎的手工用浏览器点来点去。作为功能的开发者,我们可以(而且有义务)利用自动化测试来加速整个试错过程。

很多 web 框架都为这类测试提供了帮助。拿 Django 为例,你可以使用 django.test.Client 来轻松编写这类测试:

  1. # 以下代码片段来自 Django 官方文档
  2. import unittest
  3. from django.test import Client

  4. class SimpleTest(unittest.TestCase):
  5.     def test_details(self):
  6.         client = Client()
  7.         response = client.get('/customer/details/')
  8.         # 测试某次请求是否返回了 200 状态码
  9.         self.assertEqual(response.status_code, 200)
复制代码

对于前面的需求,我们可以直接编写下面这样的单元测试代码。
  1. # 针对不同的角色定义不同的单元测试类

  2. class RoleEditorTestCases(TestCase):
  3.     """编辑角色的测试类
  4.     """

  5.     def test_create_post(self):
  6.         # 编辑角色可以正常调用创建帖子接口
  7.         response = self.request_post('/posts/', {'title': 'foo'}, current_user=self.user)
  8.         assert response.status_code == 201
  9.         assert isinstance(response.data, dict)

  10.     def test_create_admin(self):
  11.         # 编辑应该无权调用创建管理员接口
  12.         response = self.request_post('/admins/', {'user_id': 100}, current_user=self.user)
  13.         assert response.status_code == 403


  14. class RoleAdminTestCases(TestCase):
  15.     """管理员角色的测试类
  16.     """

  17.     def test_create_admin(self):
  18.         # 管理员可以调用创建管理员接口
  19.         response = self.request_post('/admins/', {'user_id': 100}, current_user=self.user)
  20.         assert response.status_code == 201
复制代码

有了这些单元测试后,整个试错流程可以得到极大改进。每当我改完代码后,只要运行 pytest 命令跑一遍相关的单元测试,就能知道改动是否奏效了。

  1. ❯ pytest
  2. ======== test session starts ========
  3. platform darwin -- Python 3.8.1, pytest-5.3.5
  4. collected 5 items
  5. tests/api/test_permissions.py .....
  6. ======== 5 passed in 0.72s ========
复制代码

不需要等待开发服务器加载变更、不需要打开浏览器点这点那。一切试错任务都可以在几秒钟之内完成。

编写测试其实也是 DRY

我在前面说过,在游戏《蔚蓝山》里,如果角色死掉了,那么她马上会从当前这个 房间入口处 重生。让我们设想一下,假如游戏没有采用这种设计:在新机制下,角色每次死亡后,玩家都得回到本章开始的地方,重新挑战一遍好几十个已经通过的房间。那会怎么样?估计很多人会气的把手柄摔地上。

但是,依赖人工测试的开发流程,其实就非常接近于让人摔手柄的设计。

拿用户权限功能来说,因为这个功能非常关键,所以我每次做出大改动后,都需要重复验证一下每个功能点在各角色下的表现是否正常。假如系统里一共有 20 个功能点需要和权限挂钩,那么 20 * 3 个角色,就是 60 个需要测试的点。

即便我有三头六臂,每个功能点只花 20 秒测试,整套东西测下来也需要 20 分钟。

但是,如果你已经为这些场景写好了单元测试,那么事情就变得简单多了。每次做了改动之后,你只需要重新执行一遍单元测试,就能把所有场景都验证一次。

Django 框架有一条设计哲学叫 “Don't repeat yourself (DRY)” - “不要重复你自己”。多数情况下,我们说 DRY 是指不要写重复代码。但我认为“不要重复手工测试已经测过的东西”其实也可以算是 DRY 的一种。

所以,每当你手动测试一次功能时,其实就是在重复你自己。既然如此,何不将它写成一个单元测试呢?


“所以,就是在劝我写单元测试?”


是的,我就是在劝你写单元测试。作为对比,让我们看看利用单元测试的开发流程是什么样的:

  • 修改后端代码,增加新角色:“主编”
  • 在“主编”相关的功能点,增加权限保护代码片段
  • 编写与功能代码相关的单元测试代码,与 2 同步进行
  • 执行单元测试,如果失败,从 2 开始调整代码,重复整个过程 (几秒钟)

通过把测试行为自动化,我们可以大大减少整个开发过程的试错成本。事实上,自从若干年前养成了写单元测试的习惯,我就一直坚持至今。那么,我到底是因为什么在写单元测试呢?

  • 单元测试让我的代码 Bug 更少?
  • 单元测试帮助我写出扩展性更强的代码?
  • 单元测试让我在重构时更不容易出错?
以上可能都是。但现在,我可以往上面的列表里再加上一点:使用单元测试来开发的过程,有一种流畅感,失败后就马上重试,一切就犹如在操作 Madeline 登顶那座蔚蓝色的山。

作者:Piglei
来源:Piglei
地址:https://www.zlovezl.cn/articles/what-celeste-teaches-me-about-programming/
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-3-29 03:40

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表