Skip to content

Basic Tiles

noooway edited this page May 16, 2017 · 40 revisions

In this part, I want to improve the graphical representation of the game objects. An idea is to associate each game object with a certain image and use it instead of the circles and rectangles.

LÖVE has several functions in the love.graphics module, which allow to load and display images on the screen. In particular, we are interested in love.graphics.newImage, which loads an image from the hard drive, love.graphics.draw, which displays it, and love.graphics.newQuad which allows to select a rectangular piece from the image and display only this piece, instead of the whole image.

LÖVE operates only with raster formats, such as png or jpg. This immediately forces us to choose a resolution for the game. I use 800x600 (which probably is not optimal these days). It is set with love.window.setMode function at the start of the game, in the love.load in the main.lua:

function love.load()
   local love_window_width = 800
   local love_window_height = 600
   love.window.setMode( love_window_width,
                        love_window_height,
                        { fullscreen = false } )
   .....
end

It is common to draw the graphics in the high resolution first, and then scale down as needed. In my case, I've drawn all the game images in a vector svg format (using Inkscape vector graphics editor), which LÖVE doesn't support, and it was necessary to export them into raster.

(insert a couple of figs: whole game screen mockup, tileset with all objects in a single file, one object - one file, compromise. E.g.: everything in a single file: kenney platformer pack, compromise: characters with animation are separate from ground, one object - one file: each object with it's animation in a separate file )

It is common to draw a more-or-less complete game screen to correctly get sizes, proportions, and colors of the different game objects; to obtain a complete impression of how everything looks together. When this is done, it is necessary to somehow organize the game elements to simplify a further work with them. There is no right or wrong way how to do it, as long as the game works. However, there are several considerations that probably should be taken into account, such as the ease of maintenance of the separate parts of the graphics, the format the chosen map editor can work with, and so on.

At first, it seems logical to create a separate image file for each game object. A certain drawback of such approach is that it is hard to update the graphics, unless there is a support for automation of export. On the other hand, it allows to fine tune, which images to load, providing some control over memory consumption (for the small games memory is not an issue, but modern AAA-blockbusters are tens of GB in size, mostly due to graphical content, it is just not possible to load it into memory all at once).

Another approach is to put everything into a single file. This is fine and used commonly. However, when the number of different game objects becomes significant, selecting appropriate quads can become a bit messy.

I'll use a compromise between the two methods and adapt a convention that there is a separate file with graphics for each class of objects in the game. That is, a separate image file for the bricks, separate for the platform, and so on.

The graphics is stored in the img/800x600/ folder. Now it is necessary to work with it in the code.

The first step is to load and store the images somewhere. A natural place to put them is a table with other properties of game objects. For the ball:

local ball = {}
ball.position = vector( 200, 450 )
ball.speed = vector( -250, -250 )
ball.image = love.graphics.newImage( "img/800x600/ball.png" )              --(*1)
ball.x_tile_pos = 0                                                        --(*2)
ball.y_tile_pos = 0
ball.tile_width = 18
ball.tile_height = 18
ball.tileset_width = 18
ball.tileset_height = 18
ball.quad = love.graphics.newQuad( ball.x_tile_pos, ball.y_tile_pos,       --(*3)
                                   ball.tile_width, ball.tile_height,
                                   ball.tileset_width, ball.tileset_height )
ball.radius = ball.tile_width / 2                                          --(*4)

(*1): The image file is loaded from the hard drive.
(*2): love.graphics.newQuad requires several parameters to specify a part of the image to display: the top-left corner of the part, it's width and height, and the width and height of the whole image.
(*3): A quad for the ball is created.
(*4): The ball radius should be changed in accordance with the width of the tile: ball.radius = ball_tile_width / 2.

After image is loaded, it is necessary to change the ball.draw function to display the quad instead of the circle.

function ball.draw()
   love.graphics.draw( ball.image,
                       ball.quad, 
                       ball.position.x - ball.radius,
                       ball.position.y - ball.radius )
   local segments_in_circle = 16                        --(*1)
   love.graphics.circle( 'line',
                         ball.position.x,
                         ball.position.y,
                         ball.radius,
                         segments_in_circle )   
end

(*1): I do not delete the old representation for now, just to check that quads have correct parameters.

The code for the platform is essentially similar, so I won't go over it.

With bricks, a situation is a bit more complicated. Bricks can be of different type (brick type is encoded in the level description). From this type it is necessary to determine quad position in the tileset. It is reasonable to define a function for such task: it should accept a brick type and return an appropriate quad.

We have bricks of different hardness and color, arranged in a table-like structure in our tileset. Basically, let's agree to decode brick types by two-digit number, where the first digit is a row in the tileset, and the second one is a column (indexing starts from 1 in both cases). The function to convert brick type to quad has the following form:

function bricks.bricktype_to_quad( bricktype )
   local row = math.floor( id / 10 )
   local col = id % 10
   local x_pos = single_tile_width * ( col - 1 )
   local y_pos = single_tile_height * ( row - 1 )
   return love.graphics.newQuad( x_pos, y_pos,
                                 brick_tile_width, brick_tile_height,
                                 tileset_width, tileset_height )
end

We need to call it when constructing the bricks:

function bricks.new_brick( position, width, height, bricktype )
   return( { .....
             bricktype = bricktype,
             quad = bricks.bricktype_to_quad( bricktype ) } )
end

The changes in the draw method are similar to the ball-case.

Due to the change in sizes of the bricks, it is necessary to change map dimension to 8x11, and make some adjustments in the bricks width, height and placement:

local bricks = {}
bricks.image = love.graphics.newImage( "img/800x600/bricks.png" )
bricks.tile_width = 64
bricks.tile_height = 32
bricks.tileset_width = 384
bricks.tileset_height = 160
bricks.rows = 11
bricks.columns = 8
bricks.top_left_position = vector( 47, 34 )
bricks.brick_width = bricks.tile_width
bricks.brick_height = bricks.tile_height
bricks.horizontal_distance = 0
bricks.vertical_distance = 0
.....

I don't want to deal with the wall tiles now, but I adjust walls positions to get a better representation of the final version of the game screen:

walls.side_walls_thickness = 34
walls.top_wall_thickness = 26
walls.right_border_x_pos = 576 --(*1)

function walls.construct_walls()
   local left_wall = walls.new_wall(
      vector( 0, 0 ),
      walls.side_walls_thickness,
      love.graphics.getHeight()
   )
   local right_wall = walls.new_wall(
      vector( walls.right_border_x_pos, 0 ),
      walls.side_walls_thickness,
      love.graphics.getHeight()
   )
   local top_wall = walls.new_wall(
      vector( 0, 0 ),
      walls.right_border_x_pos,
      walls.top_wall_thickness
   )
   local bottom_wall = walls.new_wall(
      vector( 0, love.graphics.getHeight() ),
      walls.right_border_x_pos,
      walls.top_wall_thickness
   ) 
   walls.current_level_walls["left"] = left_wall
   walls.current_level_walls["right"] = right_wall
   walls.current_level_walls["top"] = top_wall
   walls.current_level_walls["bottom"] = bottom_wall
end

(*1) This size and position adjustments are back-and-forth bounce process. Do not expect to get everything right on the first pass. A tool such as Lurker which "automatically hotswaps changed Lua files in a running LÖVE project" might be helpful during this process.

To demonstrate that everything works as expected, here is a test map test_all.lua with bricks of all type. Don't forget to change the sequence.lua accordingly.

return {
   {51, 51, 00, 00, 00, 00, 51, 51},
   {51, 00, 00, 00, 00, 00, 00, 51},
   {00, 00, 00, 00, 00, 00, 00, 00},
   {00, 00, 00, 00, 00, 00, 00, 00},
   {00, 00, 00, 00, 00, 00, 00, 00},
   {21, 21, 22, 23, 24, 25, 26, 26},
   {31, 31, 32, 33, 34, 35, 36, 36},
   {41, 41, 42, 43, 44, 45, 46, 46},
   {11, 11, 12, 13, 14, 15, 16, 16},
   {00, 00, 00, 00, 00, 00, 00, 00},
   {00, 00, 00, 00, 00, 00, 00, 00}
}

    Home
    Acknowledgements
    Todo

Chapter 1: Prototype

  1. The Ball, The Brick, The Platform
  2. Game Objects as Lua Tables
  3. Bricks and Walls
  4. Detecting Collisions
  5. Resolving Collisions
  6. Levels

    Appendix A: Storing Levels as Strings
    Appendix B: Optimized Collision Detection (draft)

Chapter 2: General Code Structure

  1. Splitting Code into Several Files
  2. Loading Levels from Files
  3. Straightforward Gamestates
  4. Advanced Gamestates
  5. Basic Tiles
  6. Different Brick Types
  7. Basic Sound
  8. Game Over

    Appendix C: Stricter Modules (draft)
    Appendix D-1: Intro to Classes (draft)
    Appendix D-2: Chapter 2 Using Classes.

Chapter 3 (deprecated): Details

  1. Improved Ball Rebounds
  2. Ball Launch From Platform (Two Objects Moving Together)
  3. Mouse Controls
  4. Spawning Bonuses
  5. Bonus Effects
  6. Glue Bonus
  7. Add New Ball Bonus
  8. Life and Next Level Bonuses
  9. Random Bonuses
  10. Menu Buttons
  11. Wall Tiles
  12. Side Panel
  13. Score
  14. Fonts
  15. More Sounds
  16. Final Screen
  17. Packaging

    Appendix D: GUI Layouts
    Appendix E: Love-release and Love.js

Beyond Programming:

  1. Game Design
  2. Minimal Marketing (draft)
  3. Finding a Team (draft)

Archive

Clone this wiki locally