:PROPERTIES: :header-args: :tangle main.lua :comments no :END: #+TITLE: Literate Flicky Clone #+AUTHOR: Gonzalo Delgado * Flicky I love Flicky, it's such a tight fun arcade game. I'm going to attempt to clone it using LÖVE and literate programming to note down my ideas and plans, and also explain the code a bit. It's unlikely this will ever be published, but I'm hoping to make more games in the future this way, and improve until I'm confident enough to put them up on the interwebs. * Set up I'll start off with a small [[https://love2d.org/wiki/Config_Files][~conf.lua~ file]], specifying the LÖVE version, the game title, and that it should run in full screen by default: #+begin_src lua :tangle conf.lua function love.conf(t) t.version = "11.4" t.window.fullscreen = true t.title = "Literate Flicky Clone" end #+end_src * Dependencies I won't be using many dependencies, instead trying to code stuff on my own and using LÖVE's libraries sparingly. This is mainly because I like learning what makes games tick, but also I typically find LÖVE-related libraries hard to use and unnecessarily complicated, as well as poorly documented. That being said, I do find *inspect* to be quite useful (I believe it should be a Lua built-in.) #+begin_src lua local inspect = require("inspect/inspect") #+end_src * Classy objects I can't escape an object-oriented style, especially for games, not sure if this is a good or bad thing though I'd love to make a game in a more functional style, but the code for games made with LÖVE I read so far are all OO. I mainly need "classy objects" for inheritance, I find that to be a great way to re-use code while providing a lot of flexibility. Lua doesn't have classes though, but it does have objects with prototype capabilities through their metatable property, which allow implementing something close to inheritance. Let's create a base ~Object~ "class" which allows inheritance. ~Object~ will have a ~new~ method which will create new instances. This is achieved by first setting ~Object~ as its own ~__index~ metamethod and then setting ~Object~ as the metatable of a new, empty Lua table. This has the effect of fields not present in the new table to be looked up in ~Object~ (mainly for looking up methods). All of that is encapsulated in an ~Object:new~ function: #+begin_src lua :tangle object.lua local inspect = require("inspect/inspect") local Object = {} Object.__index = Object function Object:new(...) local object = setmetatable({__index=self}, self) object.__class = self object:init(...) return object end #+end_src ~new~ calls an ~init~ method ala Python to allow subclasses to initialize themselves taking arbitrary arguments. ~Object~'s initializer simply keep a copy of the arguments being passed in. #+begin_src lua :tangle object.lua function Object:init(...) self.__args = {...} end #+end_src Now, inheritance will be a simple interface, an ~extend~ function that takes care of creating a new "class" (a Lua table) and copying all metamethods in the parent's metatable (except ~__index~ and any metamethod it already has) to it: #+begin_src lua :tangle object.lua function Object:extend(SubClass) SubClass = SubClass or {} for k, v in pairs(self) do if k:match("^__") then SubClass[k] = v end end SubClass.__index = SubClass SubClass.__super = self return setmetatable(SubClass, self) end #+end_src Also, we'll set a handy ~__super~ reference to the parent class, so subclasses can call its methods when extending them, enabling polymorphism. I figured out enough of this on my own, but later found this written a lot better (and took lots of idead from) in different places: - [[https://github.com/eevee/klinklang/blob/master/object.lua][Eevee's klingklang engine]] - [[https://github.com/egordorichev/CurseOfTheArrow/blob/5b5c65ee2b6353324cb262422a9d2d7506911f5f/src/engine.lua#L49][Curse of the Arrow]] - [[https://hump.readthedocs.io/en/latest/class.html][Hump]] - [[https://github.com/rxi/classic][Classic]] Finally, return this for module imports: #+begin_src lua :tangle object.lua return Object #+end_src * (2D) Vector Math This will be a good project to (re)learn vector math and its application to physics stuff on videogames. Let's start with the vector class extending ~Object~: #+begin_src lua :tangle vector.lua local inspect = require("inspect/inspect") local Object = require("object") local Vector = Object:extend() #+end_src ~Vector~'s initializer will take an ~x~ and ~y~ value and store them as attributes, and also compute its angle from them. #+begin_src lua :tangle vector.lua function Vector:init(x, y) Vector.__super.init(self, x, y) self.x = x self.y = y self.angle = math.atan2(self.x, self.y) end #+end_src Next, we'll define mathematical operations by extending Lua's metamethods, to get nice stuff like ~vector1 + vector2~, ~number*vector~, ~#vector~ to get its length, etc. #+begin_src lua :tangle vector.lua function Vector:__add(v) return self.__class:new(self.x + v.x, self.y + v.y) end function Vector:__sub(v) return self.__class:new(self.x - v.x, self.y - v.y) end function Vector:__mul(obj) if getmetatable(obj) == Vector then -- dot product return self.x*obj.x + self.y*obj.y else -- scalar product return self.__class:new(self.x*obj, self.y*obj) end end function Vector:__eq(v) return self.x == v.x and self.y == v.y end function Vector:__lt(v) return self.x < v.x and self.y < v.y end function Vector:__le(v) return self.x <= v.x and self.y <= v.y end function Vector:__gt(v) return self.x > v.x and self.y > v.y end function Vector:__ge(v) return self.x >= v.x and self.y >= v.y end function Vector:__unm() return self.__class:new(-self.x, -self.y) end function Vector:__len() -- magnitude return math.sqrt(self.x^2 + self.y^2) end #+end_src #+begin_src lua :tangle vector.lua function Vector:normalize() return self.__class:new(self.x/#self, self.y/#self) end -- Method to get the clockwise normal of the vector function Vector:normal_cw() return self.__class:new(self.y, -self.x) end -- Method to get the counterclockwise normal of the vector function Vector:normal_ccw() return self.__class:new(-self.y, self.x) end return Vector #+end_src #+begin_src lua local Vector = require("vector") #+end_src * Helper functions Let's create a module to contain helper functions: #+begin_src lua :tangle helpers.lua local helpers = {} #+end_src For whatever reason Lua doesn't have a sign function 🤷, let's define our own: #+begin_src lua :tangle helpers.lua function helpers.sign(n) if n == 0 then return 0 end return math.abs(n)/n end #+end_src Same with rounding numbers, need our own function for that: #+begin_src lua :tangle helpers.lua function helpers.round(num) return num >= 0 and math.floor(num + 0.5) or math.ceil(num - 0.5) end #+end_src Let's define a ~bound~ function to keep a value within bounds #+begin_src lua :tangle helpers.lua function helpers.bound(value, min, max) if value and value < min then return min elseif value and value > max then return max else return value end end #+end_src I think a functional ~map~ function can be helpful when loading the map and similar stuff #+begin_src lua :tangle helpers.lua function helpers.map(tbl, fun) local result = {} for i, v in ipairs(tbl) do result[i] = fun(v) end return result end #+end_src But ~map~ would feel too lonely being the only functional function around, let's also add ~filter~: #+begin_src lua :tangle helpers.lua function helpers.filter(tbl, fun) local result = {} for i, v in ipairs(tbl) do if fun(v) then table.insert(result, v) end end return result end #+end_src To achieve Flicky's screen wrap-around trick (or part of it at least), we'll start with a ~normalize~ function that given a number ~n~, it returns a new ~n~ that's within a range offset the original ~n~, so if the screen's width is 100, and ~n~ is ~110~, normalizing it would result in ~10~, if that makes sense: #+begin_src lua :tangle helpers.lua function helpers.normalize(n, min, max) local range = max - min return ((n - min) % range + range) % range + min end #+end_src [[Try implementing wrap-around with polar coordinates and cosine][I'm planning on later replacing that with polar coordinates]] instead, think of levels being drawn on a cylinder, and the player seamlessly moving around it, so ~normalize~ will hopefully go away in the future. We'll need a function to tell if two rectangles overlap for basic collision detection, with rectangles being Lua tables with these field names: - x :: horizontal pixel position - y :: vertical pixel position - w :: width in pixels - h :: height in pixels #+begin_src lua :tangle helpers.lua function helpers.rectangles_overlap(rect1, rect2) local x_overlaps = rect1.x < rect2.x + rect2.w and rect1.x + rect1.w > rect2.x local y_overlaps = rect1.y < rect2.y + rect2.h and rect1.y + rect1.h > rect2.y return {x_overlaps and y_overlaps, x_overlaps, y_overlaps} end #+end_src Finally return the helpers object containing all those functions, and load it from main: #+begin_src lua :tangle helpers.lua return helpers #+end_src #+begin_src lua local helpers = require("helpers") #+end_src * Swept AABB #+begin_notes This is not being used yet, but I'm keeping it for now since I believe it'll be useful for better collision handling at high speeds, like falls, or enemies going directly at the player. #+end_notes This is what I ended up learning with more difficulty than I expected. I could probably get away with something simpler for this game, but I want to learn as much as I can. In any case, the idea here is to detect two rectangles colliding and avoid the "tunneling" problem where rectangles moving at a high enough velocity move past each other rectangles that should have collided. I read a bunch of articles, but I'm now following [[https://gamedev.net/articles/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/][one from gamedev.net]], let's try and translate to Lua: #+begin_src lua function swept_aabb(box1, box2, dt) local x_inv_entry, y_inv_entry, x_inv_exit, y_inv_exit if box1.dx > 0 then x_inv_entry = box2.x - (box1.x + box1.w) x_inv_exit = (box2.x + box2.w) - box1.x else x_inv_entry = (box2.x + box2.w) - box1.x x_inv_exit = box2.x - (box1.x + box1.w) end if (box1.dy > 0) then y_inv_entry = box2.y - (box1.y + box1.h) y_inv_exit = (box2.y + box2.h) - box1.y else y_inv_entry = (box2.y + box2.h) - box1.y y_inv_exit = box2.y - (box1.y + box1.h) end local x_entry_time, y_entry_time, x_exit_time, y_exit_time if (box1.dx == 0) then x_entry_time = -math.huge x_exit_time = math.huge else x_entry_time = x_inv_entry / box1.dx x_exit_time = x_inv_exit / box1.dx end if (box1.dy == 0) then y_entry_time = -math.huge y_exit_time = math.huge else y_entry_time = y_inv_entry / box1.dy y_exit_time = y_inv_exit / box1.dy end local entry_time = math.max(y_entry_time, x_entry_time) local exit_time = math.min(y_exit_time, x_exit_time) local normalx, normaly if (entry_time > exit_time or x_entry_time < 0 and y_entry_time < 0 or x_entry_time > 1 or y_entry_time > 1) then normalx = 0 normaly = 0 entry_time = 1 else if x_entry_time > y_entry_time then if x_inv_entry < 0 then normalx = 1 normaly = 0 else normalx = -1 normaly = 0 end else if y_inv_entry < 0 then normalx = 0 normaly = 1 else normalx = 0 normaly = -1 end end end print("SWEPT AABB TIME:", entry_time ) return {normal={x=normalx, y=normaly}, dt=entry_time} end #+end_src * Broad-Phasing collisions with solid tiles #+begin_notes This is also not wired into the game just yet, although I'm not sure this will be needed, so I may end up removing it. #+end_notes This basically comes down to finding the (solid) tiles within the rectangle determined by the initial and final positions of an entity (the player for now). First, let's write a function to get that displacement rectangle: #+begin_src lua function get_displacement_rect(entity, next_pos) local displacement_rect = { x=entity.x, y=entity.y, w=entity.w, h=entity.h, } if entity.dx > 0 then displacement_rect.x = entity.x displacement_rect.w = next_pos.x + entity.w - entity.x elseif entity.dx < 0 then displacement_rect.x = next_pos.x displacement_rect.w = entity.x + entity.w - next_pos.x end if entity.dy > 0 then displacement_rect.y = entity.y displacement_rect.h = next_pos.y + entity.h - entity.y elseif entity.dy < 0 then displacement_rect.y = next_pos.y displacement_rect.h = entity.y + entity.h - next_pos.y end return displacement_rect end #+end_src Next, we'll need a function that returns a list of rectangles representing tiles found in the displacement rectangle: #+begin_src lua function distance(point_a, point_b) return math.sqrt(math.pow(point_a.x - point_b.x, 2) + math.pow(point_a.y - point_b.y, 2)) end function find_closest_tile_in_rectangle(entity, rect, game) local closest local center = {x=entity.x + entity.w/2, y=entity.y + entity.h/2} local min_distance = math.huge local top_left_tile = game:pixel_to_tile(rect) local bottom_right_tile = game:pixel_to_tile({ x=rect.x + rect.w, y=rect.y + rect.h, }) for tx=top_left_tile.x, bottom_right_tile.x do for ty=top_left_tile.y, bottom_right_tile.y do local value = game:tile_value({x=tx, y=ty}) if value ~= 0 then local tile_center = {x=game:x_to_pixel(tx) + game.map.tilewidth/2, y=game:y_to_pixel(ty) + game.map.tileheight/2} local current_distance = distance(center, tile_center) if current_distance < min_distance then closest = {x=game:x_to_pixel(tx), y=game:y_to_pixel(ty), w=game.map.tilewidth, h=game.map.tileheight, value=value, tx=tx, ty=ty} min_distance = current_distance end end end end return closest end #+end_src * Game states I'm thinking there should be at least 4 game states: - Title screen - Play - Game Over - Ending Let's start with the base ~GameState~ class, it should keep the shared logic of initialization, updating the input state, as well as drawing to the screen: #+begin_src lua :tangle states.lua local Object = require("object") local Entity = require("entity") local Enemy = require("enemy") local Map = require("map") local helpers = require("helpers") local GameState = Object:extend() function GameState:init() GameState.__super.init(self) self.input = { start=false, right=false, left=false, up=false, down=false, jump=false, pause_key_was_released=false, } self.ended = false self.next = nil end function GameState:updateInput(dt) self.input.start = love.keyboard.isDown("return") self.input.right = love.keyboard.isDown("d", "right") self.input.left = love.keyboard.isDown("a", "left") self.input.up = love.keyboard.isDown("w", "up") self.input.down = love.keyboard.isDown("down", "s") self.input.jump = love.keyboard.isDown("space") if not (self.input.pause_key_was_released or self.input.start) then self.input.pause_key_was_released = true end end function GameState:update(dt) self:updateInput(dt) end function GameState:draw() end #+end_src The first state (for now, we may later add a splash screen or similar) the game will be in is the title screen, from which the player can start the game or go to other states like options, cheats, etc. This boils down to extending the ~GameState:update~ method to check the _start_ button is pressed, in which case it'll create the next state to switch to, ~Play~: #+name: titleState #+begin_src lua :tangle no local Title = GameState:extend() function Title:update(dt) Title.__super.update(self, dt) if self.input.start then print("PRESSED START ENDING THIS STATE") self.ended = true self.next = Play:new() end end function Title:draw() Title.__super.draw(self) local font = love.graphics.getFont() local height = font:getHeight() local width = font:getWidth("MICHI") love.graphics.setColor(1, 1, 1) love.graphics.print("MICHI", 320/2, 24, 0, 2, 2, width/2, height/2) width = font:getWidth("DRAGON") love.graphics.print("DRAGON", 320/2, 64, 0, 2, 2, width/2, height/2) width = font:getWidth("START") love.graphics.print("START", 320/2, 128, 0, 1, 1, width/2, height/2) end #+end_src So, let's define that ~Play~ state. Basically this takes the place of the old ~game~ object I started with in ~main.lua~. It'll be the biggest state and probably will be split out into sub-states of its own, but for now it'll be a single level which will eventually lead to the game over and game finished states (once deaths and level progression are implemented). Right now it will load a single level in the form of an instance of the new ~Map~ class, update the player ~Entity~ state, and draw the map and player to the screen. Also, it'll spawn a couple entities for enemies that will just move around the screen. #+name: playState #+begin_src lua :tangle no local Play = GameState:extend() function Play:init() Play.__super.init(self) self.map = Map:new("assets/map.lua") self.current_frame = 0 self.physics = { gravity=self.map.tileheight*9.8, -- somewhat exagerated gravity (3x) maxdx=self.map.tilewidth*8, -- max horizontal speed maxdy=self.map.tileheight*24, -- max vertical speed jump=self.map.tileheight*512, -- instant jump impulse } self.physics.horizontal_acc = self.physics.maxdx*2 -- horizontal acceleration self.physics.friction = self.physics.maxdx*1.5 -- horizontal friction self.player_entity = Entity:new(180, 100, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map) self.enemies = { Enemy:new(16, 24, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map), Enemy:new(224, 24, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map), Enemy:new(16, 48, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map), Enemy:new(224, 48, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map), } end function Play:updatePlayer(dt) self.player_entity = self.player_entity:move(self.input, dt, self.current_frame) end function Play:updateCamera(dt) end function Play:updateItems(dt) end function Play:updateEnemies(dt) for i, enemy in ipairs(self.enemies) do self.enemies[i] = enemy:move({start=false, right=i%2~=0, left=i%2==0, up=false, down=false, jump=self.current_frame%7==0}, dt, self.current_frame) end end function Play:updateChirps(dt) end function Play:update(dt) Play.__super.update(self, dt) self.current_frame = self.current_frame + 1 self:updatePlayer(dt) self:updateCamera(dt) self:updateItems(dt) self:updateEnemies(dt) self:updateChirps(dt) if self.input.pause_key_was_released and self.input.start then self.ended = true self.next = Pause:new(self) self.input.pause_key_was_released = false end end function Play:draw() Play.__super.draw(self) love.graphics.setColor(0.2, 0.2, 0.2) self.map:draw() love.graphics.setColor(0, 0.8, 0.1) self.player_entity:draw() for i, enemy in ipairs(self.enemies) do enemy:draw() end end #+end_src Let's define a ~Pause~ state and see how it connects to ~Play~, it'll need to keep the ~Play~ state instance so it can be resumed after unpausing: #+name: pauseState #+begin_src lua :tangle no local Pause = GameState:extend() function Pause:init(play_state) Pause.__super.init(self) self.play_state = play_state end function Pause:update(dt) Pause.__super.update(self, dt) if self.input.pause_key_was_released and self.input.start then self.input.pause_key_was_released = false self.ended = true self.play_state.ended = false self.next = self.play_state end end function Pause:draw() Pause.__super.draw(self) local pause_str = "~ PAUSE ~" local font = love.graphics.getFont() local height = font:getHeight() local width = font:getWidth(pause_str) love.graphics.setColor(1, 1, 1) love.graphics.print(pause_str, 320/2, 200/2, 0, 2, 2, width/2, height/2) end #+end_src #+begin_src lua :tangle states.lua :noweb yes <> <> <> return { Play=Play, Title=Title, } #+end_src #+begin_src lua local states = require("states") #+end_src * Debugging Let's add a function to draw some debug stuff, starting with just the player's position. I'll later set it up as a toggle with a key or similar. #+begin_src lua :tangle no function game:draw_debug_infos() love.graphics.setColor(0.1, 0.1, 0.1) love.graphics.setFont(self.debug_font) love.graphics.print("pos:("..math.floor(self.player_entity.pos.x)..", "..math.floor(self.player_entity.pos.y)..") g="..tostring(self.player_entity.grounded), 2, 2) love.graphics.print( "vel:("..math.floor(self.player_entity.vel.x)..", "..math.floor(self.player_entity.vel.y)..")", 96, 2) love.graphics.print("input:(L="..tostring(self.input.left)..",R="..tostring(self.input.right)..",U="..tostring(self.input.up)..",D="..tostring(self.input.down)..",J="..tostring(self.input.jump)..")", 2, 12) end #+end_src * Map class I'm using the venerable [[https://www.mapeditor.org/][Tiled]] to create the level(s). Tiled is so nice it exports levels as .lua files containing a big table with all the information one can need. I'll need an interface for that though, to make it more manageable and fit my style of coding, so let's create a ~Map~ class that can provide useful functionality, such as: - load the .lua file generated by Tiled - spit out [[Tile class][~Tile~ objects]] that provide information such as their location and whether their solid tiles or not - draw the whole level to the screen - query tiles by pixel position Of course, a ~Map~ will be an ~Object subclass: #+begin_src lua :tangle map.lua local Object = require("object") local Tile = require("tile") local helpers = require("helpers") local inspect = require("inspect/inspect") local Map = Object:extend() #+end_src The map will be initialized with just the path to the Tiled Lua file, which it'll load and get some metadata from. Mainly, it'll look up a "platforms" layer, which is where I'm laying out the blocks that make up a level's platforms. #+begin_src lua :tangle map.lua function Map:init(filename) Map.__super.init(self) local map = love.filesystem.load(filename)() local platform_layers = helpers.filter(map.layers, function (layer) return layer.name == "platforms" end) self.width = map.width self.height = map.height self.tilewidth = map.tilewidth self.tileheight = map.tileheight self.data = platform_layers[1].data self.max_pixel = {x=map.width*map.tilewidth, y=map.height*map.tileheight} end #+end_src We'll need a way to iterate through each of the platform tiles of a level. Tiled provides a single-dimension array representing each tile on the map. Each element in the array is simply a number representing the tile's index (to be mapped to a spritesheet), with 0 meaning "no tile here". We'll translate that to instances of our [[Tile class][~Tile~ class]] which will use that value for its ~solid~ attribute. I tried using the ~__ipairs~ metamethod to get a nice ~for i, tile in ipairs(map) do~ bit of code, but turns out ~__ipairs~ was deprecated and removed from Lua, bummer. Instead, we'll simply have a ~itertiles~ method that will do the same: #+begin_src lua :tangle map.lua function Map:itertiles() local function iterator(map, index) index = index + 1 local value = map.data[index] if value ~= nil then local tx = ((index - 1) % map.width) + 1 local ty = math.floor((index - 1) / map.width) + 1 local tile = Tile:new(tx, ty, map.tilewidth, map.tileheight, value) return index, tile end end return iterator, self, 0 end #+end_src Having ~Map:itertiles~ makes drawing the map to the screen super simple: #+begin_src lua :tangle map.lua function Map:draw() for i, tile in self:itertiles() do love.graphics.setColor(0.9, 0.8, 0.7) if tile.solid then love.graphics.setColor(0.2, 0.2, 0.2) end love.graphics.rectangle("fill", tile.pixel_rect.x, tile.pixel_rect.y, tile.w, tile.h) love.graphics.setColor(0.9, 0.1, 0.3) love.graphics.rectangle("line", tile.pixel_rect.x, tile.pixel_rect.y, tile.w, tile.h) end end #+end_src Finally, we'll need the map to provide the tile for a given pixel position on the screen. This will be useful for how I'm doing collision detection. #+begin_src lua :tangle map.lua function Map:get_tile_from_pixel(pixel) local tile_x = math.floor(pixel.x/self.tilewidth) + 1 local tile_y = math.floor(pixel.y/self.tileheight) + 1 local value = self.data[tile_x + (tile_y - 1)*self.width] return Tile:new(tile_x, tile_y, self.tilewidth, self.tileheight, value) end return Map #+end_src * Tile class Tiles will be represented as very simple objects consisting of basic attributes such as: - width and height - tile-space position (not pixel) - whether their solid or not I'm making tiles be vectors in order to easily find adjacent tiles by adding or subtracting unit vectors. #+begin_src lua :tangle tile.lua local Vector = require("vector") local Tile = Vector:extend() #+end_src The initializer is otherwise simple, storing the arguments and creating a ~pixel_rect~ table to easily map a tile to its pixels on the screen. #+begin_src lua :tangle tile.lua function Tile:init(x, y, w, h, value) Tile.__super.init(self, x, y) self.w = w self.h = h self.pixel_rect = { x=(x - 1)*self.w, y=(y - 1)*self.h, w=self.w, h=self.h, } self.value = value self.solid = not (value == 0 or value == nil) end return Tile #+end_src * Entity class If my mind's bandwidth allows, I may also learn some ECS, so let's start with an entity class that has position and velocity (which should later become components?). In any case, an entity will have these attributes: - ~pos~ :: the x and y coordinates of the entity's *center* - ~vel~ :: the entity's velocity vector - ~acc~ :: the entity's acceleration vector - ~rect~ :: the entity's bounding rect, with ~x~ and ~y~ for the top left corner, and ~w~ and ~h~ for its width and height, respectively Also, I want to create an API to allow controlling any entity's actions, namely: - walk - jump - attack - draw All of that while having gravity always affect an entity. I'm thinking of starting off with walking and jumping, for which an entity object would provide ~move~ and ~jump~ methods respectively. The ~move~ method in particular would take a ~movement~ object, which specifies in which direction it's moving (left or right) or jumping, which is basically the game's ~input~ object. #+begin_src lua :tangle entity.lua local Object = require("object") local Vector = require("vector") local Tile = require("tile") local helpers = require("helpers") local inspect = require("inspect/inspect") local Entity = Object:extend() function Entity:init(x, y, w, h, gravity, max_jump_height, horizontal_acc, friction, map) Entity.__super.init(self, x, y, w, h) self.gravity = gravity self.pos = Vector:new(x, y) self.vel = Vector:new(0, 0) self.maxvel = Vector:new(8*8, 8*24) self.width = w self.height = h self.grounded = false self.horizontal_acc = horizontal_acc self.max_jump_height = max_jump_height self.friction = friction self.map = map end function Entity:get_rect() return {x=self.pos.x - self.width/2, y=self.pos.y - self.height/2, w=self.width, h=self.height} end function Entity:draw() local rect = self:get_rect() love.graphics.setColor(0, 0, 0.8) love.graphics.rectangle("fill", helpers.round(rect.x), helpers.round(rect.y), rect.w, rect.h) love.graphics.setColor(1, 0, 0) love.graphics.circle("fill", self.pos.x, self.pos.y, 1) end #+end_src Each frame, we'll want to update an entity's state. This means updating its velocity and position based on its previous state, and then adjusting that next state if there are collisions that would prevent actually moving there. The first step is to create a copy of the current state so we work on that rather than the actual entity, thus, let's define a ~copy~ method: #+begin_src lua :tangle entity.lua function Entity:copy() local new = self.__class:new(self.pos.x, self.pos.y, self.width, self.height, self.gravity, self.max_jump_height, self.horizontal_acc, self.friction, self.map) new.vel.x = self.vel.x new.vel.y = self.vel.y new.grounded = self.grounded return new end #+end_src Next, let's define the ~move~ method, which takes a movement object (basically, the player's input), and the time delta since the last frame, and will create a copy, update its velocity and position, check for collisions, and return a new entity with the updated state. First, let's do the horizontal movement (walking or flying?) #+begin_src lua :tangle entity.lua -- TODO: check if not grounded and allow air control function Entity:move_horizontally(movement, dt) local x_accel = 0 local next_state = self:copy() if movement.right or movement.left then if movement.right then x_accel = self.horizontal_acc else x_accel = -self.horizontal_acc end else if self.vel.x < 0 then x_accel = self.friction elseif self.vel.x > 0 then x_accel = -self.friction end end next_state.vel.x = helpers.bound(self.vel.x + x_accel*dt, -self.maxvel.x, self.maxvel.x) next_state.pos.x = helpers.normalize(self.pos.x + next_state.vel.x*dt, self.width/2, self.map.max_pixel.x + self.width*1.5) if self.vel.x < 0 and next_state.vel.x > 0 or self.vel.x > 0 and next_state.vel.x < 0 then next_state.vel.x = 0 end return next_state end #+end_src Next, let's handle jumps with a method that assumes the player is allowed to jump, i.e. is grounded. #+begin_src lua :tangle entity.lua function Entity:jump(dt) local next_state = self:copy() next_state.grounded = false next_state.vel.y = -math.sqrt(2*self.max_jump_height*self.gravity) next_state.pos.y = helpers.bound(next_state.pos.y + next_state.vel.y*dt, 0, self.map.max_pixel.y - self.height/2) return next_state end #+end_src When the entity isn't grounded and isn't jumping, it's falling: #+begin_src lua :tangle entity.lua function Entity:fall(dt) local next_state = self:copy() next_state.grounded = false next_state.vel.y = self.vel.y + self.gravity*dt next_state.pos.y = helpers.bound(next_state.pos.y + next_state.vel.y*dt, 0, self.map.max_pixel.y - self.height/2) return next_state end #+end_src Lastly, we also need a way to /ground/ the entity, setting the ~grounded~ flag to ~true~, and its vertical velocity to 0. #+begin_src lua :tangle entity.lua function Entity:ground() local next_state = self:copy() next_state.grounded = true next_state.vel.y = 0 return next_state end #+end_src We'll need to check for collisions against solid tiles and adjust the entities position: #+begin_src lua :tangle entity.lua function Entity:adjust_for_collisions(old_state, dt, frame) local next_state = self:copy() local top_left_tile = self.map:get_tile_from_pixel({ x=next_state.pos.x - next_state.width/2, y=next_state.pos.y - next_state.height/2, }) local bottom_left_tile = self.map:get_tile_from_pixel({ x=next_state.pos.x - next_state.width/2, y=next_state.pos.y + next_state.height/2, }) local bottom_right_tile = self.map:get_tile_from_pixel({ x=next_state.pos.x + next_state.width/2, y=next_state.pos.y + next_state.height/2, }) local top_right_tile = self.map:get_tile_from_pixel({ x=next_state.pos.x + next_state.width/2, y=next_state.pos.y - next_state.height/2, }) local next_rect = next_state:get_rect() if old_state.grounded and self.vel.y >= 0 then if self.vel.x < 0 and not bottom_left_tile.solid or self.vel.x > 0 and not bottom_right_tile.solid then next_state = next_state:fall(dt) end end if not self.grounded then if self.vel.y < 0 and ( top_left_tile.solid and helpers.rectangles_overlap(next_rect, top_left_tile.pixel_rect)[2] or top_right_tile.solid and helpers.rectangles_overlap(next_rect, top_right_tile.pixel_rect)[2] ) then next_state.pos.y = top_right_tile.y + top_right_tile.h - self.height/2 next_state.vel.y = 0 print(frame, "CEILING BUMP!") elseif self.vel.y >= 0 and ( bottom_left_tile.solid and helpers.rectangles_overlap(next_rect, bottom_left_tile.pixel_rect)[3] or bottom_right_tile.solid and helpers.rectangles_overlap(next_rect, bottom_right_tile.pixel_rect)[3] ) then next_state = next_state:ground() print(frame, "GROUNDED BECAUSE OF LANDING") next_state.pos.y = bottom_right_tile.pixel_rect.y - self.height/2 end end if old_state.vel.x < 0 then if top_left_tile.solid and helpers.rectangles_overlap(next_rect, top_left_tile.pixel_rect)[2] then next_state.vel.x = 0 next_state.pos.x = top_left_tile.pixel_rect.x + top_left_tile.pixel_rect.w + next_rect.w/2 end elseif old_state.vel.x > 0 then if top_right_tile.solid and helpers.rectangles_overlap(next_rect, top_right_tile.pixel_rect)[2] then next_state.vel.x = 0 next_state.pos.x = top_right_tile.pixel_rect.x - next_rect.w/2 end end if next_state.grounded and next_state.vel.y ~= 0 then print(frame, "GROUNDED WITH VERTICAL VELOCITY", next_state.vel.y, "THIS SHOULD NOT HAPPEN") next_state.vel.y = 0 end return next_state end #+end_src #+begin_src lua :tangle entity.lua function Entity:move(movement, dt, frame) local wasright = self.vel.x > 0 local wasleft = self.vel.x < 0 local justjumped = false local accel = Vector:new(0, self.gravity) local next_state = self:move_horizontally(movement, dt) if self.grounded then if movement.jump then next_state = next_state:jump(dt) end else next_state = next_state:fall(dt) end if not (next_state.pos == self.pos) then if distance(self.pos, next_state.pos) > 8 then print(frame, "ENTITY MOVING TOO FAST!", inspect(self.pos), inspect(next_state.pos)) -- swept AABB else next_state = next_state:adjust_for_collisions(self, dt, frame) end end return next_state end return Entity #+end_src #+begin_src lua local Entity = require("entity") #+end_src * Enemy class Now that we have the ~Entity~ class, let's extend it for enemies. Initially this will only override the ~draw~ method to use a different color, but later we'll add more specific behavior (as well as different sprites) and maybe even more subclasses for each enemy type. #+begin_src lua :tangle enemy.lua local helpers = require("helpers") local Entity = require("entity") local Enemy = Entity:extend() function Enemy:draw() self.__super.draw(self) local rect = self:get_rect() love.graphics.setColor(0.9, 0, 0.2) love.graphics.rectangle("fill", helpers.round(rect.x), helpers.round(rect.y), rect.w, rect.h) end return Enemy #+end_src * Initialization Next, we define ~love.load()~, which should take care of: - loading graphics, setting the scale - setting up animations - loading the level and mapping it into data in the ~game~ object. - initialize the player and other objects - this will be moved to a level initialization function later on I want a retro look, so pixels should look sharp, I could use a library like maid64, but I'll just keep things simple by: - setting the nearest neighbor interpolation as the default scaling filter - calculate the scaling factors for width and height, which will be used when drawing stuff #+name: initGraphics #+begin_src lua :tangle no local screen_width = 320 --self.map.width*self.map.tilewidth local screen_height = 200 -- self.map.height*self.map.tileheight love.graphics.setDefaultFilter("nearest", "nearest") local desktop_width, desktop_height = love.window.getDesktopDimensions() scale_width = desktop_width/screen_width scale_height = desktop_height/screen_height #+end_src Next, all we need to do is create the first game state players will see, which is the title screen. All the logic for switching to other states is self-contained: #+name: initGameState #+begin_src lua :tangle no current_state = states.Title:new() #+end_src Putting it all together in LÖVE's ~load~ callback: #+begin_src lua :noweb yes local scale_width, scale_height, current_state function love.load() <> <> end #+end_src * Game logic Flicky's gameplay seems basic at first, but there's quite a bit of stuff going on: - controls :: move left and right, jump - objective :: collect "Chirps" and take them to the exit - Chirps will follow Flicky around until then - enemies :: cats move around the level (there's other types of enemies in later levels) - if they touch Flicky, a life is lost - if they touch a Chirp, it stops following Flicky - enemies respawn from fixed points - enemies can be taken down if Flicky kicks throwable items like flower pots at them - levels wrap around, giving the illusion of a bigger, infinite level This means the game logic should keep track of a few things: - Flicky's state - position - probably other physics stuff too, like velocity, etc. - lives - whether Flicky is carrying an item or not (maybe just an item attribute that can be ~nil~) - Camera's position, based on Flick's position - Chirps' position, and whether they're following Flicky - I'm thinking Chirps could be small state machines - Level-specific stuff - Initial enemies position - Enemy spawner positions - How many enemies are active - Maximum allowed enemies - Platforms - Game state stuff - Collected chirps count - Timer At the most basic (i.e. the whole game being a single level) that results in a few main functions ~love.update(dt)~ will call: * Updating each frame At the highest level, updating each frame is pretty simple: - update the current game state - check if the current state has ended - if it has, switch to its next state #+begin_src lua function love.update(dt) current_state:update(dt) if current_state.ended then current_state = current_state.next end end #+end_src * Drawing to the screen First up, let's reset the color every time we draw so it doesn't tint everything with the color that was used last: #+name: resetColor #+begin_src lua :tangle no love.graphics.setColor(1, 1, 1) #+end_src Go through each tile in the map and query its value to draw solid tiles (value > 0) in color (this will later be replaced by drawing the actual tile graphics form the tilesheet.) We'll also do scaling using the calculated scale factors from the game object: game:draw_debug_infos() #+end_src #+begin_src lua :noweb yes function love.draw() <> love.graphics.push() love.graphics.scale(scale_width, scale_height) current_state:draw() love.graphics.pop() end #+end_src * Ideas I'm thinking once I have the core of the game working I can try introducing some throwing mechanics like *Yoshi's Story*. Either throw the chicks/eggs themselves, or have some special pickup that can be thrown at enemies. * Devlog ** [2024-08-17 sáb] Not a huge amount of work this week, but feeling good about it, noteworthy stuff so far: - broke up entity movement and collision detection into separate methods - jumping feels nice, and got the code layed out so I can set whatever max height I want - collision with tiles still not perfect, but we're getting there Next week I want to work on: - game states (at least get a menu/title screen) - add other entities to the game with stupid AI - stretch: some pixel art ** [2024-09-02 lun] I was able to work on game states but got detoured with making a ~Map~ class that would allow iterating over instances of it returning each tile that makes up the map, and got into a Lua iterators rabbit hole which ended up finding out ~__ipairs~ is no longer supported (for quite a while), so I had to just make a plain iterator method (that led me to think *a lot* about my choosing Lua for this.) In any case, I'm glad I was able to stub out the title screen along with the game states architecture. The game is currently not working, but this is what I did this week: - ~GameState~ class - Move gameplay code into ~Play~ ~GameState~ subclass - Implement ~Title~ ~GameState~ subclass, working with input to switch to ~Play~ state - Clean up a bunch of old code - ~Map~ class to encapsulate tile logic, with iterator yielding ~Tile~ instances - Got working title idea: "Michi Dragon" - Started stubbing out title screen art This week I'd like to: - get game working again (fix map/tiles code) - work on simple title screen art - update literary parts for all the code I added last week ** [2024-09-24 mar] Hadn't realized it's been such a while since I added a devlog, I'll start making specific commits for these. I actually got a good rhythm going on now that some of the big ground-level stuff is working somewhat decently. These are the latest updates: - Game is working again! - Game states themselves work nicely, there's a simple title screen from which the main play state can be accessed - Updated and added a bunch of "literary" parts - Added missing files to git - Entities no longer endlessly bump with tiles when moving horizontally - Started off with an ~Enemy~ class and added a few "enemies" moving around Next up I'd love to work on these: - collisions between entities (enemy/player) - game over state - pause (sub)state - some pixel art for heck's sake * Plan ** TODO Install dependencies (inspect, debugger) through ~luarocks~ ** DONE Create object base "class" with copy metamethods functionality, explain literarily ** DONE Refactor ~Object:new~, ~Object:init~, introduce ~Object:extend~ ** TODO Convert acceleration to vector in ~Entity:move~ ** DONE Fix gravity always changing position in y axis on ~Entity:move~ :bug: SCHEDULED: <2024-08-08 jue 14:00-15:00> DEADLINE: <2024-08-09 vie> :LOGBOOK: CLOCK: [2024-08-07 mié 11:42]--[2024-08-07 mié 12:11] => 0:29 :END: ** DONE Add ~grounded~ attribute to player/entity ** DONE ~love.load()~ ** DONE [#A] Bugfix drawing offset by 1 in x and y ** DONE ~love.update(dt)~ ** DONE ~love.draw()~ Explain ~game.map:box2d_draw~ call for debugging purposes. ** DONE Level wrap-around ** DONE Bugfix level wrap-around Can't move past right side of the level, but going to the left works (?) ** DONE Update "literary" parts DEADLINE: <2024-09-11 mié> :LOGBOOK: CLOCK: [2024-09-10 mar 07:08]--[2024-09-10 mar 07:45] => 0:37 :END: ** TODO Player slide Tweak friction values ** TODO changing direction while in mid air should have lots of friction ** DONE Bugfix ~Enemy:draw~ not working SCHEDULED: <2024-10-02 mié 12:15-12:45> DEADLINE: <2024-09-30 lun> :LOGBOOK: CLOCK: [2024-10-02 mié 14:34]--[2024-10-02 mié 14:39] => 0:05 :END: I think the problem is actually with ~Enemy:copy~ returning entities instead of enemy objects. ** DONE Refactor player, enemies, items code into entities ** DONE Add a couple enemy entities with stupid AI DEADLINE: <2024-09-23 lun> :LOGBOOK: CLOCK: [2024-09-24 mar 08:12]--[2024-09-24 mar 08:51] => 0:39 :END: maybe they just move left or right constantly, jump if they "see" a gap or blocking tile ahead. ** TODO Title screen art mock DEADLINE: <2024-09-27 vie> :LOGBOOK: CLOCK: [2024-09-04 mié 11:45]--[2024-09-04 mié 12:08] => 0:23 CLOCK: [2024-08-20 mar 11:39]--[2024-08-20 mar 12:06] => 0:27 :END: fonts I like: - Essays - dhurjati (maybe for a shmup) - lmmono10-italic - lmromanusl10-regular - lmu10 - +qzcmi+ - +texgyrechorus-med+ ** TODO Gameplay screen art mock DEADLINE: <2024-10-02 mié> ** DONE Fix Tile class ~ipairs~ SCHEDULED: <2024-08-29 jue 10:30-11:30> DEADLINE: <2024-08-30 vie> :LOGBOOK: CLOCK: [2024-08-29 jue 10:42]--[2024-08-29 jue 11:14] => 0:32 :END: Turns out Lua no longer supports the ~__ipairs~ metamethod (hasn't for years), bummer. ** DONE Collision with platforms above when jumping ** TODO Camera fixed on player position Bonus points for a small offset left and right before moving camera position. ** DONE Implement game states DEADLINE: <2024-08-30 vie> :LOGBOOK: CLOCK: [2024-08-21 mié 07:26]--[2024-08-21 mié 07:57] => 0:31 :END: - [ ] Title/Menu - [ ] Gameplay - [ ] Game Over - [ ] Finished game ** TODO Explain ~game~ object components ** TODO Refine player movement ** TODO Bugfix scaling and/or drawing ** DONE Maybe create a game class, player class, etc. ** TODO Explain 320x200 resolution ** TODO Fix aspect ratio Using 16:10, should add "black bars" on the sides (or on top and bottom) But how? ** DONE Bugfix jump code Jumping is somewhat inconsistent, if you hold jump, final jump height varies quite a bit. Got this one fixed by setting a fixed Y velocity when jump button is hit, rather than calculating a new acceleration with ~dt~. (Should explain this in the literate part) ** TODO Set up some kind of debug mode (toggle?) ** TODO Define enemy spawner logic (a few different options) ** DONE Map player position correctly when drawing ** TODO Create lower-resolution internal game model ** DONE Consider dropping ~game.world~ and not using STI's box2d support I'm following Code inComplete's Tiny Platformer article, coding the physics from scratch somewhat. Maybe start off like that and later try to refactor into LOVE's physics? ** Draw debug helpers - [ ] draw player's "tile" ** DONE Draw platforms from ~game.platforms~ table ** TODO slide animation when direction button is released ** TODO Generic ~Object:copy~ method ** TODO Decouple ~GameState~ objects, map (maybe), and graphics ** DONE Fix collision with solid tile when moving horizontally SCHEDULED: <2024-09-19 jue 15:45-16:30> DEADLINE: <2024-09-21 sáb> :LOGBOOK: CLOCK: [2024-09-19 jue 19:04]--[2024-09-19 jue 19:24] => 0:20 CLOCK: [2024-09-19 jue 08:12]--[2024-09-19 jue 08:58] => 0:46 CLOCK: [2024-08-13 mar 12:01]--[2024-08-13 mar 12:13] => 0:12 :END: 289 DETECTED COLLISION WHEN MOVING LEFT TOWARDS 200 NEXT X: 199.17442941018 CURRENT VEL.X -64 NEXT RECT { h = 8, w = 4, x = 197.17442941018, y = 184 } TILE RECT { h = 8, w = 8, x = 192, y = 184 } #+begin_notes Seems it's detecting the wrong tile in the next frame after a collision detection? Need to check ~next_state.pos~ and resulting ~top_left_tile~ for this case. #+end_notes 289 ADJUSTED X 202 291 NO COLLISION WHEN MOVING LEFT TOWARDS 208 NEXT X: 201.85257872008 CURRENT VEL.X -1.7781952 NEXT RECT { h = 8, w = 4, x = 199.85257872008, y = 184 } ** DONE Implement game pause as ~Play~ substate DEADLINE: <2024-09-24 mar> :LOGBOOK: CLOCK: [2024-10-02 mié 14:40]--[2024-10-02 mié 15:01] => 0:21 :END: It's kinda working but I need to debounce it. ** TODO replace hardcoded player initial position (180, 100) with marker/point from Tiled map ** TODO Reimplement ~draw_debug_infos~ DEADLINE: <2024-09-20 vie> Need something more global, maybe that takes a game state instance or something. ** DONE Fix jump SCHEDULED: <2024-08-13 mar 11:15-12:00> ** TODO Bugfix ceiling bump teleport DEADLINE: <2024-10-07 lun> ** TODO Try implementing wrap-around with polar coordinates and cosine DEADLINE: <2024-10-02 mié> #+begin_src lua :tangle angularwrap.lua -- Constants local radius = 100 -- Radius of the "circle" that represents the screen width local max_velocity = 4 * math.pi -- Max angular velocity (2 full circle per second) local acceleration = 0.2 -- Angular acceleration -- Initial state local theta = 0 -- Initial angle (in radians) local angular_velocity = 0 -- Initial angular velocity local delta_time = 1/30 -- Time step (e.g., 1/30th of a second) -- Function to update the entity's position function update_position() -- Apply angular acceleration angular_velocity = angular_velocity + acceleration * delta_time -- Cap the angular velocity at max_velocity if angular_velocity > max_velocity then angular_velocity = max_velocity end -- Update the angle (theta) theta = theta + angular_velocity * delta_time -- Ensure theta stays within [0, 2 * pi] to simulate wrap-around if theta >= 2 * math.pi then theta = theta - 2 * math.pi elseif theta < 0 then theta = theta + 2 * math.pi end -- Calculate the horizontal position using the cosine of the angle local x_position = radius * math.cos(theta) return x_position end -- Example usage for i = 1, 60 do local x = update_position() print(string.format("Time: %.2f, X Position: %.2f", i * delta_time, x)) end #+end_src