In part 1 we constructed a basic camera. Now we’re going to extend it by adding some parallax scrolling. Note that the method I’ll use is probably not the prettiest, as I came up with it in half an hour. Nevertheless, this will be a starting point for you to develop your own system.
What Is It?
Now, for those who don’t know, what is parallax scrolling? It’s a way to get a pseudo-3D effect while still have all graphics and gameplay based in 2D. It’s currently used very widely among 2D games, as it adds a sense of depth that normally isn’t there.
The technique we’ll use to achieve this is called the layer method. What we need to do is render many layers of graphics, which are affected (move) at different rates when the camera moves. Normally the playing field (where the player is, and all the action happens) will be affected at a 1:1 ratio. Layers “closer” to the camera than the playing field will move at a faster rate than the camera. Layers “farther” from the camera than the playing field will move at a slower rate than the camera.
Implementation
Alright, enough talk, now onto implementation. To implement this, we’ll use a rough layering system which is managed by the camera. Here’s the modifications to the original camera
module we made. First add this to the top, after setting the other values like x
and y
.
camera.layers = {}
The will, obviously, hold our layers. Next add this method:
function camera:newLayer(scale, func)
table.insert(self.layers, { draw = func, scale = scale })
table.sort(self.layers, function(a, b) return a.scale < b.scale end)
end
This is how we’ll create new layers. scale
is what the camera’s x and y position are multiplied by before drawing the layer. func
is a function which will draw the objects on that layer. We then have to sort the layers table by their scale, so ones with a lower scale will be drawn first; as in, the further away the layer, the earlier is will be drawn.
Next add this method:
function camera:draw()
local bx, by = self.x, self.y
for _, v in ipairs(self.layers) do
self.x = bx * v.scale
self.y = by * v.scale
camera:set()
v.draw()
camera:unset()
end
end
The function takes care of drawing all the layers. bx
and by
are the “base” x/y position, as in the original position of the camera. Before calling camera:set
and the drawing function, we multiply the camera’s position by the layer’s scale. We then do the usual drawing steps.
So will all this in place, our love.draw
definition will become:
function love.draw()
camera:draw()
end
A Quick Example
I made a quick example to show off the parallax system. Here’s the code for it:
require('camera')
math.randomseed(os.time())
math.random()
math.random()
math.random()
function love.load(args)
camera.layers = {}
for i = .5, 3, .5 do
local rectangles = {}
for j = 1, math.random(2, 15) do
table.insert(rectangles, {
math.random(0, 1600),
math.random(0, 1600),
math.random(50, 400),
math.random(50, 400),
color = { math.random(0, 255), math.random(0, 255), math.random(0, 255) }
})
end
camera:newLayer(i, function()
for _, v in ipairs(rectangles) do
love.graphics.setColor(v.color)
love.graphics.rectangle('fill', unpack(v))
love.graphics.setColor(255, 255, 255)
end
end)
end
end
function love.update(dt)
camera:setPosition(love.mouse.getX() * 2, love.mouse.getY() * 2)
end
function love.draw()
camera:draw()
love.graphics.print("FPS: " .. love.timer.getFPS(), 2, 2)
end
function love.keypressed(key, unicode)
if key == ' ' then
love.load()
elseif key == 'escape' then
love.event.push('q')
end
end
What this does is it creates 6 layers, starting at a scale of .5, going up to 3. The rectangles
table stores the information for a random number of rectangles to be drawn on the current layer. We don’t need a global because, the only function using this table is created inside the current local scope. The function for each layer simply draws the information contained each rectangle’s table.
You can reset everything by pressing space. This calls love.load
, and this is the reason why we re-create the camera.layers
table. You can quit by pressing escape.
One final thing to note is that up the top we’re calling math.random
three times, because it has been reported that the first three or so times you call math.random
it’s not a very random number. I don’t think I’ve ever experienced this myself, but it can’t hurt to throw in a few calls just in case.
Conclusion
Well that coves a basic parallax scrolling implementation. From here you can go on to create something a bit more beautiful in design for your own projects. If you’ve got any thoughts or suggestions, I’d greatly appreciate it if you could leave them in the comments.
Thanks for reading!