这是我独立开发的第三款游戏,也是我用pico-8开发的第二款游戏。
上次开发完Bullet Jam后,我对于用pico-8这种提供基础功能库的引擎开发游戏起了非常浓厚的兴趣,于是打算立刻用它开发一款自己一直以来都想做的游戏——横版格斗。
因为我第一次学习游戏开发的书是那本《Flash mx 2004 游戏设计与制作》,里面教了一款名为《闪客快打》的游戏,我后来又玩到了它的续作《闪客快打2》,一下子对这种游戏印象深刻,一直想自己做一款类似玩法的游戏。
另外这次我也想要提高游戏的规模和完成度,做一个有有限流程的游戏。
以及因为pico-8是一个只提供基础接口的引擎,所以我很想在这个基础上编写一套2d格斗游戏的游戏框架,这相当于重复造一遍很多游戏引擎的轮子,虽然对游戏开发而言不是一个高效的选择,但却是我个人的兴趣所在。
还有一个动机,我这次打算试着在社交网络上同步自己的开发日志,试着体验一下游戏的网络宣传。
比起前两个项目,我这次花了不少时间来做前期规划,包括渲染逻辑的初步搭建,玩法和一些关键的画面诉求,现在回头看,发现有些规划准确地实现了,有些则完全抛弃了(比如对话系统),还有一些则是做的过程中的意外收获。
另外还做了游戏的主循环的逻辑和一些数据规划,因为格斗游戏里,对于2d角色动画的控制是核心,很多设计打击感的前摇、硬直以及攻击判定和被击时间窗口都需要对动画帧信息做处理,所以我专门花了不少心思来设计帧动画的数据结构和状态机的逻辑(但其实后面状态机的逻辑是重构了的)
首先就是状态机和动画系统,在做了一些测试后,我初步搭建了player角色的状态机,绘制了它的站立动画,从而也初步搭建了动画系统。
对了,pico-8用的是lua语言,这是一个本身不面向对象的脚本语言,但可以通过一些方式来实现面向对象的编码。于是我在写动画系统和状态机时,在网上找了一些lua实现面向对象的资料,却意外地发现lua非面向对象的特性反而导致它在实现面向对象编码时可以整一些歪招,反而可以提高类继承的自由度(其实只是节省了一些代码量,但这对于有代码量限制的pico-8来说却很关键)
初步实现了player的站立和奔跑之后,我继续绘制了player的三段攻击动画,并且实装进了游戏里。然后发布了第一个开发日志。(pico-8自带的录制gif功能真的很适合输出社交媒体素材)
然后是player的翻滚动作,这里我第一次收到了社交媒体上网友的反馈,为这个动作加上了烟雾效果。
制作烟雾效果时我第一次遇到了需要判断在哪一帧出现烟雾的问题,这个问题很关键,因为之后当我需要调试手感和制作敌人的各种动作时间点时都会需要能快速地查找动画的信息,于是我在图二中加入了一个debug系统,这就是我在规划外意外搭建了第一个系统,这个系统结合pico8的录屏功能,可以准确地逐帧回看游戏记录。
再然后,我开始继续绘制第一个敌人,而此时,我原来写的动画系统就开始出现问题。
情况是这样的,我原本写的动画系统是把动画里的序列帧信息、帧长度、速度等信息都保存成一个lua脚本,在游戏加载时读取这个脚本,把所有的动画信息都存进内存里,播放的时候调用,这当然是个简陋且低效的系统,但对于这个小游戏来说也够用了,我当时是这么认为的,但有一个问题却摆在了我的面前,那就是pico-8对于游戏代码文件的大小限制:
pico-8对于游戏的代码限制除了文件本身大小外,还有一个lua脚本的token数量,也就是最小语义单元,在一个游戏文件里最多只能试用8192个token(最小语义单元,比如 if 语句中,一个 if then end),而我把动画信息存进lua脚本的做法会大大增加脚本文件的token数量,所以我必须对动画数据做压缩,把空间留给更重要的游戏逻辑代码。
于是我想了个歪招(后来我才知道那其实是pico-8圈子里的惯用套路了),就是把原本占用大量token的一串代码,转成只被识别为一个token的字符串,用某个特定字符来分割信息,然后再写代码来批量读取信息,再在运行时存入内存里。这样动画数据的token数量就被大大压缩了。
另一个我费了很大功夫实现的功能是关于Aseprite的,Aseprite这个软件相信做像素风格游戏的人肯定不陌生,它可以在做完像素动画之后把图片信息批量导出,而对于我这个游戏来说,极限压缩数据肯定是刚需,所以我选择了最大程度压缩图片的边界,让它们紧密贴合的导出格式,而这导致的一个问题就是每次导出时,我只要对序列帧内容稍作修改,Aseprite自动拼接的图片位置就会改变,因此就需要重写序列帧的坐标信息,手动修改的话工作量会非常恐怖,于是我又临时复习了一下python,编写了两个自动处理Aseprite导出的JSON文件,并转化成lua脚本文件的脚本,这也是我第一次体会到用底层框架做游戏时,开发这种便携工具的重要性。
之后,我继续游戏开发,这里出现了又一个之前没有想到的问题:体积碰撞
我之前确实没想到一个格斗游戏会涉及体积碰撞,但现在想来其实是很关键的,因为绝大多数的格斗游戏里,角色的伤害是靠攻击动画的Hitbox产生的,角色碰撞本身不会产生伤害,也就无法处理当两个角色坐标接近时,角色贴图重叠在一起的情况,这种情况下游戏画面会显得很不可信,而且也会导致玩家可以叠怪后一次性击杀多个敌人或被多个重叠的敌人打中产生暴击的问题。
一些格斗游戏的处理方法是增加一个贴身投技,即当玩家和敌人考得太近时会把敌人抓起来,但我个人不太喜欢这个做法,因为我自己玩这类游戏的时候总觉的这个机制会让我的角色在抓起敌人的时候机动性大大降低。所以我决定还是自己写一个简单的体积碰撞系统,让所有角色都有一个体积属性,可以在坐标重叠时把彼此推开。
写完体积系统后,我又遇到了token数量限制的问题(这个问题在后期一直困扰着我)体积碰撞系统消耗了大约三百多的token,这在整个系统里占据的空间超出了我的预期,我感觉到如果继续下去我会没法制作想要的敌人种类(当时动画帧的压缩也还没做,整个项目的token数量非常不乐观),于是我盯上了另一个可以压缩token数量的部分——状态机。
我原本写的状态机是比较简陋的,是根据对象的当前状态,判断可能的下一个状态,这样写虽然很直观,但代价是重复的代码量会很多,比如从站立状态可以切换到移动或者攻击状态,而从攻击状态则可以切换到移动或是站立状态,这其中就出现了重复的移动状态,而一旦这种状态的重复叠加上后续要加入的不同种类的敌人,它的代码量会非常巨大,所以简化这部分的代码就很有必要了。
一番尝试后,我选择了根据一个角色在一次循环里的可能获取的信息,来判断它的下一个信息,这样以来,一个角色有多少种状态,基本就会获取多少种信息,而获取下一个状态机的代码还可以封装成函数在不同单位里复用,因此状态机可以写得很简略,只是构思状态机时就需要多花些心思了,因为要整理清楚一个角色同时用到的所有必要信息和不同状态之间的优先级。
最终,做完了状态机的重构后,我的游戏终于可以推进实际的内容了。
于是我开始编写游戏的主流程,包括一个玩家和敌人的血量、关卡完成的流程、玩家死亡流程。并且开始为后面的敌人做草稿设计。
然后,具体制作各种敌人就顺利很多了,我基本是一天画动画帧,一天写状态机脚本,很快就写好了token限制下能写的最多种敌人。
在这段时间里我也在积极更新社交媒体,同步我的开发状态,我开始意识到社交媒体的同步是一件很锻炼心态的事情,很多时候随着开发的进行,某个我自以为的巨大开发进展实际获得的反馈却很少,虽然我知道影响这些反馈的因素很可能和我自己没关系,但刚开始仍然影响到了我的心态,慢慢的,当我坚持发布开发日志后,我对这种波动变得更耐受了。
再之后是BOSS关卡的制作,由于BOSS的状态机和动画帧数量显然不可能放进当时的项目文件里了,所以我采取了pico-8游戏里最常用的办法:换卡带,让文件加载另一个游戏卡带,存放单独的BOSS关文件。
做好BOSS关卡后,游戏的主要部分基本就完成了,此时的游戏token一度逼近了极限。我为了给音效部分腾出空间,把之前写的debug代码全部注释掉了。
用剩下来的大约三百多token空间,我完成了最后的音效和标题画面以及操作提示。
我将这款游戏发布在了三个平台:机核网、itch.io 以及pico8的开发论坛。
同时我也在推特、微博、机核网和pico8论坛做了游戏的宣传。
现在从结果上看,机核网、微博和推特都得到了很多人的关注和回复,itch.io上的数据也显示有相当多的点击自于微博的引流,而pico8的论坛却反馈不大,这让我很意外,我现在推测可能和pico8论坛本身的热度以及它作为开发者论坛,对于这类玩法上并不创新,技术上突破也不大的游戏兴趣不大有关。
这次的开发我学到了超级多了东西。其中最重要的几点:
我仍然很喜欢这种从基础接口做起,再造轮子的开发方式,因为它可以让我对自己的游戏有更深层次的控制,我可以很好地掌控自己游戏的规模和玩法。
在以后的项目里也不要疏于为游戏的项目素材开发一些便捷工具,这对于提高开发效率很有意义。
在社交媒体上同步开发状态很重要,对于游戏的曝光和引流都有帮助,同时准备一些有意思的图片和媒体内容也能让你获得更大的关注度,毕竟游戏的玩法是一回事,社交媒体的玩法则是另一回事。但无论如何,保证自身有条不紊的开发仍是最重要的。
上一个项目让我对pico-8充满兴趣并且开启了这次的开发,而在这次项目之后,我对于pico-8的热情也燃烧殆尽了……其实也不算是燃烧殆尽,只是我确实想要尝试更大规模的游戏开发了,我没想到自己在这一个项目里就摸到了pico-8的规模极限(当然事实上如果进一步压缩也还是可以再精简的,但这样一来就更像是钻研技术而不是开发游戏了)。
因此之后我想试着寻找一些更大的基础库或是游戏开发框架,当然眼下还是先休息一阵子。
评论区
共 15 条评论热门最新