贪吃蛇大家小时候都玩过,整体逻辑也不是很复杂,于是就使用PICO-8将其简单的复刻了一下。

场景布置

场景上很简单,就是在屏幕的四周围了一圈墙壁,随手画了一个墙壁的sprite,并将其Flag第一个灯打开,用于检测碰撞。然后使用绿色块来作为蛇身,橙色块作为食物。

Flags:PICO-8机制,可以给精灵图设置Flag,一共0-7,对应2的0-7次方相加,比如点开0号灯和2号灯,那么结果就是 2^0 + 2^2 = 5,通过FGET函数来获取对应块的Flag情况以此来作为某些情况处理的判断依据。

初始设置

在初始设置中,设置了snake数组,运动方向,游戏状态,帧数累计以及食物的初始状态等等。

function _init()
snake = { {x=1,y=1} } --设置一个数组用于存放蛇身,初始状态下只有一个点,将其放置在1,1区块

dir_flag=2 -- 设置初始运动方向为朝右
temp_dir_flag=dir_flag --设置一个临时朝向,用于在具体执行绘制前,玩家可以随意更改方向,只要手速快
dir_btn={⬅️,➡️,⬆️,⬇️} --设置方向数组
dir_offset={{dx=-1,dy=0},{dx=1,dy=0},{dx=0,dy=-1},{dx=0,dy=1}} --设置不同朝向对坐标影响的数组

frame=0 --帧数的累计,便于设置snake运动的速度
game_state='init' --游戏状态 有 'init', 'running', 'over' 三种

score=1 --得分,即snake长度,且这个得分与速度相关,得分越高,速度越快

food_pos={ --设置食物的位置,默认第一个在地图中间
x=7,
y=7
}
end

update和draw

update和draw函数中根据游戏状态来执行不同的显示和逻辑

function _update()
if game_state=='running' then --如果当前状态为running,那么执行游戏运行的逻辑
game_running()
elseif game_state=='init' then --如果当前状态为init,那么按下x开始游戏
if btnp(❎) then
game_state='running'
end
else
if btnp(❎) then --如果为over的话,按下x重置游戏,并将状态变为running
_init()
game_state='running'
end
end
end

function _draw()
cls() --清空屏幕
map() --绘制地图

if game_state == 'running' then --如果状态为running,那么绘制游戏执行的图像
game_draw()
elseif game_state == 'init' then --如果状态为init,那么在屏幕中间打印提示
print("press ❎ to start", 33, 63)
else
print("score: "..score,48,53) --如果状态为over那么在屏幕中间打印分数和提示
print("press ❎ to restart", 30, 63)
end
end

--游戏运行中的绘制非常简单,只要遍历snake,并将蛇身的每一块都绘制在屏幕上,然后在对应的食物位置绘制食物即可
--绘制的时候对区块和像素做一下转换 pixel = block * 8
function game_draw()
for i=1, #snake do
spr(2,snake[i].x*8,snake[i].y*8)
end
spr(3,food_pos.x*8,food_pos.y*8)
end

游戏运行

_update函数每帧执行一次,默认一秒钟执行30次,那么默认情况下,frame % 15 == 0执行位移的话,那么相当于一秒钟位移2次,随着分数的上升,进行求模运算的对象越来越小,如得了10分之后,speed就为10,也就是每秒位移3次。速度有个限制,当小于6的时候会强制为6。这样最多每秒位移5次,防止因为过快导致难度太大。

在位移里面才实际设置方向,可以确保玩家在位移执行之前,可以修改自己的方向。实际设置位移时,也会有限制,比如当前是往右,此时按下左键不能生效,因为蛇不能掉头,所以加上判断。

在位移执行之前,还需要进行碰撞检测,获取下一个要运动到的区块,进行碰撞检测。

function game_running()
frame+=1 --每一帧都将帧数累计1
control_dir() --这个函数用于控制方向
--控制帧数
local speed = max(6,15 - flr(score/2))
if frame % speed == 0 then
--实际修改位移方向 限制掉头
if (dir_flag<=2) ~= (temp_dir_flag<=2) then
dir_flag=temp_dir_flag
end

--获取下一个位置的节点坐标,进行碰撞检测
local check_node = {
x=snake[1].x+dir_offset[dir_flag].dx,
y=snake[1].y+dir_offset[dir_flag].dy
}
check_collision(check_node.x,check_node.y)
--移动snake
move_snake()
end
end

function control_dir() --遍历方向数组,将每次按下对应的按钮就将方向赋值给temp_dir_flag
for i=1,#dir_btn do
if btnp(dir_btn[i]) then
temp_dir_flag=i
end
end
end

碰撞检测

蛇头不能碰撞到蛇身和墙壁,如果碰撞到了,那么游戏结束。mget函数可以获取地图对应位置区块在sprites中的编号,再使用fget可以获取对应编号的sprite上面flag,用于判断是否发生碰撞。

function check_collision(x,y)

local col = mget(x,y)
if fget(col) == 1 then --如果对应区块上的0号灯是亮着的,那么表示碰撞到了墙壁
game_state='over' -- 游戏结束
end

for i=2,#snake do --遍历蛇身
if x==snake[i].x and y==snake[i].y then --如果对应区块是蛇身的话,那么碰撞到了蛇身,游戏结束
game_state='over'
end
end
end

移动snake

snake位移的逻辑,就是不断将下一个位移到的区块添加到snake数组的第一个位置,然后将snake数组最后一个元素删除,这样就不需要维护蛇身的坐标变化了,如果吃到了食物蛇身变长,那么就不用删除最后一个元素。

function move_snake()
local head = snake[1] --获取蛇头
local dir_use=dir_offset[dir_flag] --获取对应方向需要执行的坐标操作
local new_node = { --位移的目标位置
x=head.x+dir_use.dx,
y=head.y+dir_use.dy
}

add(snake,new_node,1) --将这个位置添加到蛇头

if not food_collsion() then --如果没有吃到食物,那么将snake数组最后一个位置删除
del(snake,snake[#snake])
else --如果吃到了食物,那么分数加1,并重新生成一个食物
score+=1
generate_food()
end
end

function food_collision() --食物碰撞检测,蛇头和食物点重合即可
return snake[1].x == food_pos.x and snake[1].y == food_pos.y
end

生成食物

重新生成食物的时候,不能在蛇身,也不能在墙壁上生成,那么先得到可用位置的列表,然后在其中随机选择一个。

PICO-8的rnd函数,传入一个数组,可以随机返回数组中的某一项。

function generate_food()
local node_list = generate_node_list() --获取可用节点的列表
food_pos = rnd(node_list) --从节点中随机获取一项,作为新食物产生的位置
end

function generate_node_list()
local node_list={}
for i=1,14 do --遍历长宽,最左边和最右边,最上边,最下边都是墙壁,排除
for j=1,14 do
local access = true --判断是否是蛇身
for x=1,#snake do
if i==snake[x].x and j==snake[x].y then
access = false
end
end
if access then --如果不是蛇身,那么添加到可用节点列表中
add(node_list,{x=i,y=j})
end
end
end
return node_list
end

试玩