Fork of Tangara with customizations
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.
 
 
 
 
 
 
tangara-fw/lib/luavgl/examples/flappyBird/flappyBird.lua

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)