大制作来临了!
这回的游戏涉及的内容比以前多,除了控制小飞船打来打去之外,动画、音效、UI 等也都引入了游戏中。但每一部分的实现都可以通过之前尝试的小功能叠加实现。简单地了解游戏图像和声音到底怎么运作后,并无特别的困难。只是这一次我学着一个模块一个模块渐次开发和测试,一个功能调通无误,再进行下一个。
反而是在游戏设计方面,制作这个游戏的过程给我带来很多思考。在这个游戏中可供调整的变量太多了:飞船需要推进和旋转;但推进是给飞船一个向前的加速度,而飞船本身还会有向着其他方向的速度。宇宙空间中微小的摩擦力、和陨石撞击后受到的力,都要考虑并且编入游戏中。
这时你会发现,同样的一些参数,经过调整会让整个游戏变得彻底不同。这艘飞船到底是笨重、转向慢、射速慢、射程远的战列舰,还是轻盈、转向快、射速快、射程近的战斗机?你要躲闪的是从一个方向袭来的流星群(陨石都从一边来,而且向一个方向阻力特别大),还是四面八方出现的乱石?每一种选择,好像都挺好玩的……
到这时我才了解到一个游戏设计者脑中「指挥意图」清晰的重要性。你到底要做一个什么样的游戏,给玩家带来什么样的情感?只有一个大概的「我要爽」是不够的:究竟是控制巨大战舰缓慢机动将将擦过一块流星的那种屏气凝神的爽,还是控制战斗机高速穿梭在流星群中那种险象环生的爽?有时候自己也会犹豫。只有记住一开始你要提供的是怎么样的情感,并且在全程中反复回看,才不会偏离目标。
一个人制作尚且如此,当需要团队合作的时候,若不把一个确定的思路贯彻到底,怎么行呢?

第八周:2048

啊,HTML 小游戏。在这个星期,2048 游戏突然流行了起来。于是我也跟风复刻了一个。看似简单的游戏,真的要做出来,对于新手来说还是挺费脑筋的。
第一个问题就是,这个网格怎么做呢?我采用的转化方法是使用一个二维的list。看起来就是:
[[0, 1, 2, 3]
[0, 1, 2, 3]
[0, 1, 2, 3]
[0, 1, 2, 3]]
这样一来,如果我要定位到第三(2)行第一(0)个格子,我就读取这个 list 中的 List[2][0] 即可。这样一来看起来颇为直观,又能解决问题。
接下来又有好几个问题需要一一解决。首先,当你按下一个方向键以后,所有行(列)的数字都会向着那个方向合并。这件事怎么办呢?
首先我单独写了一个 merge() 方法。只要传来一个 list,就逐个 iterate 并将合并后的值返回去。然后在主要 Class 中间的移动方法 move() 中规定,向哪个方向移动,就以那个方向的四个格子为排头建立四个 list,传去 merge() 那边再替换回来。这样一来这个游戏的核心规则就实现完成,剩下的边边角角多测试修缮即可。当测试成功的那一刻真是有一种爆棚的成就感——很少有解谜游戏的谜题能这样让你研究琢磨几个小时。
当你把游戏的每个部分分入不同的 Class 和方法中后,可以感觉到效率提升不少。例如你在制作模块 B ,此时要用到模块 A 中的功能,你可以完全不管模块 A 怎么实现的,只要把指定的数据传进去,等着它传出结果来就好了。

第九周:Cookie Clicker(点击-放置游戏)

这是个挺有病(误)的游戏。你只要点这块饼干就可以加饼干数,饼干可以买帮你加饼干的道具,越高级的道具加饼干越快,子子孙孙无穷匮也。听说最近这种放置类游戏在一些小圈子里挺流行的……
游戏本身的设计相对简单。加饼干数,加加饼干速度,获取各种升级和冷却的时间,购买道具等等,并不复杂。
但我们不想自己玩,我们想要电脑自动玩,算出最快速的策略,看看到底能获得多少饼干。
为了这样,我们专门做了一个叫 simulator_clicker() 的方法,它会根据输入的策略,在合适的时间购买固定道具;而每个策略都可以另外定义。这样一来,这个方法里引用的方法又引用了别的方法,复杂性上了一个台阶。
至于「策略」,就进入了 AI 的范畴。此时我们虽然只能使用最基本的条件判断,但反复计算应该让 AI 怎样动作,还是挺有挑战性的。只不过,发现让 AI 采用「纯随机策略」乱买道具出来的结果比你辛苦计算的结果还好,就有点蛋疼……

第十周:Yahtzee

这是个投骰的游戏,同样涉及自己的「手」概念。大家自己玩一玩这个就明白了。 这一次制作的只涉及分数表上半区的部分。
Yahtzee游戏打印出的策略
为这个游戏编写 AI 最有趣的地方是涉及到了概率和期望。我手上还有这么些骰子,那么接下来可能出现的所有手我都要算一遍,列成一个树,然后找到概率最大的一种。我把列出所有可能手、为一手计算期望值、为一手计算分数和 AI 策略分别写在 4 个方法里。

第十一周:僵尸末日

一群人类(绿点)被僵尸(红点)包围在破败废墟中的场景。请自行脑补。
啊,僵尸。也不知道谁规定的,僵尸及其变种的怪物成了无数影视游戏中人类可以毫无道德顾虑地击杀的游戏怪物。
这个游戏的画面如上图所示:
黑色是障碍物。可以理解为房子、篱笆、烂掉的车什么的;任何单位不能通过。
红色是僵尸。它们可以向上下左右四个方向移动,会自动前往最近的人类。
绿色是人类。他们可以向8个方向移动,会自动远离僵尸。请不要吐槽为什么颜色好像应该反过来。
紫色是感染者。被僵尸抓到的人类就会这样,不会动。可以理解为啃翻在地上,过一会儿就要变成僵尸起来。
所有的格子都是可以由玩家自行布置的。因此这是个乐趣在于 YY 的沙盒游戏。
点击 "humans flee" 按钮则人类移动一回合,点击 "zombies stalk" 按钮则僵尸移动一回合。它们采取的寻路策略都是广度优先搜索。游戏不会结束,你可以在这个沙盒中给自己安排胜利条件。布置各种各样的场面看着它们行动,也还能支撑个半小时的乐趣,是到目前为止制作的可玩性最强的游戏……
同样的,这个游戏也是一个具有充分扩展性的游戏。感染者会不会转化成僵尸?人类能不能拿到武器反击僵尸?僵尸中间会不会有特殊感染者,能够范围攻击、远程拉住人类、能跳来跳去或者会爆炸?玩家这个上帝的力量有多大?跳出「玩家扮演游戏中的某个角色」的框框,会发现沙盒类游戏的乐趣所在。

第十二周:猜词游戏

猜词游戏就是这样:你指定一个词,电脑会搜索词库,将这个词的字母能组成的所有词以星号遮住,你逐个尝试将他们列出来的游戏。
这个游戏中第一次涉及到读取文件。
为了成功的读取到输入的词汇并且匹配所有可能组成的词,我们需要使用一个merge_sort()方法来将一个打乱的列表变成有序的。这时我第一次接触到「递归(recursion)」。
要理解递归,首先要理解递归(误)。也就是说这个方法自己不断引用自己。看起来就像
这样。
设计一个递归方法前,首先要明确停止递归的条件(base case)。在这个基础上推算每一步应该怎么办。可以拿一个简单的例子在纸上演示,无误后写出来看看效果。
我的设想中,当给出一个 list 后,首先应当将其分成两半,当字母的个数小于等于 1 就应该停止递归。
最后写成的方法看起来像这样:
对我来说递归还是挺复杂的。一个简单的递归就要想很久,不过想清楚了之后的效果还是不错的。不少复杂的游戏设计中都会出现类似的规则。
当然,你也可以不使用递归,而是设定一些条件重复地调用一个方法。但那样的话代码量就变得很大,执行效率可能也会变慢。你是要牺牲易理解性换取效率,还是牺牲效率换取易理解性呢?很多时候玩家也会试图来理解你游戏的内在逻辑,能不能让他们轻松办到呢?

第十三周:九宫格(tic-tac-toe)

九宫格,世界各地的小朋友可能都玩过的经典游戏。放大到5连就是五子棋。
为这个游戏编写电脑对手采用的是所谓的「蒙特卡罗方法」。也就是从目前这一步开始,推算出每一个可能的游戏结果。胜则加分,负则扣分,和则不加不减;最后选定分数最高的一步落子。这种算法在棋盘复杂的的情况下很难实用,但应付九宫格是绰绰有余。
然后,为了测试这个对手到底强不强,我把游戏规则反了一下变成「逆九宫格」。也就是谁先连到 3 个就算输。这种模式下,没有下中间那个位置的不败手,更能看出电脑的实力。第一盘我还没反应过来,结果输掉了。
逆九宫格:先达成三个一线者负
到这里,我编写的 AI 就摆脱了特别直觉的 if-else 或者广度优先搜索规则,进入了一个发挥其强大计算力的时代。假如把棋盘扩大几倍,胜利条件相应放大,人类就很难战胜电脑了。

第十四周:数字推盘游戏(n-Puzzle)

一开始的游戏是15个方格,数字错乱了,需要你来把它们移动回正确的位置。有一种改进型就是拼图,首先你要找出图片的正确顺序,然后还要推回正确位置。
游戏本身的规则不难,但要做一个自动解 Puzzle 的AI 就有点意思了,根据反复试玩观察,一个盘面可以分为几个区域,各自有固定解法:
第二行以下第一列右侧的的
第二行以下最左边一列的
第一行的
第二行的
最末阶段左上角的4个
大家可以观察动画里面解开的过程,研究一下在这些区域我让电脑怎么动作的……
一个个模块分别编写和测试,在内部再分情况讨论,真是件体力活!但只要测试无误,无论这个 puzzle 扩展到多大,解开它也就是时间问题。以后谁再拿这种东西为难你,只要把题目输入进去,就能看着电脑瞬间自动解开并且给你一个操作顺序了。

结语

在整个的 14 周过程中间,我从能写简单的几十行程序,逐渐进步到能完成较复杂的600行程序(不含UI部分)。在此过程中,我逐步学到和应用的知识有:
python 基础语法
变量
list
方法(function)
类(class)
各种算法
递归
编程的 style 要求
等等,族繁不及备载。这些知识以及应用的方法有可能忘却,但在此过程中有着更多东西是令我体会深刻,很难忘记的:
将「手感好」、「手感不好」等感觉分析成多个具体部分,进行调整。
评估各种实现某个功能的手段,依据其复杂程度或者实现效率。
分步计划并实现你期望的功能,最后组合成完整的游戏。
这些是在布鲁姆教育目标分类法被列为比较高级的认知类型。