From 8a0a167adbf3d9b6f8b6f16aaf20ca39ad5549de Mon Sep 17 00:00:00 2001 From: jacqueline Date: Sun, 12 Nov 2023 19:14:09 +1100 Subject: Convert the main menu screen to lua lol --- lib/luavgl/examples/flappyBird/flappyBird.lua | 765 ++++++++++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 lib/luavgl/examples/flappyBird/flappyBird.lua (limited to 'lib/luavgl/examples/flappyBird/flappyBird.lua') 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) -- cgit v1.2.3