You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
765 lines
18 KiB
765 lines
18 KiB
local lvgl = require("lvgl")
|
|
|
|
local MOVE_SPEED = 480 / 8000 -- 8s for 480 pixel, pixel per ms
|
|
local PIXEL_PER_METER = 80
|
|
local TOP_Y = 20
|
|
local BOTTOM_Y = 480 - 112
|
|
local PIPE_COUNT = 5
|
|
local PIPE_GAP = 100
|
|
local PIPE_SPACE = 120
|
|
|
|
-- SCRIPT_PATH is set in simulator/main.c, used to get the abs path of first
|
|
-- lua script lua get called. In this example, SCRIPT_PATH is set to
|
|
-- path of `examples.lua`, flappyBird.lua is called when button is clicked.
|
|
|
|
local IMAGE_PATH = SCRIPT_PATH
|
|
if not IMAGE_PATH then
|
|
IMAGE_PATH = "/"
|
|
print("Note image root path is set to: ", IMAGE_PATH)
|
|
end
|
|
|
|
IMAGE_PATH = IMAGE_PATH .. "/flappyBird/"
|
|
print("IMAGE_PATH:", IMAGE_PATH)
|
|
|
|
local function randomY()
|
|
return math.random(TOP_Y + 30, BOTTOM_Y - 50 - 50)
|
|
end
|
|
|
|
local function screenCreate(parent)
|
|
local property = {
|
|
w = 480,
|
|
h = 480,
|
|
bg_opa = 0,
|
|
border_width = 0,
|
|
pad_all = 0
|
|
}
|
|
|
|
local scr
|
|
if parent then
|
|
scr = parent:Object{
|
|
w = 480,
|
|
h = 480,
|
|
bg_opa = 0,
|
|
border_width = 0,
|
|
pad_all = 0
|
|
}
|
|
else
|
|
scr = lvgl.Object(nil, property)
|
|
end
|
|
scr:clear_flag(lvgl.FLAG.SCROLLABLE)
|
|
scr:clear_flag(lvgl.FLAG.CLICKABLE)
|
|
return scr
|
|
end
|
|
|
|
local function Image(parent, src)
|
|
local img = {}
|
|
img.widget = parent:Image{
|
|
src = src
|
|
}
|
|
|
|
img.w, img.h = img.widget:get_img_size()
|
|
if not img.w or not img.h then
|
|
error("failed to load image: " .. src)
|
|
end
|
|
return img
|
|
end
|
|
|
|
local function ImageScroll(root, src, animSpeed, y)
|
|
-- image on right
|
|
local right = Image(root, src).widget
|
|
right:set{
|
|
src = src,
|
|
x = 480,
|
|
y = y,
|
|
pad_all = 0
|
|
}
|
|
|
|
local img = Image(root, src).widget
|
|
img:set{
|
|
x = 0,
|
|
y = y,
|
|
src = src,
|
|
pad_all = 0
|
|
}
|
|
|
|
img:Anim{
|
|
run = true,
|
|
start_value = 0,
|
|
end_value = -480,
|
|
time = 480 / animSpeed,
|
|
repeat_count = lvgl.ANIM_REPEAT_INFINITE,
|
|
path = "linear",
|
|
exec_cb = function(obj, value)
|
|
img:set{
|
|
x = value
|
|
}
|
|
|
|
right:set{
|
|
x = value + 480
|
|
}
|
|
end
|
|
}
|
|
|
|
img:clear_flag(lvgl.FLAG.CLICKABLE)
|
|
|
|
return img
|
|
end
|
|
|
|
local function Frames(parent, src, fps)
|
|
local frame = Image(parent, src[1])
|
|
fps = fps ~= 0 and fps or 25
|
|
|
|
frame.src = src
|
|
frame.len = #src
|
|
frame.i = 0
|
|
|
|
frame.timer = lvgl.Timer {
|
|
period = 1000 / fps,
|
|
cb = function(t)
|
|
frame.widget:set{
|
|
src = frame.src[frame.i]
|
|
}
|
|
|
|
frame.i = frame.i + 1
|
|
if frame.i == frame.len then
|
|
frame.i = 1
|
|
end
|
|
end
|
|
}
|
|
|
|
frame.start = function(self)
|
|
self.timer:resume()
|
|
end
|
|
|
|
frame.pause = function(self)
|
|
self.timer:pause()
|
|
end
|
|
|
|
return frame
|
|
end
|
|
|
|
local function Pipe(parent)
|
|
local up = Image(parent, IMAGE_PATH .. "pipe_up.png")
|
|
local down = Image(parent, IMAGE_PATH .. "pipe_down.png")
|
|
local pipe = {
|
|
up = up.widget,
|
|
down = down.widget,
|
|
w = up.w,
|
|
h = up.h,
|
|
x = 0,
|
|
y = 0
|
|
}
|
|
|
|
function pipe:updatePipePos()
|
|
self.up:set{
|
|
x = self.x,
|
|
y = self.y - up.h
|
|
}
|
|
|
|
self.down:set{
|
|
x = self.x,
|
|
y = self.y + PIPE_GAP
|
|
}
|
|
end
|
|
|
|
pipe:updatePipePos()
|
|
return pipe
|
|
end
|
|
|
|
local function ObjInfo(x, y, w, h)
|
|
return {
|
|
x = x,
|
|
y = y,
|
|
w = w,
|
|
h = h
|
|
}
|
|
end
|
|
|
|
local function Pipes(parent)
|
|
local pipes = {}
|
|
|
|
-- add initial pipe
|
|
for i = 1, PIPE_COUNT do
|
|
pipes[i] = Pipe(parent)
|
|
if i == 1 then
|
|
pipes.w = pipes[i].w -- record pipe size
|
|
pipes.h = pipes[i].h
|
|
end
|
|
end
|
|
|
|
local function pipesPosinit()
|
|
local x = 480;
|
|
local y = randomY()
|
|
|
|
for i = 1, PIPE_COUNT do
|
|
local pipe = pipes[i]
|
|
pipe.x = x
|
|
pipe.y = y
|
|
pipe:updatePipePos()
|
|
pipes[i] = pipe
|
|
x = x + PIPE_SPACE + pipe.w
|
|
y = randomY()
|
|
end
|
|
end
|
|
|
|
pipesPosinit()
|
|
|
|
pipes.score = 0
|
|
pipes.last = PIPE_COUNT -- first pipe index in pipes.pipes
|
|
pipes.totalWidth = (PIPE_COUNT) * (PIPE_SPACE + pipes.w)
|
|
pipes.birdInfo = ObjInfo(0, 0, 0, 0)
|
|
pipes.gapInfo = ObjInfo(0, 0, 0, 0)
|
|
|
|
function pipes:setObjInfo(x, y, w, h)
|
|
self.birdInfo.x = x
|
|
self.birdInfo.y = y
|
|
if w then
|
|
self.birdInfo.w = w
|
|
end
|
|
if h then
|
|
self.birdInfo.h = h
|
|
end
|
|
end
|
|
|
|
local function setGapInfo(x, y, w, h)
|
|
pipes.gapInfo.x = x
|
|
pipes.gapInfo.y = y
|
|
pipes.gapInfo.w = w
|
|
pipes.gapInfo.h = h
|
|
end
|
|
|
|
pipes.objPassing = -1
|
|
|
|
local function isBirdCollision()
|
|
local bird = pipes.birdInfo
|
|
local gap = pipes.gapInfo
|
|
|
|
-- far left
|
|
if bird.x + bird.w < gap.x then
|
|
return false
|
|
end
|
|
|
|
-- far right
|
|
if bird.x > gap.x + gap.w then
|
|
return false
|
|
end
|
|
|
|
-- in middle
|
|
|
|
if (bird.y > gap.y) and (bird.y + bird.h < gap.y + gap.h) then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function moveVirtualX(dx)
|
|
for i = 1, PIPE_COUNT do
|
|
local pipe = pipes[i]
|
|
local newX = pipe.x + dx
|
|
|
|
if newX + pipes.w < 0 then
|
|
newX = newX + pipes.totalWidth
|
|
pipe.y = randomY()
|
|
pipes.last = i
|
|
end
|
|
|
|
pipe.x = newX
|
|
pipe.updatePipePos(pipe)
|
|
end
|
|
end
|
|
|
|
local function checkScore(i)
|
|
local pipe = pipes[i]
|
|
|
|
local bird = pipes.birdInfo
|
|
local gap = pipes.gapInfo
|
|
local passing = pipes.objPassing
|
|
|
|
-- far left or right
|
|
if bird.x + bird.w < gap.x or bird.x > gap.x + gap.w then
|
|
if passing > 0 and i == passing then
|
|
pipes.score = pipes.score + 1
|
|
passing = -1
|
|
pipes.scoreUpdateCB(pipes.score)
|
|
end
|
|
else
|
|
if passing < 0 then
|
|
passing = i
|
|
end
|
|
end
|
|
|
|
pipes.objPassing = passing
|
|
end
|
|
|
|
--- Detect if obj has collision with pipes
|
|
local function collisionDetect()
|
|
local first = (pipes.last % PIPE_COUNT) + 1
|
|
for idx = 0, PIPE_COUNT - 1 do
|
|
local i = (first + idx - 1) % PIPE_COUNT + 1
|
|
local pipe = pipes[i]
|
|
setGapInfo(pipe.x, pipe.y, pipe.w, PIPE_GAP)
|
|
|
|
if isBirdCollision() then
|
|
local bird = pipes.birdInfo
|
|
if pipes.collisionCB then
|
|
pipes.collisionCB()
|
|
end
|
|
end
|
|
|
|
checkScore(i)
|
|
end
|
|
end
|
|
|
|
pipes.preValue = 0
|
|
pipes.anim = pipes[1].up:Anim{
|
|
run = false,
|
|
start_value = 0,
|
|
end_value = 480,
|
|
time = 480 / MOVE_SPEED, -- MOVE_SPEED
|
|
repeat_count = lvgl.ANIM_REPEAT_INFINITE,
|
|
path = "linear",
|
|
exec_cb = function(obj, value)
|
|
local x = pipes.preValue
|
|
local d
|
|
if value < x then
|
|
d = value + 480 - x
|
|
else
|
|
d = value - x
|
|
end
|
|
pipes.preValue = value
|
|
moveVirtualX(-d)
|
|
collisionDetect()
|
|
end
|
|
}
|
|
|
|
function pipes:start()
|
|
self.anim:start()
|
|
end
|
|
|
|
function pipes:stop()
|
|
self.anim:stop()
|
|
end
|
|
|
|
function pipes:reset()
|
|
pipesPosinit()
|
|
pipes.score = 0
|
|
pipes.preValue = 0
|
|
pipes.objPassing = -1
|
|
end
|
|
|
|
function pipes:setCollisionCB(collisionCB)
|
|
self.collisionCB = collisionCB
|
|
end
|
|
|
|
function pipes:setScoreUpdateCB(cb)
|
|
self.scoreUpdateCB = cb
|
|
end
|
|
|
|
return pipes
|
|
end
|
|
|
|
local function Bird(parent, birdMovedCB)
|
|
-- create bird Frame(sprite) in 5FPS
|
|
local bird = Frames(parent,
|
|
{IMAGE_PATH .. "bird1.png", IMAGE_PATH .. "bird2.png", IMAGE_PATH .. "bird3.png"}, 5)
|
|
|
|
local function birdVarInit()
|
|
bird.x = 240 - bird.w / 2
|
|
bird.y = 240 - bird.h / 2
|
|
bird.widget:set{
|
|
x = bird.x,
|
|
y = bird.y
|
|
}
|
|
|
|
bird.head = 0
|
|
bird.force = 0 -- in unit of m/s^2 rather than N
|
|
bird.velocity = 0 -- vertical verlocity
|
|
bird.time = 0 -- time stamp when it updates
|
|
bird.moving = false
|
|
end
|
|
|
|
birdVarInit()
|
|
|
|
bird.setY = function(self)
|
|
bird.widget:set{
|
|
y = bird.y
|
|
}
|
|
end
|
|
|
|
bird.setHead = function(self)
|
|
bird.widget:set{
|
|
angle = self.head
|
|
}
|
|
end
|
|
|
|
bird.applyForce = function(self, force)
|
|
self.force = force
|
|
if bird.moving then
|
|
return
|
|
end
|
|
|
|
bird.moving = true
|
|
self.y_anim:start()
|
|
end
|
|
|
|
bird.pressed = function(self)
|
|
bird:applyForce(-13)
|
|
bird.velocity = 0
|
|
end
|
|
|
|
bird.released = function(self)
|
|
bird:applyForce(9.8)
|
|
bird.velocity = 0
|
|
end
|
|
|
|
local function velocity2HeadAngle(v)
|
|
-- -9.8 ~ 9.8:90 ~ -90
|
|
return v * 60
|
|
end
|
|
|
|
-- y moving anim, in time.
|
|
bird.y_anim = bird.widget:Anim{
|
|
run = false,
|
|
start_value = 0,
|
|
end_value = 1000,
|
|
time = 1000, -- 1000 ms
|
|
repeat_count = lvgl.ANIM_REPEAT_INFINITE,
|
|
path = "linear",
|
|
exec_cb = function(obj, tNow)
|
|
-- we use anim to get current time, can calculate position based on force/velocity
|
|
if tNow < bird.time then
|
|
tNow = tNow + 1000
|
|
end
|
|
local y = bird.y
|
|
local preT = bird.time
|
|
local v = bird.velocity
|
|
local t = tNow < preT and tNow + 1000 - preT or tNow - preT
|
|
t = t * 0.001 -- ms to s
|
|
|
|
v = bird.force * t + v
|
|
if v > 10 then
|
|
v = 10
|
|
end
|
|
|
|
if v < -10 then
|
|
v = -10
|
|
end
|
|
|
|
y = y + v * t * PIXEL_PER_METER
|
|
if y > BOTTOM_Y - 30 then
|
|
y = BOTTOM_Y - 30
|
|
v = 0
|
|
end
|
|
if y < TOP_Y then
|
|
y = TOP_Y
|
|
v = 0
|
|
end
|
|
|
|
bird.y = y
|
|
bird.time = tNow
|
|
bird.velocity = v
|
|
bird.head = velocity2HeadAngle(v)
|
|
|
|
birdMovedCB(bird.x, bird.y)
|
|
-- set y
|
|
bird:setY()
|
|
bird:setHead()
|
|
end
|
|
}
|
|
|
|
function bird:stop()
|
|
bird.y_anim:stop()
|
|
end
|
|
|
|
function bird:gameOver()
|
|
-- like it's released forever
|
|
bird.released()
|
|
end
|
|
|
|
function bird:start()
|
|
bird.y_anim:start()
|
|
end
|
|
|
|
function bird:reset()
|
|
bird.stop()
|
|
birdVarInit()
|
|
end
|
|
|
|
return bird;
|
|
end
|
|
|
|
local function Background(root, bgEventCB)
|
|
local bgLayer = screenCreate(root) -- background layer
|
|
bgLayer:add_flag(lvgl.FLAG.CLICKABLE) -- we accept event here
|
|
|
|
local bg = ImageScroll(bgLayer, IMAGE_PATH .. "bg_day.png", MOVE_SPEED * 0.4, 0)
|
|
local pipes = Pipes(bgLayer)
|
|
local land = ImageScroll(bgLayer, IMAGE_PATH .. "land.png", MOVE_SPEED, BOTTOM_Y)
|
|
|
|
bgLayer:onevent(lvgl.EVENT.PRESSED, function(obj, code)
|
|
bgEventCB(lvgl.EVENT.PRESSED)
|
|
end)
|
|
|
|
bgLayer:onevent(lvgl.EVENT.RELEASED, function(obj, code)
|
|
bgEventCB(lvgl.EVENT.RELEASED)
|
|
end)
|
|
|
|
return {
|
|
pipes = pipes
|
|
}
|
|
end
|
|
|
|
local function SysLayer(root)
|
|
local sysLayer = screenCreate(root) -- upper layer
|
|
return sysLayer
|
|
end
|
|
|
|
local function createPlayBtn(sysLayer, onEvent)
|
|
local playBtn = Image(sysLayer, IMAGE_PATH .. "button_play.png").widget
|
|
playBtn:add_flag(lvgl.FLAG.CLICKABLE)
|
|
playBtn:set{
|
|
align = {
|
|
type = lvgl.ALIGN.CENTER,
|
|
y_ofs = 80
|
|
}
|
|
}
|
|
|
|
playBtn:onevent(lvgl.EVENT.PRESSED, onEvent)
|
|
|
|
return playBtn
|
|
end
|
|
|
|
local function entry()
|
|
local scr = screenCreate()
|
|
local bgLayer
|
|
local mainLayer
|
|
local sysLayer
|
|
local bird
|
|
local pipes
|
|
local bgEventCB -- background layer pressed/released event
|
|
local birdMovedCB -- callback when bird position updates
|
|
local collisionCB -- callback when collision happends
|
|
local flagRunning = false
|
|
local gameStart -- API to start game
|
|
local gameOver -- API to stop game
|
|
local scoreLabel
|
|
local scoreBest = 0
|
|
local scoreNow = 0
|
|
local debouncing = false
|
|
-- global event process
|
|
|
|
local scoreUpdateCB = function(score)
|
|
scoreLabel:set{
|
|
text = string.format("%03d", score)
|
|
}
|
|
scoreNow = score
|
|
end
|
|
|
|
print("font:", lvgl.BUILTIN_FONT.MONTSERRAT_26)
|
|
gameStart = function()
|
|
if flagRunning then
|
|
return
|
|
end
|
|
|
|
bird:reset()
|
|
pipes:reset()
|
|
pipes:start()
|
|
bird:start()
|
|
flagRunning = true
|
|
scoreNow = 0
|
|
if scoreLabel then
|
|
scoreLabel:set{
|
|
text = string.format("%03d", 0)
|
|
}
|
|
end
|
|
end
|
|
|
|
gameOver = function()
|
|
if not flagRunning then
|
|
return
|
|
end
|
|
|
|
debouncing = true
|
|
flagRunning = false
|
|
|
|
pipes:stop()
|
|
bird:gameOver()
|
|
if scoreNow > scoreBest then
|
|
scoreBest = scoreNow
|
|
end
|
|
|
|
local gameoverImg = Image(sysLayer, IMAGE_PATH .. "text_game_over.png").widget
|
|
gameoverImg:set{
|
|
align = {
|
|
type = lvgl.ALIGN.TOP_MID,
|
|
y_ofs = 100
|
|
}
|
|
}
|
|
|
|
gameoverImg:Anim{
|
|
run = true,
|
|
start_value = 0,
|
|
end_value = 3600,
|
|
time = 2000,
|
|
repeat_count = 2,
|
|
path = "bounce",
|
|
exec_cb = function(obj, value)
|
|
obj:set{
|
|
angle = value
|
|
}
|
|
end
|
|
}
|
|
|
|
local scoreImg = Image(sysLayer, IMAGE_PATH .. "score.png").widget
|
|
scoreImg:set{
|
|
align = {
|
|
type = lvgl.ALIGN.CENTER,
|
|
y_ofs = -20,
|
|
x_ofs = 0
|
|
}
|
|
}
|
|
scoreImg:Anim{
|
|
run = true,
|
|
start_value = 480,
|
|
end_value = 0,
|
|
time = 1000,
|
|
repeat_count = 1,
|
|
path = "ease_in",
|
|
exec_cb = function(obj, value)
|
|
obj:set{
|
|
align = {
|
|
type = lvgl.ALIGN.CENTER,
|
|
x_ofs = value,
|
|
y_ofs = -20
|
|
}
|
|
}
|
|
end
|
|
}
|
|
|
|
local scoreResultLabel = scoreImg:Label{
|
|
text = string.format("%03d", scoreNow),
|
|
text_font = lvgl.BUILTIN_FONT.MONTSERRAT_22,
|
|
align = {
|
|
type = lvgl.ALIGN.TOP_MID,
|
|
x_ofs = 0,
|
|
y_ofs = 25
|
|
}
|
|
}
|
|
|
|
local scoreBestLabel = scoreImg:Label{
|
|
text = string.format("%03d", scoreBest),
|
|
text_font = lvgl.BUILTIN_FONT.MONTSERRAT_22,
|
|
align = {
|
|
type = lvgl.ALIGN.BOTTOM_MID,
|
|
x_ofs = 0,
|
|
y_ofs = -5
|
|
}
|
|
}
|
|
scoreNow = 0
|
|
|
|
local playBtn;
|
|
playBtn = createPlayBtn(sysLayer, function(obj, code)
|
|
if debouncing then
|
|
return
|
|
end
|
|
|
|
gameStart()
|
|
playBtn:delete()
|
|
playBtn = nil
|
|
gameoverImg:delete()
|
|
gameoverImg = nil
|
|
scoreImg:delete()
|
|
scoreImg = nil
|
|
end)
|
|
|
|
lvgl.Timer {
|
|
period = 1000,
|
|
cb = function(t)
|
|
t:delete()
|
|
debouncing = false
|
|
end
|
|
}
|
|
end
|
|
|
|
bgEventCB = function(event)
|
|
if not flagRunning then
|
|
return
|
|
end
|
|
|
|
if event == lvgl.EVENT.PRESSED then
|
|
bird:pressed()
|
|
else
|
|
bird:released()
|
|
end
|
|
end
|
|
|
|
local birdMovedCB = function(x, y)
|
|
pipes:setObjInfo(bird.x, bird.y) -- set intial bird position.
|
|
end
|
|
|
|
local collisionCB = function()
|
|
print("bird collision, stop game")
|
|
-- call later
|
|
lvgl.Timer {
|
|
period = 10,
|
|
cb = function(t)
|
|
t:delete()
|
|
gameOver()
|
|
end
|
|
}
|
|
|
|
end
|
|
|
|
-- background layer, including sky, then pipes and land above
|
|
bgLayer = Background(scr, bgEventCB) -- background layer
|
|
pipes = bgLayer.pipes -- get pipes from bg layer for set bird info etc.
|
|
pipes:setCollisionCB(collisionCB)
|
|
pipes:setScoreUpdateCB(scoreUpdateCB)
|
|
|
|
-- main layer, the bird
|
|
mainLayer = screenCreate(scr) -- main layer
|
|
bird = Bird(mainLayer, birdMovedCB)
|
|
pipes:setObjInfo(bird.x, bird.y, bird.w, bird.h)
|
|
-- system layer, score etc.
|
|
sysLayer = SysLayer(scr)
|
|
|
|
local title = Image(sysLayer, IMAGE_PATH .. "title.png").widget
|
|
title:set{
|
|
align = {
|
|
type = lvgl.ALIGN.TOP_MID,
|
|
y_ofs = 80
|
|
}
|
|
}
|
|
|
|
local playBtn;
|
|
playBtn = createPlayBtn(sysLayer, function()
|
|
print("pressed")
|
|
gameStart()
|
|
playBtn:delete()
|
|
playBtn = nil
|
|
title:delete()
|
|
title = nil
|
|
|
|
local medal = Image(sysLayer, IMAGE_PATH .. "medals.png").widget
|
|
medal:set{
|
|
align = {
|
|
type = lvgl.ALIGN.TOP_MID,
|
|
y_ofs = 10,
|
|
x_ofs = -50
|
|
}
|
|
}
|
|
scoreLabel = sysLayer:Label{
|
|
text = " 000",
|
|
text_font = lvgl.BUILTIN_FONT.MONTSERRAT_28,
|
|
align = {
|
|
type = lvgl.ALIGN.TOP_MID,
|
|
x_ofs = 10,
|
|
y_ofs = 20
|
|
}
|
|
}
|
|
end)
|
|
end
|
|
|
|
entry()
|
|
|
|
-- bird = Bird(, nil)
|
|
|