Random Intervals

The first update to the game was to randomise the interval between pipes spawning.

The logic that spawns new pipes lives inside the update function of the game’s Play state. It’s a fairly simple set-up: if a timer exceeds a preset value, a new pair of pipes is spawned:

-- in PlayState:update(dt)
...
if self.timer > 2 then
	-- modify the last Y coordinate we placed so pipe gaps aren't too far apart
	-- no higher than 10 pixels below the top edge of the screen,
	-- and no lower than a gap length (90 pixels) from the bottom
	local y = math.max(-PIPE_HEIGHT + 10, math.min(self.lastY + math.random(-20, 20), VIRTUAL_HEIGHT - 90 - PIPE_HEIGHT))
	self.lastY = y
	
	-- add a new pipe pair at the end of the screen at our new Y
	table.insert(self.pipePairs, PipePair(y))
	
	-- reset timer
	self.timer = 0
end
...

My first thought was to simply add a call to math.random() as part of the if statement. This was a terrible idea and made the game absolutely cursed - hundreds if not thousands not pipes were being generated and it made the game unplayable. In hindsight this was obvious: by placing if self.timer > math.random(2,5) then as part of the update loop I was forcing the game to do a check every single frame, and thereby causing pipes to spawn at incredibly high rates.

The solution was quite simple. As part of the game state I initialised a variable called self.interval and set it to 2. This means that the first pipe should spawn after 2 seconds of the game starting; when a pipe does spawn I then reset the timer and randomise the interval. The result is that the interval between pipes is only calculated as each pipe spawns and entirely avoids the issue of having multiple pipes spawn frames apart from each other.

function PlayState:init()
	...
	self.interval = 2
	...
 
function PlayState:update(dt)
...
if self.timer > self.interval then
	-- modify the last Y coordinate we placed so pipe gaps aren't too far apart
	-- no higher than 10 pixels below the top edge of the screen,
	-- and no lower than a gap length (90 pixels) from the bottom
	local y = math.max(-PIPE_HEIGHT + 10, 
						math.min(self.lastY + math.random(-20, 20), 
								 VIRTUAL_HEIGHT - 90 - PIPE_HEIGHT))
	self.lastY = y
	
	-- add a new pipe pair at the end of the screen at our new Y
	table.insert(self.pipePairs, PipePair(y))
	
	-- reset timer
	self.timer = 0
	self.interval = math.random(2,4)
end
...

Random Gaps

The next step was to randomise the vertical gap between pipes. On the first version of the game the gap was hard-coded to 90 pixels. While fun, having different sized gaps makes the game a tad more interesting.

The way pipes are generated is with logic that lives inside a subclass of each Pipe, called PipePair in the code base. When the game is in its Play state, the state’s update function runs and does the interval check described above. Once a pipe is ready to spawn the code generates the y coordinate for the pipe (since the gap between pipes is based on that coordinate). This coordinate is also saved to self.lastY, since we want to ensure that the new gap isn’t too far from the previous one.

function PlayState:update(dt)
	...
        local y = math.max(-PIPE_HEIGHT + 10,
				            math.min(self.lastY + math.random(-20, 20), 
						             VIRTUAL_HEIGHT - 90 - PIPE_HEIGHT))
        self.lastY = y
 
        -- add a new pipe pair at the end of the screen at our new Y
        table.insert(self.pipePairs, PipePair(y))
    ...

The y coordinate is then passed to the PipePair constructor, which itself creates two pipes, one “upper” pipe and one “bottom” pipe:

function PipePair:init(y)
	...
    self.pipes = {
        ['upper'] = Pipe('top', self.y),
        ['lower'] = Pipe('bottom', self.y + PIPE_HEIGHT + math.random(75,105))
    }
    ...
end

Because both pipes are based on the upper pipe’s y coordinate the bottom pipe is drawn at y plus the height of the pipe plus a value that defines the gap between them (the bottom pipe has values added to it because the xy coordinate system starts at the top left). To create random sized gaps between pipes the solution was to simply add a random value to the bottom pipe.