-
Notifications
You must be signed in to change notification settings - Fork 17
Arx 11 Basic Tiles
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 } )
Gamestate.registerEvents()
Gamestate.switch( menu )
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 couple of figs: whole game screen, one object - one file, single file, compromise>
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 of how to do it, as long as your game works. However, there are several considerations that you should probably take into account, such as the ease of maintenance of the separate parts of the graphics, and the format, your map editor of choice supports.
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 you have 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, you just can't 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. Each instance of the class should have access to the appropriate image and it is undesirable to copy the image into each instance. On the other hand, each instance should store a quad, that selects an appropriate tile from the whole image. The whole image, therefore, is convenient to store as a class variable, and quads should be individual for each object and should be defined in the constructor. For the Ball:
image = love.graphics.newImage( "img/800x600/ball.png" ) --(*1)
local x_tile_pos = 0 --(*2)
local y_tile_pos = 0
local ball_tile_width = 18
local ball_tile_height = 18
local tileset_width = 18
local tileset_height = 18
.....
function Ball:new()
.....
o.quad = o.quad or love.graphics.newQuad( x_tile_pos, y_tile_pos, --(*3)
ball_tile_width, ball_tile_height,
tileset_width, tileset_height )
.....
end
(*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): The new quad is created in the constructor.
The ball radius should be changed in accordance with the width of the tile: o.radius = o.radius or ball_tile_width / 2
.
Finally, it is necessary to change the draw
method to display quad instead of the circle.
function Ball:draw()
love.graphics.draw( self.image,
self.quad,
self.position.x - self.radius,
self.position.y - self.radius )
end
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 we need 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 Brick: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 Brick:new( o )
.....
o.bricktype = o.bricktype or 11
.....
o.quad = bricktype_to_quad( o.bricktype )
return o
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 placement in BricksContainer:
function BricksContainer:new( o )
o = o or {}
.....
o.rows = 11
o.columns = 8
o.top_left_position = vector( 47, 34 )
o.horizontal_distance = o.horizontal_distance or 0
o.vertical_distance = o.vertical_distance or 0
.....
end
I don't want to deal with the wall tiles now, but I adjust walls position to get a better representation of the final version of the game screen:
local defaultThickness = 34
local top_wall_thickness = 26
local right_border_x_pos = 576 --(*1)
function WallsContainer:new( o )
.....
o.wall_thickness = o.wall_thickness or defaultThickness
local left_wall = Wall:new{
position = vector( 0, 0 ),
width = o.wall_thickness,
height = love.graphics.getHeight(),
.....
}
local right_wall = Wall:new{
position = vector( right_border_x_pos, 0 ),
width = o.wall_thickness,
height = love.graphics.getHeight(),
.....
}
local top_wall = Wall:new{
position = vector( 0, 0 ),
width = right_border_x_pos,
height = top_wall_thickness,
.....
}
local bottom_wall = Wall:new{
position = vector( 0, love.graphics.getHeight() ),
width = love.graphics.getWidth(),
height = o.wall_thickness,
.....
}
.....
end
(*1) This size and position adjustments are back-and-forth bounce process. Do not expect to get everything right on the first pass.
To demonstrate that everything works as expected, here is a test map with bricks of all type.
Don't forget to change the sequence.lua
accordingly.
return {
name = "everything",
bricks = {
{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},
}
}
Feedback is crucial to improve the tutorial!
Let me know if you have any questions, critique, suggestions or just any other ideas.
Chapter 1: Prototype
- The Ball, The Brick, The Platform
- Game Objects as Lua Tables
- Bricks and Walls
- Detecting Collisions
- Resolving Collisions
- Levels
Appendix A: Storing Levels as Strings
Appendix B: Optimized Collision Detection (draft)
Chapter 2: General Code Structure
- Splitting Code into Several Files
- Loading Levels from Files
- Straightforward Gamestates
- Advanced Gamestates
- Basic Tiles
- Different Brick Types
- Basic Sound
- 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
- Improved Ball Rebounds
- Ball Launch From Platform (Two Objects Moving Together)
- Mouse Controls
- Spawning Bonuses
- Bonus Effects
- Glue Bonus
- Add New Ball Bonus
- Life and Next Level Bonuses
- Random Bonuses
- Menu Buttons
- Wall Tiles
- Side Panel
- Score
- Fonts
- More Sounds
- Final Screen
- Packaging
Appendix D: GUI Layouts
Appendix E: Love-release and Love.js
Beyond Programming: