Literary updates, and remove global game object.

This commit is contained in:
Gonzalo Delgado 2024-09-02 11:50:01 -03:00
parent bac6df1ef4
commit 4e83ed9807
5 changed files with 123 additions and 218 deletions

View File

@ -110,17 +110,17 @@ function Entity:adjust_for_collisions(old_state, dt, frame)
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]
top_left_tile.solid and helpers.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]
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 rectangles_overlap(next_rect, bottom_left_tile.pixel_rect)[3]
bottom_left_tile.solid and helpers.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]
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")
@ -130,14 +130,14 @@ function Entity:adjust_for_collisions(old_state, dt, frame)
if old_state.vel.x < 0 then
if top_left_tile.solid and rectangles_overlap(next_rect, top_left_tile.pixel_rect)[2] then
if top_left_tile.solid and helpers.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
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 - self.width*1.5
end

View File

@ -247,6 +247,19 @@ To achieve Flicky's screen wrap-around trick (or part of it at least), we need a
end
#+end_src
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
@ -402,9 +415,7 @@ I'm thinking there should be at least 4 game states:
- Game Over
- Ending
~main.lua~ should keep the logic for switching between them. Not sure if each state should "know" about each other, maybe just get a reference of the states it can switch to and under which conditions, like a FSM. Let's start with that and see where we get to.
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")
@ -442,7 +453,40 @@ I'm thinking there should be at least 4 game states:
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.
#+name: playState
#+begin_src lua :tangle no
local Play = GameState:extend()
function Play:init()
@ -457,7 +501,6 @@ I'm thinking there should be at least 4 game states:
}
self.physics.horizontal_acc = self.physics.maxdx*2 -- horizontal acceleration
self.physics.friction = self.physics.maxdx*1.5 -- horizontal friction
-- TODO: replace x, y (180, 100) with player position from Tiled map
self.player_entity = Entity:new(180, 100, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map)
end
@ -490,121 +533,24 @@ I'm thinking there should be at least 4 game states:
love.graphics.setColor(0, 0.8, 0.1)
self.player_entity:draw()
end
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
return {
Play=Play,
Title=Title,
}
#+end_src
#+begin_src lua :tangle states.lua :noweb yes
<<playState>>
<<titleState>>
return {
Play=Play,
Title=Title,
}
#+end_src
#+begin_src lua
local states = require("states")
#+end_src
* Game object
We'll have a game object that will hold all the state of the game and provide some functions to modify it. This involves keeping the current state (title screen, gameplay, game over, ending) and having logic to switch between those states. Let's define a game class for that
#+begin_src lua
local game = {
current_frame = 0,
current_state = nil,
}
#+end_src
* 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.
#+begin_src lua
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
#+end_src
* Map & Base game data
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
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
#+end_src
~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:
#+begin_src lua
function game:tile_value(tile)
return self.platforms[tile.x + (tile.y - 1)*self.map.width]
end
#+end_src
* 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:
#+begin_src lua
function game:initializeGraphics()
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()
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
#+end_src
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.
#+begin_src lua
#+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)
@ -617,9 +563,10 @@ Let's add a function to draw some debug stuff, starting with just the player's p
* Map class
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.)
Tile values deserve 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".
#+begin_src lua :tangle map.lua
local Object = require("object")
local Tile = require("tile")
@ -870,17 +817,17 @@ We'll need to check for collisions against solid tiles and adjust the entities p
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]
top_left_tile.solid and helpers.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]
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 rectangles_overlap(next_rect, bottom_left_tile.pixel_rect)[3]
bottom_left_tile.solid and helpers.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]
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")
@ -890,14 +837,14 @@ We'll need to check for collisions against solid tiles and adjust the entities p
if old_state.vel.x < 0 then
if top_left_tile.solid and rectangles_overlap(next_rect, top_left_tile.pixel_rect)[2] then
if top_left_tile.solid and helpers.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
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 - self.width*1.5
end
@ -945,15 +892,6 @@ We'll need to check for collisions against solid tiles and adjust the entities p
local Entity = require("entity")
#+end_src
* Player
Let's use the entity class to set up a player object that tracks its position and velocity.
#+begin_src lua
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
#+end_src
* Initialization
Next, we define ~love.load()~, which should take care of:
@ -964,25 +902,33 @@ Next, we define ~love.load()~, which should take care of:
- this will be moved to a level initialization function later on
To initialize the graphics we simply call the method we defined for the game object:
#+name: initializeGraphics
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
game:initializeGraphics()
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: initPlayState
#+name: initGameState
#+begin_src lua :tangle no
game.current_state = states.Title:new()
-- game:initPlayer(180, 100)
current_state = states.Title:new()
#+end_src
Putting it all together in LÖVE's ~load~ callback:
#+begin_src lua :noweb yes
function love.load()
<<initializeGraphics>>
<<initPlayState>>
end
local scale_width, scale_height, current_state
function love.load()
<<initGraphics>>
<<initGameState>>
end
#+end_src
* Game logic
@ -1020,11 +966,18 @@ This means the game logic should keep track of a few things:
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)
game.current_state:update(dt)
if game.current_state.ended then
game.current_state = game.current_state.next
current_state:update(dt)
if current_state.ended then
current_state = current_state.next
end
end
#+end_src
@ -1041,18 +994,12 @@ We'll also do scaling using the calculated scale factors from the game object:
game:draw_debug_infos()
#+end_src
Let's draw the player as a rectangle for now:
#+name: drawPlayer
#+begin_src lua :tangle no
love.graphics.setColor(0, 0.8, 0.1)
game.player_entity:draw()
#+end_src
#+begin_src lua :noweb yes
function love.draw()
<<resetColor>>
love.graphics.push()
love.graphics.scale(game.scale_width, game.scale_height)
game.current_state:draw()
love.graphics.scale(scale_width, scale_height)
current_state:draw()
love.graphics.pop()
end
#+end_src

View File

@ -44,4 +44,10 @@ function helpers.normalize(n, min, max)
return ((n - min) % range + range) % range + min
end
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
return helpers

View File

@ -121,75 +121,32 @@ end
local states = require("states")
local game = {
current_frame = 0,
current_state = nil,
}
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
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
function game:tile_value(tile)
return self.platforms[tile.x + (tile.y - 1)*self.map.width]
end
function game:initializeGraphics()
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()
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
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
local Tile = require("tile")
local Entity = require("entity")
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
function love.load()
game:initializeGraphics()
game.current_state = states.Title:new()
-- game:initPlayer(180, 100)
end
local scale_width, scale_height, current_state
function love.load()
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
current_state = states.Title:new()
end
function love.update(dt)
game.current_state:update(dt)
if game.current_state.ended then
game.current_state = game.current_state.next
current_state:update(dt)
if current_state.ended then
current_state = current_state.next
end
end
function love.draw()
love.graphics.setColor(1, 1, 1)
love.graphics.push()
love.graphics.scale(game.scale_width, game.scale_height)
game.current_state:draw()
love.graphics.scale(scale_width, scale_height)
current_state:draw()
love.graphics.pop()
end

View File

@ -49,7 +49,6 @@ function Play:init()
}
self.physics.horizontal_acc = self.physics.maxdx*2 -- horizontal acceleration
self.physics.friction = self.physics.maxdx*1.5 -- horizontal friction
-- TODO: replace x, y (180, 100) with player position from Tiled map
self.player_entity = Entity:new(180, 100, 4, 8, self.physics.gravity, 2*8, self.physics.horizontal_acc, self.physics.friction, self.map)
end
@ -82,7 +81,6 @@ function Play:draw()
love.graphics.setColor(0, 0.8, 0.1)
self.player_entity:draw()
end
local Title = GameState:extend()
function Title:update(dt)
@ -106,10 +104,7 @@ function Title:draw()
width = font:getWidth("START")
love.graphics.print("START", 320/2, 128, 0, 1, 1, width/2, height/2)
end
return {
Play=Play,
Title=Title,
Play=Play,
Title=Title,
}