Reputation: 855
I am doing the CS50 Intro to Game Development course, which uses Lua and LOVE2D for its games. I've discovered a bug in the assignment code, but I am unsure how to fix it.
The assignment is a Flappy Bird clone. If you're unfamiliar, the game has a bird flying through gaps between sets of upper and lower pipes. I will post the code at the end of this post. The students are given the assignment code for the complete game and then have to make modifications. I will post the code at the end.
Here are the relevant parts. In PipePair.lua, we set a constant GAP_HEIGHT to 90. We then have the init() function where we instantiate a pair of pipes as follows:
self.pipes = {
['upper'] = Pipe('top', self.y),
['lower'] = Pipe('bottom', self.y + PIPE_HEIGHT + GAP_HEIGHT)
}
PIPE_HEIGHT is 288, the size of our pipe sprite.
In PlayState.lua. we set a variable self.y as follows:
self.lastY = -PIPE_HEIGHT + math.random(1,80) + 20
Then in the update method we set a local variable y to the following:
local y = math.max(-PIPE_HEIGHT + 10, math.min(self.lastY + math.random(-20, 20), VIRTUAL_HEIGHT - 90 - PIPE_HEIGHT))
self.lastY = y
table.insert(self.pipePairs, PipePair(y))
VIRTUAL_HEIGHT is 288, same as PIPE_HEIGHT. The purpose of this method is to change the location of the gap for each pipe pair somewhere between 20 pixels lower and 20 pixels higher than the last gap, in order to create a kind of flow. The purpose of the (+10) in the first value of math.max is to make sure the upper pipe is showing at least 10 pixels so you see the pipe. The hard-coded value 90 in the second value of math.min is supposed to be the gap height. The purpose of this second value in math.min is to make sure the bottom pipe doesn't start being drawn below the screen and is thus not visible.
Finally, Pipe.lua has the render function for each pipe as follows:
love.graphics.draw(PIPE_IMAGE, self.x,
(self.orientation == 'top' and self.y + PIPE_HEIGHT or self.y),
0, 1, self.orientation == 'top' and -1 or 1)
Here's the bug I discovered. I noticed in the game that sometimes the bottom pipe was not visible. After doing some testing and calculating, I discovered that it was possible for the y of the bottom pipe to be set to 288, which is the virtual screen height. Unlike the top pipe, the bottom pipe is not set to be visible above the ground as a minimum height. The fix is to change the hard-coded 90 in PlayState's local y calculation to 110. But this only works if the gap height is 90. If the gap height is 60 the value needs to be 80, and if the gap height is 120, the value needs to be 140.
So here is the problem. The assignment requires us to randomize the gap height in each spawn of pipe pairs. I'm trying to randomize between 60 and 120. But to avoid the bug I need to provide the same value of gap height to both the PipePair class and the PlayState class. The random number needs to change on each update. But I can't figure out where to put a random number generation that will change for each update and share that value with both the PipePair and PlayState classes. How can I do that?
By the way, I've posted this question in multiple places for CS50 discussions and have had no responses, which is why I'm turning here for help.
Here are the relevant files, minus comments:
PlayState.lua
PlayState = Class{__includes = BaseState}
PIPE_SPEED = 60
PIPE_WIDTH = 70
PIPE_HEIGHT = 288
BIRD_WIDTH = 38
BIRD_HEIGHT = 24
function PlayState:init()
self.bird = Bird()
self.pipePairs = {}
self.timer = 0
self.score = 0
self.lastY = -PIPE_HEIGHT + math.random(80) + 20
end
function PlayState:update(dt)
self.timer = self.timer + dt
if self.timer > 2 then
local y = math.max(-PIPE_HEIGHT + 10,
math.min(self.lastY + math.random(-20,20), VIRTUAL_HEIGHT - 90 - PIPE_HEIGHT))
self.lastY = y
table.insert(self.pipePairs, PipePair(y))
self.timer = 0
end
for k, pair in pairs(self.pipePairs) do
if not pair.scored then
if pair.x + PIPE_WIDTH < self.bird.x then
self.score = self.score + 1
pair.scored = true
sounds['score']:play()
end
end
pair:update(dt)
end
for k, pair in pairs(self.pipePairs) do
if pair.remove then
table.remove(self.pipePairs, k)
end
end
for k, pair in pairs(self.pipePairs) do
for l, pipe in pairs(pair.pipes) do
if self.bird:collides(pipe) then
sounds['explosion']:play()
sounds['hurt']:play()
gStateMachine:change('score', {
score = self.score
})
end
end
end
self.bird:update(dt)
if self.bird.y > VIRTUAL_HEIGHT - 15 then
sounds['explosion']:play()
sounds['hurt']:play()
gStateMachine:change('score', {
score = self.score
})
end
end
function PlayState:render()
for k, pair in pairs(self.pipePairs) do
pair:render()
end
love.graphics.setFont(flappyFont)
love.graphics.print('Score: ' .. tostring(self.score), 8, 8)
self.bird:render()
end
function PlayState:enter()
scrolling = true
end
function PlayState:exit()
scrolling = false
end
PipePair.lua
PipePair = Class{}
local GAP_HEIGHT = 90
function PipePair:init(y)
self.scored = false
self.x = VIRTUAL_WIDTH + 32
self.y = y
self.pipes = {
['upper'] = Pipe('top', self.y),
['lower'] = Pipe('bottom', self.y + PIPE_HEIGHT + GAP_HEIGHT)
}
self.remove = false
end
function PipePair:update(dt)
if self.x > -PIPE_WIDTH then
self.x = self.x - PIPE_SPEED * dt
self.pipes['lower'].x = self.x
self.pipes['upper'].x = self.x
else
self.remove = true
end
end
function PipePair:render()
for l, pipe in pairs(self.pipes) do
pipe:render()
end
end
Pipe.lua
Pipe = Class{}
local PIPE_IMAGE = love.graphics.newImage('pipe.png')
function Pipe:init(orientation, y)
self.x = VIRTUAL_WIDTH + 64
self.y = y
self.width = PIPE_WIDTH
self.height = PIPE_HEIGHT
self.orientation = orientation
end
function Pipe:update(dt)
end
function Pipe:render()
love.graphics.draw(PIPE_IMAGE, self.x,
(self.orientation == 'top' and self.y + PIPE_HEIGHT or self.y),
0, 1, self.orientation == 'top' and -1 or 1)
end
main.lua (not sure if actually relevant):
push = require 'push'
Class = require 'class'
require 'StateMachine'
require 'states/BaseState'
require 'states/CountdownState'
require 'states/PlayState'
require 'states/ScoreState'
require 'states/TitleScreenState'
require 'Bird'
require 'Pipe'
require 'PipePair'
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
VIRTUAL_WIDTH = 512
VIRTUAL_HEIGHT = 288
local background = love.graphics.newImage('background.png')
local backgroundScroll = 0
local ground = love.graphics.newImage('ground.png')
local groundScroll = 0
local BACKGROUND_SCROLL_SPEED = 30
local GROUND_SCROLL_SPEED = 60
local BACKGROUND_LOOPING_POINT = 413
function love.load()
love.graphics.setDefaultFilter('nearest', 'nearest')
math.randomseed(os.time())
love.window.setTitle('Fifty Bird')
smallFont = love.graphics.newFont('font.ttf', 8)
mediumFont = love.graphics.newFont('flappy.ttf', 14)
flappyFont = love.graphics.newFont('flappy.ttf', 28)
hugeFont = love.graphics.newFont('flappy.ttf', 56)
love.graphics.setFont(flappyFont)
sounds = {
['jump'] = love.audio.newSource('jump.wav', 'static'),
['explosion'] = love.audio.newSource('explosion.wav', 'static'),
['hurt'] = love.audio.newSource('hurt.wav', 'static'),
['score'] = love.audio.newSource('score.wav', 'static'),
['music'] = love.audio.newSource('marios_way.mp3', 'static')
}
sounds['music']:setLooping(true)
sounds['music']:play()
push:setupScreen(VIRTUAL_WIDTH, VIRTUAL_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT, {
vsync = true,
fullscreen = false,
resizable = true
})
gStateMachine = StateMachine {
['title'] = function() return TitleScreenState() end,
['countdown'] = function() return CountdownState() end,
['play'] = function() return PlayState() end,
['score'] = function() return ScoreState() end
}
gStateMachine:change('title')
love.keyboard.keysPressed = {}
love.mouse.buttonsPressed = {}
end
function love.resize(w, h)
push:resize(w, h)
end
function love.keypressed(key)
love.keyboard.keysPressed[key] = true
if key == 'escape' then
love.event.quit()
end
end
function love.mousepressed(x, y, button)
love.mouse.buttonsPressed[button] = true
end
function love.keyboard.wasPressed(key)
return love.keyboard.keysPressed[key]
end
function love.mouse.wasPressed(button)
return love.mouse.buttonsPressed[button]
end
function love.update(dt)
backgroundScroll = (backgroundScroll + BACKGROUND_SCROLL_SPEED * dt) % BACKGROUND_LOOPING_POINT
groundScroll = (groundScroll + GROUND_SCROLL_SPEED * dt) % VIRTUAL_WIDTH
gStateMachine:update(dt)
love.keyboard.keysPressed = {}
love.mouse.buttonsPressed = {}
end
function love.draw()
push:start()
love.graphics.draw(background, -backgroundScroll, 0)
gStateMachine:render()
love.graphics.draw(ground, -groundScroll, VIRTUAL_HEIGHT - 16)
push:finish()
end
Upvotes: 0
Views: 150
Reputation: 855
I figured it out. I am able to create a global variable at the start of my main.lua file, before the love.load() function. I previously thought that I could only create constants there, for which I wouldn't be able to update with a random number each game cycle. By putting a global variable there, I am able to share it with both PlayState.lua and PipePair.lua.
Upvotes: 0