Lisp Game Jam 2022

出于对游戏的爱,参加game jam的冲动常伴我左右;出于对“建筑美”的爱,使用Lisp编程的冲动同样常伴我左右当我得知这世界上有以Lisp为主题的game jam时,我没想太多就参加了。

目录

到底用那款Lisp

Lisp是一系列语言的统称,想要做游戏的话得先确定到底使用哪一种Lisp方言。我其实没怎么使用过Lisp,只浅尝过RacketEmacs Lisp(Elisp。Elisp依托于一个很小众的编辑器Emacs,如果使用Elisp写游戏的话直接就把大部分玩家拒之门外了。因此我最初的选择是使用Racket。

由于Pollen的原因,我对Racket印象很好。可是经过一番搜索,发现使用Racket的游戏引擎都像是玩具项目——虽然我要做的也是玩具游戏,但使用别人的玩具项目实现自己的想法估计会很痛苦。所以我又准备放弃Racket。

随后我想起了Fennel。Fennel是个Source-to-source编译器:它接收Lisp代码,吐出Lua代码。因此,可以把Fennel和以Lua编程的著名独立游戏引擎LÖVE合起来用。我之前没有尝试LÖVE的原因是不会Lua,但这几个月Lua写了好多Pandoc filter,已经没有理由拒绝LÖVE了。另外,Fennel接受的代码与其说是Lisp,不如说是披着括号的Lua——这意味着它对于使用过Lua的人来说易于上手。所以,在想到Fennel + LÖVE的组合后,我就决定不再变更工具方面的计划了。

超市里的<span
class="autospace"></span>Fennel

做什么样的游戏

在《高达》系列里经常可以看到驾驶员用精神力控制好几门浮在空中的激光炮(浮游炮)上下翻飞从四面八方以各种角度同时攻击对手。Fennel和浮游炮的英文“Funnel”很像,所以我最开始准备做一款浮游炮模拟器。具体来说,就是升级版的《Asteroids:原版《Asteroids》是控制一艘飞船射击陨石,我想做的是同时控制好几艘飞船射击陨石。我的第一款游戏就是跟着油管上的教程做的翻版《Asteroids,虽然它的代码丢失了,但重写一遍应该难度不大。

之所以要强调同时操控多艘飞船,除去向《高达》系列致敬的原因,还因为樱井政博可以一个人玩双人游戏——他玩的双人游戏是为双人设计的,我则想探索为单人设计的多人游戏会不会好玩。在最终的游戏中看不到任何浮游炮的痕迹,但是“为单人设计的多人游戏”的思想得到了贯彻。另外我的答案是:会好玩。你也可以去体验一下会不会好玩。

继续说回游戏机制的变迁。多艘飞船”的想法很快得到了否定——想法是吃饭时想出的,否定是吃完饭否定的——因为玩家完全可以用一艘飞船击毁所有陨石。如果不强迫玩家使用多艘飞船的话就不能解答我的问题,而且做出的游戏也就等同于原版《Asteroids

强迫控制多艘飞船的方法很简单:把屏幕分割成N份,每份是单独的世界这是可行的方法,然而灵感马上又更进一步:都分屏了,为什么不同屏幕还要玩同样的游戏?犹记得小时候玩过的“左手捶腿,右手搓腿”游戏:刚开始时两只手很容易干同样的事——换句话说,将屏幕一分为二后,同时玩不同的小游戏比相同的小游戏更有挑战性。

就这样决定了!左边屏幕玩《Asteroids,右边屏幕玩别的游戏。可是,右边具体是什么游戏呢?最好是和左边越不一样越好,要一点动作元素都没有的那种——我还真写过这样的游戏:魔鬼计算。左右两边各玩各的可没意思,所以我设计了联动机制:如果计算题答对了,飞船可以多射出一道激光;反之则少一道。

直到第三天(这次game jam一共十天)我才发现用LÖVE很难实现《Asteroids:陨石是不规则的多边形,LÖVE自带的碰撞检测很繁琐——甚至官方wiki都不推荐用,而支持检测多边形的第三方库又停止维护了。回想一下,貌似我之前使用的GameMaker Studio自带易用的碰撞检测功能,所以没遇到相关问题。总之,为了保证规定时间内能做完游戏,我决定把左屏换成其他游戏。

LÖVE社区里有个口碑不错的矩形碰撞检测库bump.lua,所以左屏的游戏最好是基于方格的游戏:比如平台跳跃类。我感觉整个世界在衰退,所以决定做一个世界在向下滚动,而主角为了求生必须向上跳的游戏——可以说是《是男人就下一百层》的颠倒版。如果右边的计算题答对了,主角多一次跳跃机会;反之少一次。下图就是最终游戏。

游戏截图

另外,我还想过在左右两屏上面再加一个贪吃蛇:如果蛇咬到自己或者碰壁,左边屏幕里向下滚动的世界直接崩塌——游戏结束。三个屏幕三款互相影响的游戏,想想就刺激。不过最后因为时间原因没把贪吃蛇加上来。

怎么做

既然游戏机制已经拍板,接下来只需要做出来即可。话说得容易,实际制作却因为不熟悉FennelLÖVE的原因写一行改十行。这导致进度远远落后于预期,最后砍了很多锦上添花的功能。

Parinfer

据说写Lisp最好的方式据说是开一个REPL以交互的方式写,不过我还是习惯用传统的编辑器写代码。写Lisp最好的编辑器是Emacs——而它不是我的菜,我更倾向于功能弱一些但启动和输入延迟都更低的Vim。

最开始我是什么插件也不装直接写Fennel。写的时候没问题,但改的时候经常要数括才能保证改完的代码可以工作。这我可遭不住,然后想起几年前热衷于Emacs时听说的Parinfer插件:根据输入时的缩进等条件推导出哪里该加括号哪里该删括号。装了Vim后,80%的时间不用担心括号的问题,15%的时间可以手动快速解决括号问题,5%的时间要关掉这个插件才能输入我想要的代码。算是利大于弊吧。

后来在网上看到有人为VimLisp支持写了长篇大论,不过没有提Parinfer,很怪。

魔鬼计算

相比之前JS的版本,简化了出题算法。新的算法只需要随机出09a09-ab,即可生成个位数答案的加法式或减法式。因为a+b得到的c一定是个位数,c-b得到的a也一定是个位数。

我在写这个模块时用到了tick,把“每个更新循环里累加时间,检查是否到了某个预设时间从而决定是否执行某段代码”的逻辑换成了两个函数调用。感觉十分舒服。

平台跳跃

让主角没有bug地动起来

主角的移动框架(各种加速度根据按键改变主角X、Y方向的分速度)都是从bump.lua的示例代码里学来的。

虽然有示例,但实现起来并不一帆风顺。最开始主角只要站在平台上就会急剧地上下抽搐。修复抽搐后,又发现主角站立的平台越高,下落时速度越快。原因是站立在平台之上时没有清零Y方向上的速度:重力加速度一直在累加速度。我的主角出生点在低谷,跳到高处需要时间。所以高度越高,时间越长,累加的下落速度就越快。

最后一个bug是在最后一天写好平台下落的逻辑后出现的。它的表现形式是在头上平台下落时向上跳,有一定几率能直接穿过平台站上去。

这个bug的原因很简单:下落的平台没有做碰撞检测。如果下落时平台撞到主角头,它会直接穿过主角头卡到主角身体上。主角嵌在平台里是bump.lua没有预料的情况,所以有时会出现问题。由于时间关系我打算避开碰撞检测的代码,直接在移动平台的同时把主角下降相同距离。然而bug仍然存在,所以我又让平台下落速度变慢、平台间距变大来减少这个bug出现的频率。效果确实立竿见影,但这让游戏变得无趣起来。后来在上厕所时突然想起来我在下移主角时只移动了绘制主角图形的位置,没有移主角在bump.lua的世界里的位置。回到电脑前加上一行代码就修复了这个粗心bug。

虽然bug修复了,但是我忘了恢复为减轻bug而调慢的速度。结果有人留言游戏太简单,因为下落速度很慢。失误啊!

动是没有问题了,但手感是真的不行。具体哪里不行我也说不上来,也因此不知道怎么修改,这是我中途灰心了几天的重要原因。在最后加上了离开平台后0.25秒内还能起跳的“coyote time,希望能增强手感。

无限下降平台

最初我想做自动生成的平台:大致思路是设定起始平台后用玩家的移动参数计算可以放下一块平台的位置,如此循环。由于时间关系,我使用了固定的平台位置。

最开始想偏了,以为要使用队列模拟某层平台从屏幕下方消失,又在屏幕上方出现的效果。但其实只要在遍历平台数组时加个偏移量就好了。所以这个滚动功能的本质就是一个加法和一个取模。

好玩的是我使用SpriteBatch画平台时出现了下图中的glitch。每次移动时清空SpriteBatch,调整完坐标后再挨个加回去就解决了。

glitch

音乐和音效

音乐和音效是最后才加进去的。不得不说,加入它们的效果立竿见影:有了背景音乐和跑跳的音效后整个游戏马上显得活了起来。后来试玩其他没有音效的参赛作品时,更加深刻地认识到了音效之重要。

实装虽晚,但其实第一天我就在寻找合适的声音了。当时正在写魔鬼计算,所以想找答题正确和错误的提示音。正确的“汀”声很好找,就是钱币碰撞的音效。但错误的“嘟”却怎么也找不到,于是就把找音效的事往后拖了。

最后一天准备加入跳跃和落地音效时下载了一个很大的音效包。实际听过以后,发现拾取道具和爆炸的音效作正误提示音合适得很。所有音效就这么找齐了。

音乐方面使用了搜索时排在第一位的音乐。一开始不想用它,因为害怕用得人太多导致玩家听烦了。可是它的最后一首——用于结尾的音乐正是我想要的:舒缓好听的chiptune。从评价上看,我选这首曲子完全正确:大家很喜欢背景音乐。只是我不知道该做何感受,因为我只是选中了它,并没有创造它。

问答

问:你享受这次game jam吗?做出的游戏满意么?

答:不享受,也不满意。如前所述,我因为调教不好角色移动的手感而一度终止制作,最后只是为了结束它才完成并提交了游戏。虽然评比时别的开发者给我的评论都很正面,让我很开心。但做游戏确实远比玩游戏难受:这是一个充满了妥协的过程。所以我并不享受这次game jam。最后的作品只是玩过魔鬼计算的人很少,所以看起来比较新颖罢了。而且评分也不高。

问:移动手感有什么难调的,照搬现实不就得了?

答:游戏中的移动有悖现实,比如按键时间长就能跳得高、离开地面也可以跳等等。游戏要模拟的移动是玩家脑中想象的移动,虽然有些文章讲了需要注意的点。但是“纸上得来终觉浅,绝知此事要躬行,真正制作时除了慢慢调没别的方法。我就是这么给调烦了,歇了两天。

更新:突然发现樱井政博发布了讲解游戏中跳跃物理的视频。他提到了在《星之卡比 超级豪华版》中发明并且在《任天堂明星大乱斗 特别版》再次使用的跳跃机制:起跳时上升速度快,然后快速减小上升速度。樱井声称这样可以帮助反应慢的玩家操作,然后紧接着又提到了:这会增加bug出现的风险。是了,精致的跳跃要引入更多的参数和代码。如果加入了某项改进,作者调试起来会累,但玩家可能注意不到;舍弃某项改进,作者舒服了,玩家却会感到不对劲。樱井做了五部《任天堂明星大乱斗,每部跳跃机制都不相同——可见游戏中的跳跃有多令设计者纠结。

卡比跳跃示意图

问:图案是你自己画的吗?

答:不是,我使用了网上的免费资源。这次制作我有意使用了大量以知识共享0许可协议(简称CC0,是放弃著作权的许可)进行许可的素材。这样做的目的是想测试下独立游戏社区繁荣到了何种程度。结论是:已经繁荣到可以提供一款玩具游戏所需的所有素材了。

问:有多少人参加呢?提交率是多少?

答:一共43人,最后只提交了14个作品,其中有一个作品是三个人合作的。提交率有16÷43≈37%——我也不清楚这个提交率算高算低。

问:你对Fennel + LÖVE的组合满意吗?

答:其实不只是它俩,我还用到了lume, tick, anim8, bump.lua这些库。我对它们整体的组合感到满意,因为它们的功能都很少,所以不用学太多东西就可以开始使用。之后如果有想法做新游戏,我还会用这一套。

问:其他人使用了什么Lisp呢?

答:两个用Racket的;两个用Guile的;三个Common Lisp,剩下的七个全部使用Fennel。Fennel之所以如此受欢迎,简单易学肯定是原因之一,但我觉得与它捆绑的引擎(LÖVE、TIC-80)良好的跨平台特性也是重要原因。在评比阶段有三款其他语言写的游戏,我是死活也没法玩。做出的游戏没有人玩是很打击作者的,因此从开始制作前就得考虑分发的问题。

问:Lisp在编写游戏时有什么独到之处么?

答:据说Lisp可以在游戏运行时增删改带你,且不同于热重载,但我是门外汉所以没有用这方法。Fennel的维护者记录了如何使用Fennel动态修改LÖVE的代码,但我因为对Lua并不是很懂所以没有用。所以对我来说,Lisp没有任何独到之处——也许下次会试试所谓的interactive development吧。

问:打分环节是如何进行的?

答:分别在娱乐性、感官和创意上给出评分。打分与否完全自愿,我一开始懒得打分。但其他人玩了我的游戏并留下好评,我也没法不去试试他们的游戏。另外itch.io会调整评分不够多的游戏的分数,这一点可以理解,但调整公式过于粗暴,相当于惩罚。所以我不喜欢这个功能。

问:有没有印象深刻的其他参赛游戏?

答:Frozen Horizon》的UI背景色和音效都很像纸,有一种捧着报纸冒险的感觉。而且它应该是本次game jam中最完善的作品。ctga》是出色的轻度解密游戏,音效很可爱,唯一的缺点是关卡太少玩不够。Hojo》是个文本游戏引擎的demo,那个引擎用图的节点和边来整理事件。我觉得这点挺有启发性,因为我是手工编排事件的。Rhombihexadeltille game》和《Marad》还有《Metabolize!》看起来是有独特机制的游戏,可惜我没办法运行它们。

问:说了半天,在哪能玩到你说的游戏?

答:Game jam的地址是:https://itch.io/jam/lisp-game-jam-2022