flickyclone/flickyclone.org
2024-08-17 09:13:37 -03:00

53 KiB
Raw Blame History

Literate Flicky Clone

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 conf.lua file, specifying the LÖVE version, the game title, and that it should run in full screen by default:

  function love.conf(t)
     t.version = "11.4"
     t.window.fullscreen = true
     t.title = "Literate Flicky Clone"
  end

Dependencies

I won't be using any dependencies, instead coding 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.)

  local inspect = require("inspect/inspect")

Classy objects

Lua doesn't have classes, 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:

  local inspect = require("inspect/inspect")
  local Object = {}
  Object.__index = Object

  function Object:new(...)
     local object = setmetatable({__index=self}, self)
     object:init(...)
     object.__class = self
     return object
  end

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.

    function Object:init(...)
  	 self.__args = {...}
    end

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:

  function Object:extend()
     local SubClass = {}
     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

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:

Finally, return this for module imports:

  return Object

(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:

  local inspect = require("inspect/inspect")
  local Object = require("object")

  local Vector = Object:extend()

Vector's initializer will take an x and y value and store them as attributes, and also compute its angle from them.

  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

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.

  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
  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
  local Vector = require("vector")

Helper functions

Let's create a module to contain helper functions:

  local helpers = {}

For whatever reason Lua doesn't have a sign function 🤷, let's define our own:

  function helpers.sign(n)
     if n == 0 then
  	  return 0
     end
     return math.abs(n)/n
  end

Same with rounding numbers, need our own function for that:

  function helpers.round(num)
     return num >= 0 and math.floor(num + 0.5) or math.ceil(num - 0.5)
  end

Let's define a bound function to keep a value within bounds

  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

I think a functional map function can be helpful when loading the map and similar stuff

  function helpers.map(tbl, fun)
     local result = {}
     for i, v in ipairs(tbl) do
  	  result[i] = fun(v)
     end
     return result
  end

But map would feel too lonely being the only functional function around, let's also add filter:

  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

To achieve Flicky's screen wrap-around trick (or part of it at least), we need 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:

  function helpers.normalize(n, min, max)
     local range = max - min
     return ((n - min) % range + range) % range + min
  end

Finally return the helpers object containing all those functions, and load it from main:

  return helpers
  local helpers = require("helpers")

Swept AABB

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 one from gamedev.net, let's try and translate to 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

Broad-Phasing collisions with solid tiles

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:

  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

Next, we'll need a function that returns a list of rectangles representing tiles found in the displacement rectangle:

  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

Game object

We'll have a game object that will hold all the state of the game and provide some functions to modify it. Let's start with an empty object.

  local game = {
     current_frame = 0,
  	 input = {
  		right=false,
  		left=false,
  		up=false,
  		down=false,
  		jump=false,
  	 }
  }

Input

The game object will keep the state of the input. I'm adding this right now mainly for debugging purposes, but it'll be good to keep that in its own place.

  function game:updateInput(dt)
	 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")
  end

Map & Base game data

Let's add a function to the game object to load a Tiled map from a .lua file. In the tiled map, the platforms are non-empty tiles. Since the layer format I chose is CSV, I get a 1-dimensional array (a table in Lua) with numbers representing each tile on the map, with 0 being an empty tile, and any other number representing the graphic for that tile from the tilesheet.

Also, the map data contains tile sizes and the map size from which we define the (virtual) screen size, as well as some physics values.

  function game:loadWorldMap(filename)
	 self.map = dofile(filename)
	 local platform_layers = helpers.filter(self.map.layers, function (layer) return layer.name == "platforms" end)
	 self.platforms = platform_layers[1].data
	 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
  end

The map has the tile sizes, so it makes sense that these utility functions are defined as methods of the game object (who will also be their main user):

  function game:pixel_to_tile(pixel)
	 return {
		x=math.floor(pixel.x/self.map.tilewidth) + 1,
		y=math.floor(pixel.y/self.map.tileheight) + 1,
	 }
  end

  function game:x_to_pixel(tile_x)
	 return (tile_x - 1)*self.map.tilewidth
  end

  function game:y_to_pixel(tile_y)
	 return (tile_y - 1)*self.map.tileheight
  end

  function game:tile_to_pixel(tile)
	 local pixel = {
		x=self:x_to_pixel(tile.x),
		y=self:y_to_pixel(tile.y),
	 }
	 return pixel
  end

  function game:tile_to_rect(tile)
	 local pixel = self:tile_to_pixel(tile)
	 return {
		x=pixel.x,
		y=pixel.y,
		w=self.map.tilewidth,
		h=self.map.tileheight,
	 }
  end

We'll also 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
  function 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

tile_value (defined below) deserves a quick explanation: 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".

Since the array is one-dimensional, we need to map a tile with coordinates to this array to be able to obtain its value, hence:

  function game:tile_value(tile)
	 return self.platforms[tile.x + (tile.y - 1)*self.map.width]
  end

Graphics

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

Our game object will provide a method for taking care of that:

  function game:initializeGraphics()
	 local screen_width = self.map.width*self.map.tilewidth
	 local screen_height = self.map.height*self.map.tileheight
	 love.graphics.setDefaultFilter("nearest", "nearest")
	 local desktop_width, desktop_height = love.window.getDesktopDimensions()
	 self.scale_width = desktop_width/screen_width
	 self.scale_height = desktop_height/screen_height
	 self.screen_width = screen_width
	 self.screen_height = screen_height
	 self.debug_font = love.graphics.newFont(8, "normal", 2)
  end

The game's scale_width and scale_height attributes will be used whenever anything needs to be drawn.

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.

  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

Tile class

  local inspect = require("inspect/inspect")
  local Vector = require("vector")
  local helpers = require("helpers")

  local map = dofile("map.lua")
  local tile_layers = helpers.filter(map.layers, function (layer) return layer.name == "platforms" end)
  local tile_data = tile_layers[1].data

  local Tile = Vector:extend()
  Tile.max_pixel = {x=map.width*map.tilewidth, y=map.height*map.tileheight}

  function Tile:init(x, y)
  	 Tile.__super.init(self, x, y)
  	 local value = tile_data[x + (y - 1)*map.width]
  	 self.w = map.tilewidth
  	 self.h = map.tileheight
  	 self.pixel_rect = {
  		x=(x - 1)*map.tilewidth,
  		y=(y - 1)*map.tileheight,
  		w=map.tileheight,
  		h=map.tilewidth,
  	 }
  	 self.value = value
  	 self.solid = value ~= 0
  end

  function Tile:from_pixel(pixel)
  	 return Tile:new(math.floor(pixel.x/map.tilewidth) + 1,	math.floor(pixel.y/map.tileheight) + 1)
  end

  function Tile:get_pixel_rect()
  	 local rect = self.pixel_rect
  	 rect.w = map.tilewidth
  	 rect.h = map.tileheight
  	 return rect
  end

  return Tile
  local Tile = require("tile")

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.

  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)
     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
  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

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:

  function Entity:copy()
     local new = Entity:new(self.pos.x, self.pos.y, self.width, self.height, self.gravity, self.max_jump_height, self.horizontal_acc, self.friction)
     new.vel.x = self.vel.x
     new.vel.y = self.vel.y
     new.grounded = self.grounded
     return new
  end

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?)

  -- 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, Tile.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

Next, let's handle jumps with a method that assumes the player is allowed to jump, i.e. is grounded.

  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, Tile.max_pixel.y - self.height/2)
     return next_state
  end

When the entity isn't grounded and isn't jumping, it's falling:

  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, Tile.max_pixel.y - self.height/2)
     return next_state
  end

Lastly, we also need a way to ground the entity, setting the grounded flag to true, and its vertical velocity to 0.

  function Entity:ground()
     local next_state = self:copy()
     next_state.grounded = true
     next_state.vel.y = 0
     return next_state
  end

We'll need to check for collisions against solid tiles and adjust the entities position:

  function Entity:adjust_for_collisions(old_state, dt, frame)
     local next_state = self:copy()
     local top_left_tile = Tile:from_pixel(next_state.pos)
     local bottom_left_tile = top_left_tile + Tile:new(0, 1)
     local bottom_right_tile = top_left_tile + Tile:new(1, 1)
     local top_right_tile = top_left_tile + Tile:new(1, 0)

     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 rectangles_overlap(next_rect, top_left_tile.pixel_rect)[2]
  		 or
  		 top_right_tile.solid and 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 rectangles_overlap(next_rect, bottom_left_tile.pixel_rect)[3]
  		 or
  		 bottom_right_tile.solid and 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 rectangles_overlap(next_rect, top_left_tile.pixel_rect)[2] then
  		 print(frame, "DETECTED COLLISION WHEN MOVING LEFT TOWARDS", top_left_tile.pixel_rect.x + top_left_tile.pixel_rect.w, "NEXT X:", next_state.pos.x, "CURRENT VEL.X", old_state.vel.x)
  		 next_state.vel.x = 0
  		 next_state.pos.x = math.ceil(top_left_tile.pixel_rect.x + top_left_tile.w + next_state.width/2) -1
  		 print(frame, "ADJUSTED X", next_state.pos.x)
  	  end
     elseif old_state.vel.x > 0 then
  	  if top_right_tile.solid and 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 - self.width*1.5
  	  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
  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
  local Entity = require("entity")

Player

Let's use the entity class to set up a player object that tracks its position and velocity.

  function game:initPlayer(x, y)
     self.player_entity = Entity:new(x, y, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction)
  end

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

First, we'll load the map which also contains physics and screen data we'll need later on, using the method we defined earlier:

  game:loadWorldMap("map.lua")

To initialize the graphics we simply call the method we defined for the game object:

  game:initializeGraphics()

Let's place the player somewhere in the level:

  game:initPlayer(180, 100)

Putting it all together in LÖVE's load callback:

  function love.load()
	<<loadMap>>
	<<initializeGraphics>>
	<<initPlayer>>
  end

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:

  function love.update(dt)
     game.current_frame = game.current_frame + 1
  	 game:updateInput(dt)
  	 game:updatePlayer(dt)
  	 game:updateCamera(dt)
  	 game:updateItems(dt)
  	 game:updateEnemies(dt)
  	 game:updateChirps(dt)
  end

updatePlayer should take care of a few basic things:

  • moving the player left or right when the A/left arrow or S/right arrow keys are pressed
  • applying momentum and friction to the player
  • making the player jump when the jump button/key is hit
  • applying gravity to the player
  • checking collisions

First, let's check the current physics and input state:

  local wasleft = self.player_entity.vel.x < 0
  local wasright = self.player_entity.vel.x > 0
  local wasjumping = self.player_entity.vel.y < 0
  local wasfalling = self.player_entity.vel.y > 0

Based on those values, we'll want to calculate the next position of the player, but not update it just yet:

	local ddx = 0
	local ddy = self.physics.gravity
	local next_p = self.player_entity:copy()
	local player_rect = self.player_entity:get_rect()
	if self.input.right then
	   ddx = self.physics.accel
	elseif wasright then
	   ddx = -self.physics.friction
	end

	-- if self.input.up then
	--    next_dy = -48
	-- end
	-- if self.input.down then
	--    next_dy = 48
	-- end

	if self.input.left then
	   ddx = -self.physics.accel
	elseif wasleft then
	   ddx = self.physics.friction
	end
	if self.player_entity.grounded then
	   if self.input.jump and not wasjumping then
		  next_p.vel.y = -self.physics.jump
		  self.player_entity.grounded = false
	   end
	else
	   next_p.vel.y = helpers.bound(self.player_entity.vel.y + ddy*dt, -self.physics.maxdy, self.physics.maxdy)
	end
	next_p.vel.x = helpers.bound(self.player_entity.vel.x + ddx*dt, -self.physics.maxdx, self.physics.maxdx)
	next_p.pos.x = helpers.normalize(
	   self.player_entity.pos.x + next_p.vel.x*dt,
	   -player_rect.w/2,
	   self:x_to_pixel(game.map.width) + player_rect.w*1.5
	)
	next_p.pos.y = helpers.bound(
	   self.player_entity.pos.y + next_p.vel.y*dt,
	   0,
	   self:y_to_pixel(game.map.height) - self.player_entity.height/2
	)

Having the next position, we need to check it for collisions and adjust before updating the player's actual position.

  if next_p.pos.x ~= self.player_entity.pos.x or next_p.pos.y ~= self.player_entity.pos.y then
	 if distance(self.player_entity.pos, next_p.pos) > self.map.tilewidth then
		-- TODO: sweep AABB??
		print("PLAYER MOVING TOO FAST!", inspect(self.player_entity.pos), inspect(next_p.pos))
	 else
		local top_left_tile = self:pixel_to_tile(next_p:get_rect())
		local bottom_left_tile = {x=top_left_tile.x, y=top_left_tile.y + 1}
		local bottom_right_tile = {x=top_left_tile.x + 1, y=top_left_tile.y + 1}
		local top_right_tile = {x=top_left_tile.x + 1, y=top_left_tile.y}
		local next_rect = next_p:get_rect()

		if not wasjumping and self.player_entity.grounded then
		   if wasleft and self:tile_value(bottom_left_tile) == 0 or wasright and self:tile_value(bottom_right_tile) == 0 then
			  self.player_entity.grounded = false
		   end
		end

		if wasjumping and (
		   self:tile_value(top_left_tile) ~= 0
		   and rectangles_overlap(next_rect, self:tile_to_rect(top_left_tile))[2]
		   or self:tile_value(top_right_tile) ~= 0
		   and rectangles_overlap(next_rect, self:tile_to_rect(top_right_tile))[2]
		)  then
		   next_p.pos.y = self:y_to_pixel(bottom_left_tile.y) + self.player_entity.height/2
		   next_p.vel.y = 0
		elseif self:tile_value(bottom_left_tile) ~= 0 and rectangles_overlap(next_rect, self:tile_to_rect(bottom_left_tile))[2]
		   or self:tile_value(bottom_right_tile) ~= 0 and rectangles_overlap(next_rect, self:tile_to_rect(bottom_right_tile))[2] then
		   next_p.pos.y = self:y_to_pixel(bottom_right_tile.y) - self.player_entity.height/2
		   next_p.vel.y = 0
		   self.player_entity.grounded = true
		end

		if wasleft and (
		   self:tile_value(top_left_tile) ~= 0 and not wasjumping or self:tile_value(bottom_left_tile) ~= 0 and not self.player_entity.grounded
		) then
		   next_p.pos.x = self:x_to_pixel(top_right_tile.x) + self.player_entity.width/2
		   next_p.vel.x = 0
		end
		if wasright and (
		   self:tile_value(top_right_tile) ~= 0 and rectangles_overlap(next_rect, self:tile_to_rect(top_right_tile))[1] and not wasjumping or self:tile_value(bottom_right_tile) ~= 0 and not self.player_entity.grounded and rectangles_overlap(next_rect, self:tile_to_rect(bottom_right_tile))[1]
		) then
		   next_p.pos.x = self:x_to_pixel(bottom_right_tile.x) - self.player_entity.width/2
		   next_p.vel.x = 0
		end
	 end
  end

Finally, we can update the player's position as well as its velocity and acceleration

  self.player_entity.vel.x = next_p.vel.x
  self.player_entity.vel.y = next_p.vel.y
  self.player_entity.pos.x = next_p.pos.x
  self.player_entity.pos.y = next_p.pos.y

The following code helps stop the player at some point when being affected by friction, rather than jittering because the calculated velocity isn't exactly 0.

  if wasleft and self.player_entity.vel.x > 0 or wasright and self.player_entity.vel.x < 0 then
	 self.player_entity.vel.x = 0
  end
  function game:updatePlayer(dt)
  	 -- local accel = {x=0, y=self.physics.gravity}
  	 -- if self.input.right then
       -- 	  	accel.x = self.physics.accel
  	 -- elseif wasright then
       -- 	  	accel.x = -self.physics.friction
  	 -- end
  	 -- if self.input.left then
       -- 	  	accel.x = -self.physics.accel
  	 -- elseif wasleft then
       -- 	  	accel.x = self.physics.friction
  	 -- end
  	 -- if self.input.jump and self.player_entity.grounded then
       -- 	  	accel.y = -self.physics.jump
  	 -- end
  	 -- self.player_entity = self.player_entity:update(accel, dt)
     self.player_entity = self.player_entity:move(self.input, dt, self.current_frame)
  end

Let's stub out the rest functions for now

  function game:updateCamera(dt)
  end
  function game:updateItems(dt)
  end
  function game:updateEnemies(dt)
  end
  function game:updateChirps(dt)
  end

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:

  love.graphics.setColor(1, 1, 1)

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:

  love.graphics.setColor(0.2, 0.2, 0.2)
  for ty = 1, game.map.height do
	 for tx = 1, game.map.width do
		love.graphics.setColor(0.9, 0.8, 0.7)
		local tile = {x=tx, y=ty}
		local pixel = game:tile_to_pixel(tile)
		local value = game:tile_value(tile)
		if value > 0 then
		   love.graphics.setColor(0.2, 0.2, 0.2)
		end
		local tile_rect = {
		   x=pixel.x,
		   y=pixel.y,
		   w=game.map.tilewidth,
		   h=game.map.tileheight,
		}
		love.graphics.rectangle("fill", pixel.x, pixel.y, game.map.tilewidth, game.map.tileheight)
		love.graphics.setColor(0.9, 0.1, 0.3)
		love.graphics.rectangle("line", pixel.x, pixel.y, game.map.tilewidth, game.map.tileheight)
	 end
  end
  game:draw_debug_infos()

Let's draw the player as a rectangle for now:

  love.graphics.setColor(0, 0.8, 0.1)
  game.player_entity:draw()
  function love.draw()
		<<resetColor>>
		love.graphics.push()
		love.graphics.scale(game.scale_width, game.scale_height)
		<<drawMap>>
		<<drawPlayer>>
		love.graphics.pop()
  end

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

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 Fri>

CLOCK: [2024-08-07 mié 11:42][2024-08-07 mié 12:11] => 0:29

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 (?)

TODO Player slide

Tweak friction values

TODO changing direction while in mid air should have lots of friction

DONE Refactor player, enemies, items code into entities

TODO Add a couple enemy entities with stupid AI

DEADLINE: <2024-08-24 sáb>

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-08-24 sáb>

TODO Gameplay screen art mock

DEADLINE: <2024-08-24 sáb>

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.

TODO Implement game states

DEADLINE: <2024-08-24 sáb>

  • 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 Fix collision with solid tile when moving horizontally

DEADLINE: <2024-08-31 sáb>

CLOCK: [2024-08-13 mar 12:01][2024-08-13 mar 12:13] => 0:12

DONE Fix jump

SCHEDULED: <2024-08-13 mar 11:15-12:00>

TODO Try implementing wrap-around with polar coordinates and cosine

DEADLINE: <2024-08-31 sáb>

    -- 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

Old jam code

I initially tried making this game for Week Sauce jam but abandoned it, below is where I ended up at, keeping it here as reference (quite likely to remove it):

	local anim8 = require("anim8")
	local sti = require("sti")
	local inspect = require("inspect")
	local dbg = require("debugger")

	love.graphics.setDefaultFilter("nearest", "nearest")

	local game = {
	   screen_width=320,
	   screen_height=200,
	   player={speed=5120, jumping=false},
	   timeToSpawn=8,
	   entities={
		  chicks={},
		  spawners={},
		  enemies={},
	   },
	   spawnerIndex=1,
	   maxEnemies=1,
	}

	-- TODO: Track platform enemies are on, so they can jump only when grounded, and detect when a platform's edge is coming
	function beginContact(a, b, contact)
	   for i, chick in ipairs(game.entities.chicks) do
		  if not chick.rescued and (a == chick.fixture or b == chick.fixture) then
			 if a == game.player.fixture or b == game.player.fixture then
				chick.rescued = true
				print("Player rescued chick!")
				return true
			 end
		  end
	   end
	   for i, enemy in ipairs(game.entities.enemies) do
		  if a == enemy.fixture or b == enemy.fixture then
			 if a == game.player.fixture or b == game.player.fixture then
				-- player dies!
			 end
			 -- NOTE: first body is platforms
			 platforms = game.world:getBodies()[1]
			 for i, platFixture in ipairs(platforms:getFixtures()) do
				if a == platFixture or b == platFixture then
				   enemy.platform = platFixture
				   dbg()
				   print("ENEMY IS ON PLATFORM "..inspect(platFixture))
				end
			 end
		  end
	   end
	   if a == game.player.fixture or b == game.player.fixture then
		  game.player.jumping = false
	   end
	end

	function endContact(a, b, contact)
	end


	function love.load()
	   local desktop_width, desktop_height = love.window.getDesktopDimensions()
	   game.scaleW = desktop_width / game.screen_width
	   game.scaleH = desktop_height / game.screen_height
	   love.physics.setMeter(8)
	   game.world = love.physics.newWorld(0, 9.81*128)
	   game.world:setCallbacks(beginContact, endContact)
	   game.map = sti("map.lua", {"box2d"})
	   game.map:box2d_init(game.world)
	   game.sprites = {
		  player=love.graphics.newImage("assets/chicken.png"),
		  chick=love.graphics.newImage("assets/chick.png"),
	   }
	   local g = anim8.newGrid(32, 32, game.sprites.player:getWidth(), game.sprites.player:getHeight())
	   game.player.moveRightAnim = anim8.newAnimation(g('1-3', 2), 0.1)
	   game.player.moveLeftAnim = anim8.newAnimation(g('1-3', 4), 0.1)
	   game.player.currentAnim = game.player.moveRightAnim
	   -- TODO: move to player code
	   game.player.body = love.physics.newBody(game.world, 160, 64, "dynamic")
	   game.player.body:setFixedRotation(true)
	   game.player.shape = love.physics.newRectangleShape(8, 12)
	   game.player.fixture = love.physics.newFixture(game.player.body, game.player.shape)
	   game.player.fixture:setUserData("PLAYER")
	   game.player.anchor = game.player.body
	   game.map.layers.entities.visible = false
	   g = anim8.newGrid(24, 24, game.sprites.chick:getWidth(), game.sprites.chick:getHeight())
	   for i, mapEntity in ipairs(game.map.layers.entities.objects) do
		  if mapEntity.type == "chick" then
			 chick = {rescued=false}
			 chick.idleAnim = anim8.newAnimation(g('1-3', 2), 0.2)
			 chick.moveRightAnim = anim8.newAnimation(g('4-7', 3), 0.1)
			 chick.moveLeftAnim = anim8.newAnimation(g('4-7', 1), 0.1)
			 chick.currentAnim = chick.idleAnim
			 chick.body = love.physics.newBody(
				game.world, mapEntity.x, mapEntity.y, "static"
			 )
			 chick.shape = love.physics.newRectangleShape(
				mapEntity.width, mapEntity.height
			 )
			 chick.fixture = love.physics.newFixture(chick.body, chick.shape)
			 chick.fixture:setUserData("CHICK"..i)
			 chick.fixture:setSensor(true)
			 table.insert(game.entities.chicks, chick)
		  end
		  if mapEntity.type == "spawner" then
			 table.insert(game.entities.spawners, mapEntity)
		  end
	   end
	end

	function normalize(n, min, max)
	   local range = max - min
	   n = ((n - min) % range + range) % range + min
	   return n
	end

	function spawnEnemy()
	   spawner = game.entities.spawners[game.spawnerIndex]
	   game.spawnerIndex = game.spawnerIndex + 1
	   if game.spawnerIndex > #game.entities.spawners then
		  game.spawnerIndex = 1
	   end
	   enemy = {
		  jumpTimer=2,
		  body=love.physics.newBody(game.world, spawner.x, spawner.y - 8, "dynamic"),
		  platform=nil,
	   }
	   enemy.shape = love.physics.newRectangleShape(8, 8)
	   enemy.fixture = love.physics.newFixture(enemy.body, enemy.shape)
	   -- enemy.fixture:setSensor(true)
	   table.insert(game.entities.enemies, enemy)
	end

	function love.update(dt)
	   local velX, velY = game.player.body:getLinearVelocity()
	   local playerX, playerY = game.player.body:getPosition()
	   if #game.entities.enemies < game.maxEnemies then
		  if game.timeToSpawn <= 0 then
			 spawnEnemy()
			 game.timeToSpawn = 8
		  else
			 game.timeToSpawn = game.timeToSpawn - dt
		  end
	   end
	   for i, enemy in ipairs(game.entities.enemies) do
		  enemy.body:setLinearVelocity(64, 0)
		  -- if enemy.jumpTimer <= 0 then
		  -- 	 enemy.body:applyLinearImpulse(0, -1024)
		  -- 	 enemy.jumpTimer = 2
		  -- else
		  -- 	 enemy.jumpTimer = enemy.jumpTimer - dt
		  -- end
		  local enemyX, enemyY = enemy.body:getPosition()
		  -- NOTE: wrap around level
		  enemy.body:setPosition(normalize(enemyX, 0, game.screen_width), enemyY)
	   end
	   for i, chick in ipairs(game.entities.chicks) do
		  if chick.rescued then
			 if chick.body:getType() ~= "dynamic" then
				chick.body:setType("dynamic")
				chick.anchor = game.player.anchor
				game.player.anchor = chick.body
			 end
			 chick.body:setLinearVelocity(0, 0)
			 -- NOTE: wrap around level
			 -- local x, y = chick.anchor:getPosition()
			 local x = chick.body:getX()
			 local y = chick.body:getY()
			 -- local dx = normalize(playerX - x, -playerX, game.screen_width - playerX)
			 local dx = playerX - x
			 local dy = playerY - y*1.04
			 local distance = math.sqrt(dx^2 + dy^2)
			 if math.abs(dx) > 12 then
				if dx > 0 then
				   chick.currentAnim = chick.moveRightAnim
				elseif dx < 0 then
				   chick.currentAnim = chick.moveLeftAnim
				end
				chick.body:setX(normalize(x + (dx/distance)*128*dt, 0, game.screen_width))
			 end
			 if math.abs(dy) > 1 then
				chick.body:setY(y + (dy/distance)*96*dt)
			 end
			 chick.currentAnim:update(dt)
		  end
	   end
	   if love.keyboard.isDown("d", "right") then
		  velX = game.player.speed*dt
		  game.player.currentAnim = game.player.moveRightAnim
	   end
	   if love.keyboard.isDown("a", "left") then
		  velX = -game.player.speed*dt
		  game.player.currentAnim = game.player.moveLeftAnim
	   end
	   game.player.body:setLinearVelocity(velX, velY)
	   if velX ~= 0 then
		  game.player.currentAnim:update(dt)
	   end
	   if love.keyboard.isDown("space") then
		  if not game.player.jumping then
			 game.player.body:applyLinearImpulse(0, -512)
			 game.player.jumping = true
		  end
	   end
	   game.world:update(dt)
	   playerX, playerY = game.player.body:getPosition()
	   -- NOTE: wrap around level
	   game.player.body:setPosition(normalize(playerX, 0, game.screen_width), playerY)
	end

	function love.draw()
	   love.graphics.setColor(1, 1, 1)
	   local cameraX = math.floor(game.screen_width/2 - game.player.body:getX())
	   game.map:draw(cameraX, 0, game.scaleW, game.scaleH)
	   game.map:box2d_draw(cameraX, 0, game.scaleW, game.scaleH)

	   game.map:draw(cameraX - game.screen_width, 0, game.scaleW, game.scaleH)
	   game.map:box2d_draw(cameraX - game.screen_width, 0, game.scaleW, game.scaleH)
	   game.map:draw(cameraX + game.screen_width, 0, game.scaleW, game.scaleH)
	   game.map:box2d_draw(cameraX + game.screen_width, 0, game.scaleW, game.scaleH)
	 
	   love.graphics.push()
	   love.graphics.scale(game.scaleW, game.scaleH)
	   for i, spawner in ipairs(game.entities.spawners) do
		  love.graphics.setColor(0.1, 0.9, 0.2)
		  love.graphics.rectangle("fill", cameraX + spawner.x , spawner.y-8, 8, 8)
		  love.graphics.rectangle("fill", cameraX - game.screen_width + spawner.x, spawner.y-8, 8, 8)
		  love.graphics.rectangle("fill", cameraX + game.screen_width + spawner.x, spawner.y-8, 8, 8)
	   end
	   for i, chick in ipairs(game.entities.chicks) do
		  local x, y = chick.body:getPosition()
		  love.graphics.setColor(1, 1, 1)
		  chick.currentAnim:draw(game.sprites.chick, x + cameraX - 12, y - 18)
		  chick.currentAnim:draw(game.sprites.chick, cameraX - game.screen_width + x - 12, y - 18)
		  chick.currentAnim:draw(game.sprites.chick, cameraX + game.screen_width + x - 12, y - 18)
		  love.graphics.setColor(0.8, 0.9, 0.1)
		  love.graphics.rectangle("line", x + cameraX, y, 8, 8)
		  love.graphics.rectangle("line", cameraX - game.screen_width + x, y, 8, 8)
		  love.graphics.rectangle("line", cameraX + game.screen_width + x, y, 8, 8)
	   end
	   for i, enemy in ipairs(game.entities.enemies) do
		  local x, y = enemy.body:getPosition()
		  love.graphics.setColor(0.9, 0.2, 0.01)
		  love.graphics.rectangle("fill", x + cameraX, y, 8, 8)
		  love.graphics.rectangle("fill", cameraX - game.screen_width + x, y, 8, 8)
		  love.graphics.rectangle("fill", cameraX + game.screen_width + x, y, 8, 8)
	   end
	   love.graphics.translate(cameraX, 0)
	   love.graphics.setColor(1, 1, 1)
	   game.player.currentAnim:draw(game.sprites.player, game.player.body:getX() - 16, game.player.body:getY() - 24)
	   love.graphics.setColor(0.8, 0.7, 0)
	   love.graphics.polygon("line", game.player.body:getWorldPoints(game.player.shape:getPoints()))
	   love.graphics.pop()
	end