A Löve2D Tutorial —Part 03 Shapes
This is the third part of a Löve2D tutorial based on a old (but gold) pygame tutorial. To read part 02, click here.
Do you remember how cool was the blue rectangle we made during part 01 of the tutorial? Well, today we’re gonna go a little wild and try out building other geometric shapes; and for the wow factor, they will be rotating!
Our first shape will be our old friend, blue rectangle. First thing to do here is to change the screen size to 400x400, so it is easier to understand and debug. Just change your conf.lua to something like this:
function love.conf(t)
-- 400x400 (WxH)
t.window.width = 400
t.window.height = 400
end
The example below shows, step-by-step, how to draw our rotating triangle. It might look a little hairy, but don’t let that intimidate you.
local rect = {w=60, h=60, angle=0}
local draw_rect_pick = 1
local draw_rect = nillocal function draw_pick()
-- shows which example are we showing
love.graphics.setColor(1, 1, 1)
love.graphics.print("pick is " .. draw_rect_pick, 2, 2)
endlocal function draw_grid()
-- draws a grid to help with visualization
local ww, wh = love.graphics.getWidth(), love.graphics.getHeight()
love.graphics.setColor(1, 1, 1)
-- horizontal line
love.graphics.line(0, wh/2, ww, wh/2)
-- vertical line
love.graphics.line(ww/2, 0, ww/2, wh)
endlocal function draw_rect_a(x, y)
-- sets color
love.graphics.setColor(.08, .58, .81)
-- shows where coordinate (x, y) is
love.graphics.rectangle('fill', x, y, rect.w, rect.h)
love.graphics.setColor(1, 0, 0)
-- marks the (x,y) coordinate
love.graphics.circle('fill', x, y, 3)
endlocal function draw_rect_b(x, y)
-- sets color
love.graphics.setColor(.08, .58, .81)
-- here, we change the rectangle position to have the
-- root coordinate at the center
love.graphics.rectangle('fill', x-rect.w/2, y-rect.h/2, rect.w, rect.h)
love.graphics.setColor(1, 0, 0)
-- marks the (x,y) coordinate
love.graphics.circle('fill', x, y, 3)
endlocal function draw_rect_c(x, y)
-- we can also change the drawing root coordinate to something else
love.graphics.translate(x, y)
-- now, coordinate (0, 0) translates to (x, y)
love.graphics.setColor(.08, .58, .81)
-- x and y are no longer necessary for setting the position
love.graphics.rectangle('fill', -rect.w/2, -rect.h/2, rect.w, rect.h)
love.graphics.setColor(0, 1, 1)
-- (0, 0) shows where the root coordinate is
love.graphics.circle('fill', 0, 0, 3)
endlocal function draw_rect_d(x, y)
-- sets color
love.graphics.setColor(.08, .58, .81)
-- sets the screen root coordinate
love.graphics.translate(x, y)
-- rotates all following drawings
love.graphics.rotate(rect.angle)
-- draws the rectangle at coordinate 0, 0
love.graphics.rectangle('fill', 0, 0, rect.w, rect.h)
-- in this function, the rectangle will not rotate around its exis
-- but around screen root coordinate love.graphics.setColor(0, 1, 1)
-- (0, 0) shows where the root coordinate is
love.graphics.circle('fill', 0, 0, 3)
endlocal function draw_rect_e(x, y)
-- sets color
love.graphics.setColor(.08, .58, .81)
-- sets the screen root coordinate
love.graphics.translate(x, y)
-- rotates all following drawings
love.graphics.rotate(rect.angle)
-- changes the rectangle center to match the screen root coordinate
love.graphics.rectangle('fill', -rect.w/2, -rect.h/2, rect.w, rect.h)
love.graphics.setColor(0, 1, 1)
-- (0, 0) shows where the root coordinate is
love.graphics.circle('fill', 0, 0, 3)
endlocal draw_rect_list = {
{fn = draw_rect_a, rotate = false},
{fn = draw_rect_b, rotate = false},
{fn = draw_rect_c, rotate = false},
{fn = draw_rect_d, rotate = true},
{fn = draw_rect_e, rotate = true},
}function love.keypressed(key)
if key == 'left' then
draw_rect_pick = math.max(0, draw_rect_pick - 1)
-- set value to list size if value is 0
draw_rect_pick = draw_rect_pick < 1 and #draw_rect_list or draw_rect_pick
end
if key == 'right' then
draw_rect_pick = (draw_rect_pick % #draw_rect_list) + 1
end
draw_rect = draw_rect_list[draw_rect_pick]
endfunction love.update(dt)
-- spins our rectangle; notice the change is in radians
rect.angle = rect.angle + math.pi/3 * dt;
-- makes sure our angle is not above the max possible value
rect.angle = rect.angle % (math.pi*2)
enddraw_rect = draw_rect_list[draw_rect_pick]function love.draw()
draw_pick() -- example we are showing
draw_grid() -- helps us keep track of the screen center
draw_rect.fn(200, 200)
end
The code above creates a rectangle drawn in multiple ways, where you can change how it is draw by using the keyboard arrow keys left and right.
First thing to notice is that we have two helper functions, draw_pick and draw_grid that help us better visualize and understand what we’re doing (they mark stuff). A little below, we have the draw_rect_[a-e] functions, where function a is the simpler and e is the more sofisticated example.
note: each function draws a red dot, which is our reference coordinate to (x, y).
In draw_rect_a, we draw a simple rectangle, just like in the first part of the tutorial.
In draw_rect_b, we draw the same rectangle, but we make sure to have it positioned with its center aligned with (x, y).
In draw_rect_c, we change the root coordinate (which is the reference point to which everything is drawn) from (0, 0), most upper-left point in the window, to (x, y) using the translate function. This way, now when we draw to coordinate (0, 0) it will be “translated” to (x, y). This is quite useful to simplify some operations.
In draw_rect_d, we call rotate so that everything that we draw after it is rotated by the requested angle (in radians). Notice that for draw_rect_d the rectangle is not quite rotating on its center. It is actually rotating around the root coordinate. To make it rotate around its center, we need to change its position to match the root coordinate. Which is what we do in draw_rect_e.
love.update and love.keypressed just do the usual, updating the angle during time and handling our keyboard events.
But I Want More Shapes!
Sure, why not?! Let’s try something more complex: a triangle, a rectangle and a circle.
local shape = {w=40, h=40, angle=0}local function draw_grid()
-- draws a grid to help with visualization
local ww, wh = love.graphics.getWidth(), love.graphics.getHeight()
love.graphics.setColor(1, 1, 1)
-- horizontal line
love.graphics.line(0, wh/2, ww, wh/2)
-- vertical line
love.graphics.line(ww/2, 0, ww/2, wh)
endlocal function draw_shape(x, y)
local step = 40
-- graphics.push() stores the current state of graphics
-- so it can be easely reset later
love.graphics.push()
love.graphics.translate(x, y)
love.graphics.rotate(shape.angle)
love.graphics.setColor(0, 1, 1)
love.graphics.rectangle('fill', -shape.w/2, -shape.h/2, shape.w, shape.h)
-- reset graphics to the previous state
love.graphics.pop()
love.graphics.push()
love.graphics.translate(x-step*2, y)
love.graphics.rotate(shape.angle)
love.graphics.setColor(1, 0, 0)
-- you can make many shapes with polygon
love.graphics.polygon('fill', 0, -27, 20, 13, -20, 13)
love.graphics.pop()
love.graphics.push()
love.graphics.translate(x+step*2, y)
love.graphics.rotate(shape.angle)
love.graphics.setColor(0, 0, 1)
love.graphics.circle('fill', 0, 0, shape.h/2)
love.graphics.pop()
endfunction love.update(dt)
shape.angle = shape.angle + math.pi/3 * dt;
shape.angle = shape.angle % (math.pi*2)
endfunction love.draw()
draw_grid() -- helps us keep track of the screen center
draw_shape(200, 200)
end
Five new functions are introduced here: graphics.push, graphics.pop, graphics.line, graphics.circle and graphics.polygon.
graphics.push and graphics.pop usually work together, as they allow you to reset the state of graphics after some changes, like setting color, translating or rotating. You push to store the current state in memory, do some changes, then you pop to override what was changed. Simple.
line, circle and polygon are very straighforward drawing functions. The draw what they are named after. One thing to notice about the circle is that, contrary to the rectangle, its center is draw in the requested coordinate, so, if you draw a circle to (30, 30) and rotate it, it won’t behave like function draw_rect_c but as draw_rect_d without any position correction.
note: if you decide to try out with shapes, and polygon is your guy for that, make sure that when using polygon in fill mode (that is, the first argument is “fill”), it is not self-intersecting, that is, the lines do not cross.
Why don’t you try extending our example with some different shapes? What about creating a rotating sun, a grass field at the bottom and a blue sky to crown it? = ]