PICO-8学习日志Week1.3

Prologue

        这一章正式开始之前给大家分享两个网站:

        首先是菜鸟教程里关于LUA的内容,如果没有接触过LUA或者在之前都没有学习过编程,这里面的内容会很有帮助:Lua 教程 | 菜鸟教程

        然后是Fandom中的PICO-8 Wiki,在里面我们可以找到关于PICO-8的一些内容,以及,最重要的——APIs,这个对我们以后的游戏开发有很大帮助:PICO-8 WIKI

        在正式开始之前先介绍一下如何保存制作的PICO-8项目,在命令行中输入save [filename]指令,游戏就可以快速保存为.p8文件格式,不输入文件名的话就会保存以untitledn的文件名保存。

        不过.p8并不是制作者们常用的文件分享格式,如果大家有试过从网上下载一些PICO-8游戏的话,就会发现保存的格式都是.p8.png,最后的形式就有点像一个游戏卡:

        这就是PICO-8比较有特色的一个地方:虽然这些看上去就是一个个图片,但是所有的代码、美术、音乐,都包含在这一个小小的图片里面。

        不过这上面的两种方式都需要有一个PICO-8才可以运行,如果我想把我做的游戏分享给我的朋友,而他并没有PICO-8,他还能在他的电脑上玩到我的游戏吗?

        答案是可以的,输入export [filename].html,我们的游戏就可以保存为网页格式,打开之后会发现是一个镶嵌在网页内的PICO-8,里面就是我们的游戏:

1.4 Cave Diver游戏的开发

        经过之前的两篇,我们已经认识了PICO-8的基本操作以及Game Loop的相关知识,这一篇我们就可以继续跟随Dylan来制作第一个正儿八经的的游戏:Cave Diver

        Cave Diver对于大家来说应该不算陌生,这类型的游戏还是蛮多的,在2013年出现的现象级手机游戏Flappy Bird就是一个典型,当年我还没有自己的手机,所以只在别人的手机上玩过。后来手机游戏开始往MOBA、射击游戏的方面发展,这类小巧有趣的手机游戏就不再能见得到了。

        回到我们的PICO-8,在书里面Dylan给出了完整的代码以及制作思路,不过对于程序苦手来说理解起来还是有些困难,更不用说原文还是英文了,所以在这里我还是会一行一行地来解析这个游戏,从而能够理清楚这个游戏的运作逻辑。

        

        首先我们应该考虑的是这个游戏该做些什么:游戏开始后,我们控制一个小精灵让它在一个不断生成的“洞穴”中前行,为了保证游戏的可玩度,我们应该增加一些难度,比如让小精灵下坠,然后我们点击按钮让小精灵往上飞。

        不过我们有必要让小精灵真的往前前进吗?答案是否定的,实际上我们只需要让摄像机内的洞穴不断左移,看起来就像小精灵在往前走一样,这个相对运动的原理在游戏中的运用可以说是非常的频繁,因为向前运动对于程序来说总归不是一件方便的事情。

        清楚我们需要做的事情之后,就可以一步步来实施了。

        老样子,在开始之前不要忘了绘制你的精灵:在这个例子里面,我们的精灵有三个状态:下坠、上升以及失败,所以我们需要绘制三个精灵来对应三个状态:

        上图是Dylan给出的例子,当然我们也可以画三个属于自己的精灵:

        然后就是代码部分了:

--0
function _init()
 game_over=false
 make_player()
end

function _update()
end

function _draw()
 cls()
 draw_player()
end
--1
function make_player()
 player={}
 player.x=24 --position
 player.y=60
 player.dy=0 --fall speed
 player.rise=1 --sprites
 player.fall=2
 player.dead=3
 player.speed=2 --fly speed
 player.score=0
end

function draw_player()
 if (game_over) then
  spr(player.dead,player.x,player.y)
 elseif (player.dy<0) then
  spr(player.rise,player.x,player.y)
 else
  spr(player.fall,player.x,player.y)
 end
end

        Dylan在这个游戏的制作之中使用的代码分页来增加可读性,当然我们也可以把所有代码写进一页,不过我还是把Dylan原文中放置代码的页数备注在了最前面。

        make_player()函数初始化了player,这里将player设置成了一个table,LUA中的table可以类比于Python中的字典,每一项有对应的key以及value。

        关于table的内容菜鸟教程里面有比较详细的介绍,我就不说太多了,要补充一点是:在上面的代码里面是用“.key”来访问value,其实这是等价于["key"]的:

list={}
list.x=114

print(list.x)
print(list["x"])

--输出结果:
--114
--114

        另提一句,类似于C语言,LUA中table名存储的是一个指针,所以我们运行print(list)并不能打印出来table的内容。

        回到游戏,在初始化了player之后,draw_player()函数给三种状态绑定了各自的精灵,这样在游戏运行中精灵的外貌就可以发生变化。

        现在我们的精灵只会呆在原地,我们需要编写一个运动函数让它动起来:

--1
function move_player()
 gravity=0.2 --bigger means more gravity!
 player.dy+=gravity --add gravity

 --jump
 if (btnp(2)) then
  player.dy-=5
 end

 --move to new position
 player.y+=player.dy
end

        同时不要忘了修改你的_update()函数:

function _update()
 move_player()
end

        这里将精灵的下坠设计成了一个匀加速运动,而不是看起来很呆的匀速运动,实现方法其实也很简单,运用的就是最基础的运动公式:x = x0 + vt,由于代码是随时间运行,t 可以舍掉,v = v0 + at,同样舍掉 t,两个式子合并就是x = x0 + v0 + a,在代码中就是定义gravity,这个就是加速度a,速度的变化用player.dy+=gravity来实现,最后用player.y+=player.dy来实现位置的变化,最后就可以实现精灵的匀加速下坠。

        而当我们按下“↑”时,又会给一个向上的瞬时加速度,这样就可以实现往上飞。

        这个时候按下Ctrl+R,我们的小精灵就可以实现最基本的运动了,接下来就是cave的绘制:

--2
function make_cave()
 cave={{["top"]=5,["btm"]=119}}
 top=45 --how low can the ceiling go?
 btm=85 --how high can the floor get?
end

function update_cave()
 --remove the back of the cave
 if (#cave>player.speed) then
  for i=1,player.speed do
   del(cave,cave[1])
  end
 end

 --add more cave
 while (#cave<128) do
  local col={}
  local up=flr(rnd(7)-3)
  local dwn=flr(rnd(7)-3)
  col.top=mid(3,cave[#cave].top+up,top)
  col.btm=mid(btm,cave[#cave].btm+dwn,124)
  add(cave,col)
 end
end

function draw_cave()
 top_color=5 --play with these!
 btm_color=5 --choose your own colors!
 for i=1,#cave do
  line(i-1,0,i-1,cave[i].top,top_color)
  line(i-1,127,i-1,cave[i].btm,btm_color)
 end
end

        老样子,不要忘记修改三个流程函数:

--0
function _init()
 game_over=false
 make_cave()
 make_player()
end

function _update()
 update_cave()
 move_player()
end

function _draw()
 cls()
 draw_cave()
 draw_player()
end

        这里对于洞穴的处理使用了一个比较取巧的方法:将洞穴视作一个个连续的顶部直线与底部直线的组合,将这一个个组合以table的形式存放在数组里面,通过在右侧增加新的组合以及在左侧删除掉旧的组合,来实现洞穴的“运动”,我们的小精灵看上去就像在往前走了一样。

        make_cave()实现了这样一个table数组的创建,同时还设定了两个值:top 和 btm,这两个值规定了顶部的直线和底部的直线最多能有多长,具体的用法在draw_cave()里面。

        然后就是对cave数组的生成和删除,这一功能靠update_cave()来实现,这一个函数里面又分成了两小块,第一块 if 嵌套的 for 循环实现了cave的删除,if(#cave>player.speed)保证了cave数组不会被过早的删除,在数组名称前面加一个“#”可以获得这个数组的长度,比如这里#cave就是返回cave里面有多少个table,这个值在第一个Game Loop里面是1, 所以在第一个Game Loop里面del函数并不会实施,这样一来就可以确保cave的长度可以正确增加至128。

        这里的del()函数相比其它语言的数组删除元素函数更加友好:在删除某个元素之后,它会自动让后面所有的元素往前挪一位。

        for循环做的事情就是在每一个Game Loop里面从前往后删除掉cave中与player.speed等同数量的table元素,这样的话数组尾部会留出来同样数量的空白位置,这个位置会被随后的增加table元素的while代码块所填补,这个操作其实有点像C语言里面的队列,从尾部入队,从头部出队,这样的话就可以实现洞穴与player之间与player.speed值相同速度的相对运动——实际上speed并不是精灵的speed,而是洞穴刷新的速度。

        不知道大家有没有疑惑过PICO-8代码里面循环的运作,因为Game Loop本身就是个循环,循环里面的循环该怎么运作?其实答案很简单,每个Game Loop内所有代码都是会完整地运行一遍,比如我的代码里面有一个30次的for循环,那样的话每一个Game Loop中 for 循环里面的语句都会一遍不少的运行30次,也就是说,在 1s 内,它运行了30*30 = 900次:完全不用担心会因为过多的循环导致游戏出现一些意料之外的事情,虽然PICO-8看上去很古老,但它毕竟是在我们的计算机上运行的。

        回到游戏,接下来让我们看看while循环中如何给cave增加新的table元素:

        while的循环条件是 (#cave<128),当 cave 数组内的元素小于128个时,while循环会一直运行,直到cave数组的元素数量达到128个。为什么是128个?上一篇我们提到过,游戏运行后摄像机的可视范围是一个128*128的矩形,128正好可以填满屏幕,不多也不少。

        while函数的开始定义了三个local变量,这样可以保证循环内的数值不会受外界的干扰,同时各个循环也不会互相干扰。其中的col虽然按数组的方式进行定义,不过在后面我们也可以看到,实际上col是一个table,他有两个key,top和btm,这两个key是不是有点眼熟?它们就是在make_cave()里面出现过的,这也就是说,col就是要加到cave中的table元素,最后的add(cave,col)也实现了这一点,add()的用法也不言而喻了。

        至于这里面出现的另外两个函数:flr(num)的功能是将num向下取整,rnd(num)的功能是生成一个大于等于0.0小于num的数,并不是整数,比如rnd(10)就是生成一个0.0 ~ 9.99999的随机数,因为不是整数,所以外面套了一个flr()来取整。另外,无论是up还是down,在后面我们都可以看见实际上是变化值,最后生成的洞穴肯定是起起伏伏的,所以数值的变化也是有增有减,这里的“-3”实现的就是这个功能。

        最后再来看 col.top 和 col.btm 的赋值:mid(a, b, c)函数用于取中间值,在这里出现的3个数分别是3, cave[#cave].top+up, top,top 在 make_cave() 里面给定的值是45,cave[#cave].top+up就是新生成的top值,是对前一个cave元素加上up值,别忘了up有正有负。另外一提,LUA中的数组初始是从1开始计数的,所以这里cave[#cave]指的是队尾的元素,而不是队尾元素后面的那个空位,这是和其他语言不太一样的一点。最后,mid()函数保证了新生成的 col.top 是一个处于[3, 45]之间的值,不至于太大,也不至于太小。col.btm使用了同样的生成方法,这里就不再赘述了。

        现在cave已经生成并且可以实现更新,接下来就是如何将它绘制出来了:

        这里使用了line()函数来循环绘制直线,line()函数和EGE里面一样,五个值分别是起始点的X坐标、起始点的Y坐标、终点的X坐标、终点的Y坐标以及线条的颜色编号,这里的编号就是绘制界面的色盘从0 ~ 15的16种颜色对应的编号:

         

        现在再来运行游戏:我们可以看到洞穴已经可以生成了,但是我们的小精灵就算撞到墙上也不会发生什么,而是直接穿了过去,那是因为我们还没有添加检测碰撞的函数:

--1
function check_hit()
 for i=player.x,player.x+7 do
  if (cave[i+1].top>player.y 
   or cave[i+1].btm<player.y+7) then
   game_over=true
  end
 end
end

        然后修改_update()函数:

--0
function _update()
 if (not game_over) then
  update_cave()
  move_player()
  check_hit()
 end
end

        这里的 check_hit() 原理其实很简单:就是从左到右依次检测小精灵的每个顶部像素格和底部像素格是否“碰”到了洞穴,player.x是精灵左上角的那个点,精灵的宽度是8,所以这里的循环条件是player.x, player.x+7。检测到“碰撞”的时候,给 game_over 赋值为 true,此时 _update()内的代码不再运行,但是游戏是没有结束的,只是画面不再发生变化了而已。

        保存代码,Ctr+R运行,大功告成,小精灵可以在洞穴内自由向前,同时碰壁的时候游戏也会停止,我们的小精灵也会寄掉,接下来我们可以试着给游戏增加一个计分,同时在玩家游戏失败后会弹出Game Over的对话框,下面的内容相对简单,是对已有的两个函数添加一些新东西,就不再赘述了:

--0
function _draw()
 cls()
 draw_cave()
 draw_player()
 if (game_over) then
  print("game over!",44,44,7)
  print("your score:"..player.score,34,54,7)
 else
  print("score:"..player.score,2,2,7)
 end
end
--1
function move_player()
 --add gravity
 player.dy+=0.2

 --jump
 if (btnp(2)) then
  player.dy-=5
 end

 --move to new position
 player.y+=player.dy

 --update score
 player.score+=player.speed
end

        这里简单介绍一下print()函数:print(text, [x,] [y,] [color])需要四个参数,分别是文字内容、文本框左上角的X坐标、Y坐标以及颜色代码。

        现在运行游戏,在碰壁之后就可以显示出来Game Over对话框,同时也可以打印出玩家所获得分数,但此时,Game Over之后我们只能退出再运行程序来再次开始游戏,貌似有点不合理,那么我们可以通过按下按钮来实现游戏重新开始:

--0
function _update()
 if (not game_over) then
  update_cave()
  move_player()
  check_hit()
 else
  if (btnp(5)) _init() --restart
 end
end

function _draw()
 cls()
 draw_cave()
 draw_player()

 if (game_over) then
  print("game over!",44,44,7)
  print("your score:"..player.score,34,54,7)
  print("press × to play again!",18,72,6)
 else
  print("score:"..player.score,2,2,7)
 end
end

        这里增加一个按键触发的事件:当 game_over == true 触发 else 语句,玩家按下X时,再次运行_init()函数,这样的话游戏就实现了初始化。

        到这里,我们的游戏已经非常完善了,但是略显单调:不妨来给我们的游戏增加点音乐:

         PICO-8的音乐编辑非常的简单,我们可以直接用鼠标在上面拖动,就可以绘制出一条音轨,或者说按下这些按键:

         这个上面是键盘位置跟钢琴按键的对应关系,这样我们就可以更方便地编辑音乐。

        播放音效的函数时sfx(),括号里面放音效的编号:

--1
function move_player()
 --add gravity
 player.dy+=0.2

 --jump
 if (btnp(2)) then
  player.dy-=5
  sfx(0)
end

 --move to new position
 player.y+=player.dy

 --update score
 player.score+=player.speed
end

function check_hit()
 for i=player.x,player.x+7 do
  if (cave[i+1].top>player.y 
   or cave[i+1].btm<player.y+7) then
   game_over=true
   sfx(1)
  end
 end
end

        Ctrl+R运行:

         恭喜!你在PICO-8上做出了第一个属于自己的游戏!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PICO-8中,地图可以使用`tmap()`或`map()`函数来绘制。这两个函数都可以用于在屏幕上显示地图,但它们的用法略有不同。 `tmap()`函数可以使用任何颜色为地图定义一个tileset(瓷砖集合),然后使用这些瓷砖绘制地图。这使得您可以使用多个不同的颜色来创建一系列不同的瓷砖,并将它们组合成地图。 `map()`函数则使用单个颜色定义地图,其中每个像素都表示地图上的一个单元格。这使得创建地图变得非常简单和直观。 下面是一个使用`map()`函数绘制地图的示例代码。假设我们有一个32x32的地图,每个单元格大小为8x8像素: ``` --定义地图数据 local map_data = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, } --绘制地图 function _draw() map(0, 0, 0, 0, 32, 32, map_data) end ``` 在这个例子中,我们首先定义了一个包含地图数据的表`map_data`,其中1表示墙,0表示地面。然后我们在`_draw()`函数中使用`map()`函数绘制地图。 `map()`函数的第一个参数是地图左上角的x坐标,第二个参数是地图左上角的y坐标。第三个参数是地图中每个单元格的宽度,第四个参数是每个单元格的高度。第五个参数是地图的宽度,第六个参数是地图的高度。最后一个参数是包含地图数据的表。 希望这个示例可以帮助您了解如何在PICO-8中绘制地图!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值