A little game integrated with its tools
A week ago, my son (hearing that I was at loose ends) asked for "a game where you drag the player around the screen". This set some wheels in motion inside my head, resulting in this 3-screen app: one screen for a sprite editor, a second screen for a level/maze editor, and the final screen putting them together into a weird little game where you try to solve a maze without letting your player touch the walls. The screens exchange information using global variables for the player sprite and the maze. To try it out after downloading Lua Carousel:
1. Run the following abbreviations:
g = love.graphics pt, line, rect = g.point, g.line, g.rectangle color = g.setColor min, max = math.min, math.max floor = math.floor
2. In a new screen, run the following sprite editor (100 lines):
palette = {{1,0,0}, {0,0,1}} W,H = 8,8 sprite = {} for i=1,H do local row = {} for j=1,W do table.insert(row, (i+j)%#palette + 1) end table.insert(sprite, row) end Ezoom = 32 -- pixels -- top-left of sprite EX,EY = 40, 90 Ewidgets = {} -- one button per pixel of sprite function sprite_pixel(X,Y) local x = EX + (X-1)*Ezoom local y = EY + (Y-1)*Ezoom local draw = function() color(unpack(palette[sprite[Y][X]])) rect('fill', x,y, Ezoom-1,Ezoom-1) end local ispress = function(x2,y2) return x2 >= x and x2 <= x+Ezoom and y2 >= y and y2 <= y+Ezoom end local press = function() sprite[Y][X] = sprite[Y][X]%#palette + 1 end return {draw=draw, ispress=ispress, press=press} end for y=1,H do for x=1,W do table.insert(Ewidgets, sprite_pixel(x,y)) end end -- one slider per color component in palette function palette_component(p, c) local x0, x1 = EX+20, 200 -- left and right limit local lo, hi = 0, 1 -- what left and right map to local x = x0 + (x1-x0)*(palette[p][c]-lo)/(hi-lo) local top = EY+Ezoom*H + 30 local y = top + (p-1)*100 + (c-1)*30 local w,h = 20,20 local selected = false local slider_color = {0,0,0} slider_color[c] = 1 local draw = function() color(unpack(slider_color)) line(x0,y, x1,y) rect('fill', x-w/2, y-h/2, w,h) end local ispress = function(x2,y2) return x2 >= x-w/2 and x2 <= x+w/2 and y2 >= y-h/2 and y2 <= y+h/2 end local press = function() selected = true end local update = function(x2,y2) if selected then x = min(max(x2, x0), x1) palette[p][c] = lo + (x-x0)*(hi-lo)/(x1-x0) end end local release = function() selected = false end return {draw=draw, ispress=ispress, press=press, update=update, release=release} end for p=1,#palette do for c=1,#palette[1] do table.insert(Ewidgets, palette_component(p, c)) end end function car.draw() for name,w in pairs(Ewidgets) do w.draw() end end function car.mousepressed(x,y, b) for name,w in pairs(Ewidgets) do if w.ispress(x,y) then return w.press() end end end function car.update(dt) for name,w in pairs(Ewidgets) do if w.update then w.update(App.mouse_x(), App.mouse_y()) end end end function car.mousereleased(x,y, b) for name,w in pairs(Ewidgets) do if w.release then w.release() end end end
(Many globals here start with 'E' to avoid conflicting with the other screens.)
3. In a new screen, run the following maze editor (100 lines):
Aw = Safe_width-60 -- playing area Ah = Safe_height-Menu_bottom Pzoom = 4 local Pw = W*Pzoom -- player width local Psx = floor(Pw*1.5) -- player x space local Mcw = floor(Aw/Psx) -- maze width in cells local Mw = Psx*Mcw -- Maze width local Mx = floor((Safe_width-Mw)/2) local Ph = H*Pzoom local Psy = floor(Ph*1.5) local Mch = floor(Ah/Psy) -- maze height in cells local Mh = Psy*Mch -- Maze height local My = Menu_bottom + floor((Safe_height-Menu_bottom-Mh)/2) -- maze consists of Mcw*Mch rooms -- each room has 4 walls around it -- but the outer walls are implicitly built and so not represented -- so we have (Mch-1)*Mcw horizontal walls and Mch*(Mcw-1) vertical walls -- cell x,y has horizontal walls x,y-1 and x,y, and vertical walls x-1,y and x,y -- walls that are out of bounds of Mhw and Mvw are always solid Mhw, Mvw = {}, {} for i=1,Mch-1 do local mhwr = {}, {} for j=1,Mcw do table.insert(mhwr, 0) end table.insert(Mhw, mhwr) end for i=1,Mch do local mvwr = {} for j=1,Mcw-1 do table.insert(mvwr, 0) end table.insert(Mvw, mvwr) end widgets = {} -- one button per horizontal wall function maze_hor_wall(X,Y) local x = Mx + (X-1)*Psx+10 local y = My + Y*Psy-10 local w = Psx-20 local h = 20 local draw = function() color(0.5,0.5,0.5) if Mhw[Y][X] == 1 then rect('fill', x,y,w,h, 5) else rect('line', x,y,w,h, 5) end end local ispress = function(x2,y2) return x2 >= x and x2 <= x+w and y2 >= y and y2 <= y+h end local press = function() Mhw[Y][X] = 1-Mhw[Y][X] end return {draw=draw, ispress=ispress, press=press} end for y=1,Mch-1 do for x=1,Mcw do table.insert(widgets, maze_hor_wall(x,y)) end end -- one button per vertical wall function maze_vert_wall(X,Y) local x = Mx + X*Psx-10 local y = My + (Y-1)*Psy+10 local w = 20 local h = Psy-20 local draw = function() color(0.5,0.5,0.5) if Mvw[Y][X] == 1 then rect('fill', x,y,w,h, 5) else rect('line', x,y,w,h, 5) end end local ispress = function(x2,y2) return x2 >= x and x2 <= x+w and y2 >= y and y2 <= y+h end local press = function() Mvw[Y][X] = 1-Mvw[Y][X] end return {draw=draw, ispress=ispress, press=press} end for y=1,Mch do for x=1,Mcw-1 do table.insert(widgets, maze_vert_wall(x,y)) end end function car.draw() color(0.8,0.8,0.8) rect('line', Mx, My, Mcw*Psx, Mch*Psy) for name,w in pairs(widgets) do w.draw() end end function car.mousepressed(x,y, b) for name,w in pairs(widgets) do if w.ispress(x,y) then return w.press() end end end function car.update(dt) for name,w in pairs(widgets) do if w.update then w.update(App.mouse_x(), App.mouse_y()) end end end function car.mousereleased(x,y, b) for name,w in pairs(widgets) do if w.release then w.release() end end end
(The names got a bit cryptic here as I worked on my phone. Hopefully the comments are clear. Definitely comment if something doesn't make sense.)
4. In a new screen, run the following code for the game itself (100 lines) which uses the sprite and maze defined in the previous screens.
Game_over = nil Aw = Safe_width-60 -- playing area Ah = Safe_height-Menu_bottom Pzoom = 4 local Pw = W*Pzoom -- player width local Psx = floor(Pw*1.5) -- player x space local Mcw = floor(Aw/Psx) -- maze width in cells local Mw = Psx*Mcw -- Maze width local Mx = floor((Safe_width-Mw)/2) local Ph = H*Pzoom local Psy = floor(Ph*1.5) local Mch = floor(Ah/Psy) -- maze height in cells local Mh = Psy*Mch -- Maze height local My = Menu_bottom + floor((Safe_height-Menu_bottom-Mh)/2) Px,Py = Mx+5, My+5 move = nil function car.draw() draw_board() --draw_game_over() draw_sprite(sprite, Pzoom, Px,Py, palette) color(0.5,0.5,0.5) rect('fill', Px, Py+#sprite*Pzoom, #sprite[1]*Pzoom, 30) if Game_over then color(1,0,0) g.print('game over!', 30,30) end end function car.mousepressed(x,y, b) if Game_over then return end if x >= Px and x <= Px+#sprite[1]*Pzoom and y >= Py+#sprite*Pzoom and y <= Py+#sprite*Pzoom+30 then move = {x=x-Px, y=y-Py} end end function car.mousereleased() if Game_over then return end move = nil end function car.update() if Game_over then return end if move then Px = App.mouse_x() - move.x Py = App.mouse_y() - move.y Game_over = game_over(Px, Py) end end function game_over(x, y) -- is Px, Py colliding with a solid wall? -- borders local mcx = floor((x-Mx)/Psx)+1 local mcy = floor((y-My)/Psy)+1 if mcx < 1 or mcy < 1 then return true end if mcx > Mcw or mcy > Mch then return true end local ox = (x-Mx)%Psx local oy = (y-My)%Psy if ox > Pw/2 then if mcx == Mcw then return true end if Mvw[mcy][mcx] > 0 then return true end end if oy > Ph/2 then if mcy == Mch then return true end if Mhw[mcy][mcx] > 0 then return true end end end function draw_game_over() for x=30,Safe_width do for y=Menu_bottom,Safe_height do if game_over(x,y) then color(1,0,0, 0.2) else color(0,1,0, 0.2) end pt(x,y) end end end function draw_board() color(0.8,0.8,0.8) rect('line', Mx, My, Mcw*Psx, Mch*Psy) for y=1,Mch-1 do for x=1,Mcw do if Mhw[y][x] > 0 then line(Mx+(x-1)*Psx, My+y*Psy, Mx+x*Psx, My+y*Psy) end end end for y=1,Mch do for x=1,Mcw-1 do if Mvw[y][x] > 0 then line(Mx+x*Psx, My+(y-1)*Psy, Mx+x*Psx, My+y*Psy) end end end end function draw_sprite(sprite, zoom, x,y, palette) for j,row in ipairs(sprite) do for i,cell in ipairs(row) do color(unpack(palette[cell])) rect('fill', x+(i-1)*zoom, y+(j-1)*zoom, zoom, zoom) end end end
That's it. Once you've pasted and run these screens once, you can bounce between them editing the maze or the player sprite, and see the changes immediately in the game.
One little thing that has frustrated me on touch-screen devices is that my finger often hinders my view of the gestures its performing. In this game I interact with the player using a little handle (akin to a tab or flap you might find in a pop-up book) that is otherwise invisible to the rest of the game.
Get Lua Carousel
Lua Carousel
Write programs on desktop and mobile
Status | In development |
Category | Tool |
Author | Kartik Agaram |
Tags | LÖVE |
More posts
- New version after 3 days25 days ago
- New version after 40 days28 days ago
- Turn your phone or tablet into a chess clock42 days ago
- Guest program: bouncing balls58 days ago
- New version after 3 months, and turtle graphics69 days ago
- Interactively zooming in to the Mandelbrot set on a touchscreen88 days ago
- A little timer app89 days ago
- New version after 4 monthsJun 28, 2024
- Visualizing the digits of πMay 04, 2024
Leave a comment
Log in with itch.io to leave a comment.