前言
继上篇日志分享了第一次使用 PICO-8 开发游戏之后,我已经沉迷在 PICO-8 无限魅力之中。这次准备提高点难度开发一款动作游戏,尝试下如何处理物体移动 碰撞等。估计要分多篇日志才可以分享完,第一篇先把主要的游戏机制完成。好了先上游戏截图,介绍下游戏玩法。
在线版本地址:https://www.lexaloffle.com/bbs/?tid=31557
点击上面的链接试玩。(编辑提示:玩法见下方说明,简单的说,只要按右键蓄力就可以玩了,目标是将对方踢下去)
简单玩法
- 按方向键中的右键进行蓄力,松开后跳跃
- 在空中碰到墙壁转为攀爬状态,可以继续蓄力跳跃
- 在空中碰到最下方岩浆则死亡,等待重生
- 在空中碰到对手(2P)如果此时你的高度高于对手,则将对手踩下,我方得分;反之则被对手踩下,对手得分
角色控制
我们要控制角色在游戏中位移,首先就要记录角色位置 速度等信息。我使用了一个 table
用来记录 player1
的所有信息。
gravity = 0.25 player1 = {} player1.x = 16 //角色坐标 x player1.y = 64 //角色坐标 y player1.vx = 0 //角色水平方向速度 player1.vy = 0 //角色垂直方向速度 function _update() player1.x = player1.x + player1.vx player1.y = player1.y + player1.vy player1.vy = player1.vy + gravity end
这里的速度其实为下一帧坐标的变化量,我们在游戏循环函数 _update()
中每次执行都将 vx vy
加到 x y
中,这样就可以改变角色位置。vx vy
为 0
时角色静止不动,向右方跳跃只需要赋值 vx = 5 vy=-7
这样角色就会以每帧右移 5 像素,上移 7 像素的速度运动了。当然这里还得考虑重力才可以模拟出跳跃的抛物线,我使用了一个变量 gravity = 0.25
记录重力值,在 _update()
中每帧执行 vy = vy - gravity
改变 vy
的大小,这里的 gravity
相当于角色在 y
轴的加速度。
关于蓄力(按键时间越长跳跃越高),其实就是根据按键时间长度调整松开按键后 player1.vy
的值,按的时间越长垂直方向向上的速度越大。
角色动画
动画在 pico 中似乎比较麻烦,不像其他游戏引擎提供了良好的帧动画编辑功能。简陋的 pico 什么都没有提供,看来只能自己实现动画帧的切换了。
首先我们先把需要的 sprites 在编辑器中都画出来
- 前两帧为角色攀爬在墙上时的动画,即 1,2 两帧循环切换。
- 2,3,4,5 帧为蓄力的动画,身体靠近墙边的距离表示蓄力的程度。
- 第 6 帧为角色死亡掉落时的 sprite。
首先是角色攀爬在墙上时 1,2 两帧循环切换的方法。为了做动画首先我们得用一个变量记录时间的变化,我使用 player1.time
来记录,在 _update()
中执行 player1.time = player1.time + 1
。
实际上 time
记录了角色初始化以来的帧数,我们就可以根据这个 time
计算出当前该显示哪个 sprite。
if flr(flr(player1.time/10) % 2) == 0 then player1.frame = 1 else player1.frame = 2 end
这里的 player1.frame
记录的就是 sprite 的索引。关键代码是 flr((player1.time/10) % 2)
,这里的 flr 是向下取整函数。
这里 flr(player1.time/10)
是对 time
取商,然后又对商取余。取商的操作实际上控制了切换的速度,我们的 update
是以 30fps 的速度运行的,如果每帧都替换 sprite 那动画 1 秒将运行 15 次,显然太快了。我们 time/10
取商相当于慢了 10 倍,time
的值 1 秒钟增加 30
变成了增加 3
,每次增加的时候切换 sprite,就相当于 1 秒钟执行了 1.5 次动画。后面 %2
取 2 的余数,是为了获得 0 ,1
;当为 0 的时候显示第一帧, 1 的时候显示第二帧。这里是只有两个 sprites 就完成动画的情况,如果需要 n 张 sprites 完成动画循环,就 %n
就可以了;再根据返回的 0
到 n-1
顺序显示 sprite。
总结下 flr((time/m) % n)
,先取商 flr(time/m)
;m
越大播放速度越慢。再取余数 %n
,有几帧 n
就等于几。
蓄力动画,我们设定蓄力时间最长是 2s,也就是按下按键后蓄力动画要在 2s 之内以均匀的速度播放完毕。在 2s 内执行 4 帧动画,2s 中会执行 60 帧,60/4=15 也就是说,m=15,n=4
。flr((time/15) % 4)
,剩下的就是根据值更换 sprite 了。
角色碰撞
上一篇日志说到了根据 fget mget
来获取当前坐标值下地图中的 flag
,使用这种方法可以方便的判断角色是否碰到墙壁,或者下方的岩浆。但是游戏中另外一个角色 2P 并不是在地图编辑器中绘制出来的,所以此方法已无法获取到。我们需要新的方法判断是否于 2P 产生来碰撞。
当然这个新方法也十分简单,就是判断两个角色的 xy 坐标之差的绝对值是否同时小于 8
(角色 sprite 的宽高都是 8)。当然也可以减小阈值 8
,使碰撞更难发生。
if abs(player1.x - player2.x) < 8 and abs(player1.y - player2.y) < 8 then end
其实就是简单的 aabb 盒模型判断碰撞,pico 里几乎所有东西都是矩形,简单的矩形碰撞检测就可以通吃,可以不用复杂的碰撞检测算法。
当检测到角色碰撞后,在下方(y
比较小)的角色会被踩停下落,把下方的角色的 vx vy
都赋为 0
,角色就会在 gravity
的加速度下自由落体。
2P 颜色处理
为了跟 1P 区分,2P 至少在颜色上应该有所不同。但是仅仅是颜色不同就在 sprite sheet 上再画一遍 2P 是不是太麻烦了,而且又浪费了 6 个 sprite,要是知道 pico 只有最多 255 个 sprites。
当然 pico 提供了更好的解决方法,就是在执行 2P 绘制之前替换色板的颜色:
pal(8,12) pal(2,3) spr(player2.frame,player2.x,player2.y,1,1) pal()
这里在 spr
绘制之前调用了 pal(x,y)
方法,这个方法就是将调色板的 x
位置用 y
位置的颜色替换,这里是用 12
号蓝色替换了 8
号红色,3
号绿色替换 2
号紫色。最后在执行完 spr
绘制后,调用不带参数的 pal()
将调色板还原回去。
2P AI
这里的 AI 只做了简单的处理,就是随机取 0-2s,作为蓄力的时间,后面加入更多元素之后应该还会增加 AI 的复杂度。
目前游戏还是只是简单的模型,下面将会加入空中可以释放的道具(飞镖之类),移动的墙壁之类的丰富玩法的内容;还将加入更多的动画,爆炸,火焰等;当然还有音乐,虽然这对我来说实在是太困难了。
如果你对此游戏有任何好的建议请给我留言,当然还有文章中的错误,欢迎指正。
不错,先收藏了