PICO-8 开发动作游戏(1)

作者:83872309
2018-07-30
16 14 1

前言

继上篇日志分享了第一次使用 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 vy0 时角色静止不动,向右方跳跃只需要赋值 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 就可以了;再根据返回的 0n-1 顺序显示 sprite。

总结下 flr((time/m) % n),先取商 flr(time/m)m 越大播放速度越慢。再取余数 %n,有几帧 n 就等于几。

蓄力动画,我们设定蓄力时间最长是 2s,也就是按下按键后蓄力动画要在 2s 之内以均匀的速度播放完毕。在 2s 内执行 4 帧动画,2s 中会执行 60 帧,60/4=15 也就是说,m=15,n=4flr((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 的复杂度。

目前游戏还是只是简单的模型,下面将会加入空中可以释放的道具(飞镖之类),移动的墙壁之类的丰富玩法的内容;还将加入更多的动画,爆炸,火焰等;当然还有音乐,虽然这对我来说实在是太困难了。

如果你对此游戏有任何好的建议请给我留言,当然还有文章中的错误,欢迎指正。

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. 千罹 2018-07-30

    不错,先收藏了

您需要登录或者注册后才能发表评论

登录/注册