summaryrefslogtreecommitdiff
path: root/lib/luavgl/examples/flappyBird/flappyBird.lua
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-11-12 19:14:09 +1100
committerjacqueline <me@jacqueline.id.au>2023-11-12 19:14:09 +1100
commit8a0a167adbf3d9b6f8b6f16aaf20ca39ad5549de (patch)
tree02b6cf23f591915747ec2994381854a79979c4a0 /lib/luavgl/examples/flappyBird/flappyBird.lua
parent8471046a95ab9e00f7d42b56dbbc9ce3e5b424b9 (diff)
downloadtangara-fw-8a0a167adbf3d9b6f8b6f16aaf20ca39ad5549de.tar.gz
Convert the main menu screen to lua lol
Diffstat (limited to 'lib/luavgl/examples/flappyBird/flappyBird.lua')
-rw-r--r--lib/luavgl/examples/flappyBird/flappyBird.lua765
1 files changed, 765 insertions, 0 deletions
diff --git a/lib/luavgl/examples/flappyBird/flappyBird.lua b/lib/luavgl/examples/flappyBird/flappyBird.lua
new file mode 100644
index 00000000..26018bba
--- /dev/null
+++ b/lib/luavgl/examples/flappyBird/flappyBird.lua
@@ -0,0 +1,765 @@
+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)