apex2022
apex2022

Reputation: 855

CS50 - Lua/LOVE2D - how to share the same updated random number variable between two classes (Flappy Bird)

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

Answers (1)

apex2022
apex2022

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

Related Questions