I need help making a scrolling background

I am making a puzzle game and wanted to make a background that scrolls while you play from the left side of the screen to the right side of the screen. I am using Lettercore’s seamless pattern tiles.

I wanted this code to choose a random pattern and tile the background everytime you open the game. This worked fine until i tried making it scroll. now the background just doesn’t exist. please help

import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/object"
import "CoreLibs/timer"

pd = playdate
gfx = pd.graphics
sfx = playdate.sound
sprite = gfx.sprite

shapeTiles = gfx.imagetable.new('images/ShapeTiles')
score = 0
solvedRows = {}
solvedColumns = {}
backgroundTiles = {}
local scrollSpeed = 1
local scrollX = 0

--MAKES IMAGE DATA FOR THE BACKGROUND
local pattern = gfx.image.new('images/pattern/'.. math.random(1,10))
patternW, patternL = pattern:getSize()
for i = 0, 240/patternL+2 do
	for j = 0,400/patternW+2 do 
		table.insert(backgroundTiles, {x = patternW*j,y = patternL*i})
	end
end
--

border = sprite.new(gfx.image.new('images/Frame'))
border:moveTo(32*3+32,32*3+32)
border:setScale(1.5,1.5)
border:add()

playdate.getSystemMenu():addMenuItem("Restart game", function()
    restartGame()
end)

playdate.graphics.setLineWidth(3)
pd.display.setInverted(true)

solveSound = sfx.sampleplayer.new("sounds/Solved")
selectSound = sfx.sampleplayer.new("sounds/Select")
swapSound = sfx.sampleplayer.new("sounds/Swap")


import "tile"
import "cursor"

tileGrid = {
	tile(1,1),tile(2,1),tile(3,1),tile(4,1),tile(5,1),
	tile(1,2),tile(2,2),tile(3,2),tile(4,2),tile(5,2),
	tile(1,3),tile(2,3),tile(3,3),tile(4,3),tile(5,3),
	tile(1,4),tile(2,4),tile(3,4),tile(4,4),tile(5,4),
	tile(1,5),tile(2,5),tile(3,5),tile(4,5),tile(5,5)
}

cursorSprite = cursor(3,3)

function getTile(x, y)
    return tileGrid[(y - 1) * 5 + x]
end

function gridToScreen(x, y)
    return 32 * x + 32, 32 * y + 32
end

function getRow(y)
    local row = {}
    for x = 1, 5 do
        table.insert(row, getTile(x, y))
    end
    return row
end

function getColumn(x)
    local col = {}
    for y = 1, 5 do
        table.insert(col, getTile(x, y))
    end
    return col
end

function solveColumn(x)
	for i = 1,5 do 
		getColumn(x)[i]:reroll()
	end
	score += 1
end

function solveRow(x)
	for i = 1,5 do 
		getRow(x)[i]:reroll()
	end
	score += 1
end

function restartGame()
	for i = 1, #tileGrid do
		tileGrid[i]:reroll()
	end
	score = 0
end 

function pd.update()

    --SCROLLING EVERY UPDATE
	scrollX = (scrollX + scrollSpeed) % patternW

    for i =  1, #backgroundTiles do
        local drawX = backgroundTiles[i].x - scrollX
        pattern:drawAnchored(drawX, backgroundTiles[i].y, 0, 0)
    end
    --

    gfx.sprite.update()

    if cursorSprite.selectedTile then
        local x1, y1 = gridToScreen(cursorSprite.selectedTile.tileX, cursorSprite.selectedTile.tileY)
        local x2, y2 = gridToScreen(cursorSprite.tileX, cursorSprite.tileY)
		gfx.setColor(gfx.kColorXOR)
        gfx.drawLine(x1, y1, x2, y2)
		gfx.setColor(gfx.kColorBlack)
    end
    
    
    
    gfx.setColor(gfx.kColorWhite)
    gfx.fillRect(0,0,150,20)
    gfx.drawText("Score: *".. score.."*", 0, 0, gfx.kAlignLeft)

    pd.drawFPS(380,0)

    -- Check columns
    for i = 1, 5 do
        --shape
        local shapeSet = {}
        for j = 1, 5 do
            shapeSet[getColumn(i)[j].shape] = true
        end

        local shapeCount = 0
        for _ in pairs(shapeSet) do shapeCount += 1 end

        if shapeCount == 1 or shapeCount == 5 then
            --pattern
            local patternSet = {}
            for j = 1, 5 do
                patternSet[getColumn(i)[j].pattern] = true
            end

            local patternCount = 0
            for _ in pairs(patternSet) do patternCount += 1 end

            if patternCount == 1 or patternCount == 5 then
                print("column " .. i .. " solved")
                table.insert(solvedColumns, i)
                solveSound:play()
            end
        end
        
    end

    

    -- Now check rows separately
    for i = 1, 5 do
        --shape
        local shapeSet = {}
        for j = 1, 5 do
            shapeSet[getRow(i)[j].shape] = true
        end

        local shapeCount = 0
        for _ in pairs(shapeSet) do shapeCount += 1 end

        if shapeCount == 1 or shapeCount == 5 then
            --pattern
            local patternSet = {}
            for j = 1, 5 do
                patternSet[getRow(i)[j].pattern] = true
            end

            local patternCount = 0
            for _ in pairs(patternSet) do patternCount += 1 end

            if patternCount == 1 or patternCount == 5 then
                print("row " .. i .. " solved")
                table.insert(solvedRows, i)
                solveSound:play()
            end
        end
    end

    

    -- Solve after detection
    for _, i in ipairs(solvedRows) do
        solveRow(i)
    end
    solvedRows = {}

    for _, i in ipairs(solvedColumns) do
        solveColumn(i)
    end
    solvedColumns = {}
end


i just read a similar post and found out it’s impossible :confused:

Huh? anything is possible when you’re a programer! There is a lot of irrelevant code in what you posted. So it took a bit to figure out what was going on, but scrolling the background is totally possible. Many games already do this!
an easier way to do you background instead of making a table and stuff, would be to just use the drawTiled function for images. Inside Playdate
Then just do something like:
scrollX = (scrollX + scrollSpeed) % tileWidth
drawX = -tileWidth + scrollX
pattern:drawTiled( drawX, 0, 400+tileWidth, 240, false)

No the problem was having the background appear behing the sprites on the screen. I could make the background scroll, it’s just that updating the image can only happen after the sprites have already been loaded. So, yes, I can make a scrolling pattern but I can’t make it appear behind the sprites. It kinda beats the point of a “background”

Sorry forgot to say thank you for responding! :grin:

Oh because the sprite wont re-draw unless their rect is marked dirty. You should be able to get around this by setting AlwaysRedraw to true. Or mark the rect 0,0, 400, 240 as dirty.

edit: mark it dirty before you call sprite.update!

i am so sorry. I am kinda new to this. What does ‘marking something dirty’ mean?

The default behavior of the Sprite system is to keep track of what sprites have changed since the last frame. It does this so that if nothing has changed it doesn’t need to re-draw to the screen. (This is to improve performance as one of the most computationally expensive things to do is draw to the screen)
So if a sprite doesn’t move, collide, or overlap with another sprite it wont know that it needs to re-draw.
In our example here, we are just using the regular draw functions to draw to the whole screen with a moving pattern. So none of the sprites know they’re no longer visible. So we need to tell them that they are no longer visible, something has changed, they’re dirty and need to be re-drawn.
Reading the docs, it seems like regular drawing should be doing this now too, but I’m not sure if it is. Inside Playdate

I tried this and it didn’t even draw the background /neutral

import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/object"
import "CoreLibs/timer"

pd = playdate
gfx = pd.graphics
sfx = playdate.sound
sprite = gfx.sprite

shapeTiles = gfx.imagetable.new('images/ShapeTiles')
twoXShapeTiles = gfx.imagetable.new('images/2xShapeTiles')
score = 0
solvedRows = {}
solvedColumns = {}
backgroundTiles = {}
scrollSpeed = 0.5
scrollX = 0
gameState = 'Start Screen'

--MAKES IMAGE DATA FOR THE BACKGROUND
local pattern = gfx.image.new('images/pattern/1')
patternW, patternL = pattern:getSize()
for i = 0, 240/patternL+2 do
	for j = 0,400/patternW+2 do 
		table.insert(backgroundTiles, {x = patternW*j,y = patternL*i})
	end
end
--

local data = pd.datastore.read()
if data == nil then
    highscore = 0
else
    highscore = data.highscore or 0
end


print("Highscore loaded: " .. highscore)
timerSeconds = 30
difficultyMultiplier = 0.95
multiplyAmount = 1

local losingPopUp = sprite.new(gfx.image.new('images/Frame'))
losingPopUp:moveTo(200,120)
losingPopUp:setScale(2,1.5)

--Creates the timerTilDeath
timerTilDeath = pd.timer.new(timerSeconds*1000,0,1)
timerTilDeath.timerEndedCallback = function()
    gameState = "Game Over"
end
--


border = sprite.new(gfx.image.new('images/Frame'))
border:moveTo(32*3+32,32*3+32)
border:setScale(1.5,1.5)
border:add()

playdate.getSystemMenu():addMenuItem("Restart game", function()
    restartGame()
end)

playdate.graphics.setLineWidth(3)
pd.display.setInverted(true)

-- make sure the resource name matches the actual file (no spaces)
backgroundMusic = sfx.sampleplayer.new("sounds/BackgroundMusic")
if backgroundMusic then
    backgroundMusic:play()
else
    print("Warning: background music not found at 'sounds/BackgroundMusic'")
end
solveSound = sfx.sampleplayer.new("sounds/Solved")
selectSound = sfx.sampleplayer.new("sounds/Select")
swapSound = sfx.sampleplayer.new("sounds/Swap")


-- helper to convert grid coordinates to screen coordinates
function gridToScreen(x, y)
    return 32 * x + 32, 32 * y + 32
end

import "tile"
import "cursor"

function buttonGlyph(text)
    text = text:gsub("~A", 'Ⓐ')
    text = text:gsub("~B", 'Ⓑ')
    text = text:gsub("~DPad", '✛')
    text = text:gsub("~Menu", '⊙')
    text = text:gsub("~Crank", '🎣')
    return text
end

tileGrid = {
	tile(1,1),tile(2,1),tile(3,1),tile(4,1),tile(5,1),
	tile(1,2),tile(2,2),tile(3,2),tile(4,2),tile(5,2),
	tile(1,3),tile(2,3),tile(3,3),tile(4,3),tile(5,3),
	tile(1,4),tile(2,4),tile(3,4),tile(4,4),tile(5,4),
	tile(1,5),tile(2,5),tile(3,5),tile(4,5),tile(5,5)
}

cursorSprite = cursor(3,3)

function getTile(x, y)
    return tileGrid[(y - 1) * 5 + x]
end


function getRow(y)
    local row = {}
    for x = 1, 5 do
        table.insert(row, getTile(x, y))
    end
    return row
end

function getColumn(x)
    local col = {}
    for y = 1, 5 do
        table.insert(col, getTile(x, y))
    end
    return col
end

function solveColumn(x, multiplyAmount)
	multiplyAmount = multiplyAmount or 1
	for i = 1,5 do 
		getColumn(x)[i]:reroll()
	end
	score += 1 * multiplyAmount
    timerSeconds *= difficultyMultiplier
    -- recreate the timer with the new (shorter) duration
    timerTilDeath:remove()
    timerTilDeath = pd.timer.new(timerSeconds*1000, 0, 1)
    timerTilDeath.timerEndedCallback = function()
        gameState = "Game Over"
    end
end

function solveRow(x, multiplyAmount)
	multiplyAmount = multiplyAmount or 1
	for i = 1,5 do 
		getRow(x)[i]:reroll()
	end
	score += 1 * multiplyAmount
    timerSeconds *= difficultyMultiplier
    -- recreate the timer with the new (shorter) duration
    timerTilDeath:remove()
    timerTilDeath = pd.timer.new(timerSeconds*1000, 0, 1)
    timerTilDeath.timerEndedCallback = function()
        gameState = "Game Over"
    end
end

function restartGame()
	for i = 1, #tileGrid do
		tileGrid[i]:reroll()
	end
	score = 0
    if timerTilDeath then
        timerTilDeath:remove()
    end
    timerSeconds = 60
    timerTilDeath = pd.timer.new(timerSeconds*1000,0,1)
    timerTilDeath.timerEndedCallback = function()
        gameState = "Game Over"
    end
    timerTilDeath:reset()
end 

function playdate.gameWillTerminate()
    pd.datastore.write(highscore)
end

function playdate.gameWillSleep()
    pd.datastore.write(highscore)
end

function pd.update()  
    --SCROLLING EVERY UPDATE
	scrollX = (scrollX + scrollSpeed) % patternW
    drawX = -patternW + scrollX
    pattern:drawTiled(drawX, 0, 400+patternW, 240)
    --

    sprite.addDirtyRect(0, 0, 400, 240) -- I added this line

    gfx.sprite.update()
    pd.timer.updateTimers()


    if gameState == 'Playing' then
        losingPopUp:remove()

        if cursorSprite.selectedTile then
            local x1, y1 = gridToScreen(cursorSprite.selectedTile.tileX, cursorSprite.selectedTile.tileY)
            local x2, y2 = cursorSprite.x-12, cursorSprite.y-12
            gfx.setColor(gfx.kColorXOR)
            gfx.drawLine(x1, y1, x2, y2)
            gfx.setColor(gfx.kColorBlack)
        end
        
        gfx.setColor(gfx.kColorWhite)
        gfx.fillRect(3,33,24,190)

        gfx.setColor(gfx.kColorBlack)
        gfx.fillRect(5,35,20,186)

        gfx.setPattern({0xFF,0x00,0xFF,0x00,0xFF,0x00,0xFF,0x00})
        gfx.fillRect(7,37,16,182*timerTilDeath.value)
        
        gfx.setColor(gfx.kColorWhite)
        gfx.fillRect(0,0,400,20)
        gfx.drawText("Score: *".. score.."*", 0, 0, gfx.kAlignLeft)

        pd.drawFPS(0,0)

        -- Check columns
        for i = 1, 5 do
            --shape
            local shapeSet = {}
            for j = 1, 5 do
                shapeSet[getColumn(i)[j].shape] = true
            end

            local shapeCount = 0
            for _ in pairs(shapeSet) do shapeCount += 1 end

            if shapeCount == 1 or shapeCount == 5 then
                --pattern
                local patternSet = {}
                for j = 1, 5 do
                    patternSet[getColumn(i)[j].pattern] = true
                end

                local patternCount = 0
                for _ in pairs(patternSet) do patternCount += 1 end

                if patternCount == 1 or patternCount == 5 then
                    local multiplyAmount = 1
                    for j = 1, 5 do 
                        if getColumn(i)[j].type == "2x" then
                            multiplyAmount *= 2
                        end
                    end
                    print("column " .. i .. " solved")
                    table.insert(solvedColumns, {index = i, multiply = multiplyAmount})
                    solveSound:play()
                end
            end
            
        end

        

        -- Now check rows separately
        for i = 1, 5 do
            --shape
            local shapeSet = {}
            for j = 1, 5 do
                shapeSet[getRow(i)[j].shape] = true
            end

            local shapeCount = 0
            for _ in pairs(shapeSet) do shapeCount += 1 end

            if shapeCount == 1 or shapeCount == 5 then
                --pattern
                local patternSet = {}
                for j = 1, 5 do
                    patternSet[getRow(i)[j].pattern] = true
                end

                local patternCount = 0
                for _ in pairs(patternSet) do patternCount += 1 end

                if patternCount == 1 or patternCount == 5 then
                    local multiplyAmount = 1
                    for j = 1, 5 do 
                        if getRow(i)[j].type == "2x" then
                            multiplyAmount *= 2
                        end
                    end
                    print("row " .. i .. " solved")
                    table.insert(solvedRows, {index = i, multiply = multiplyAmount})
                    solveSound:play()
                end
            end
        end

        

        -- Solve after detection
        for _, solveData in ipairs(solvedRows) do
            solveRow(solveData.index, solveData.multiply)
        end
        solvedRows = {}

        for _, solveData in ipairs(solvedColumns) do
            solveColumn(solveData.index, solveData.multiply)
        end
        solvedColumns = {}
    elseif gameState == "Game Over" then

        losingPopUp:add()
        gfx.drawTextAligned("Your Score: *".. score.."*", 150, 100, gfx.kAlignCenter)
        if score > highscore then
            highscore = score
            pd.datastore.write({highscore = highscore})  -- save as table for persistence
        else
            gfx.drawTextAligned("Highscore: *".. highscore.."*", 150, 120, gfx.kAlignCenter)
        end
            gfx.drawTextAligned(buttonGlyph("Press ~A to Restart"), 130, 160, gfx.kAlignCenter)
        if pd.buttonJustPressed(pd.kButtonA) then
            restartGame()
            gameState = 'Playing'
        end
    elseif gameState == "Start Screen" then
        local titleImage = gfx.image.new('images/title')
        titleImage = titleImage:scaledImage(2)
        local titleBackground = gfx.image.new('images/titleBackground')
        titleBackground:drawAnchored(0,0,0,0)
        titleImage:drawAnchored(200,80,0.5,0.5)
        gfx.setColor(gfx.kColorWhite)
        gfx.fillRect(190,150,400,35)
        gfx.drawTextAligned(buttonGlyph("Press ~A to Start"), 200, 160, gfx.kAlignCenter)
        if pd.buttonJustPressed(pd.kButtonA) then
            restartGame()
            gameState = 'Playing'   
        end
    end
end


When working with sprites, the official way to draw a backround is to use a separate background drawing function that you register with playdate.graphics.sprite.setBackgroundDrawingCallback. Sprites will call this function when they want to draw a part of the background and you can force redraw of the whole background with playdate.graphics.sprite.redrawBackground().

Another option is to make your background a sprite too and call :setZIndex(-100) so that it'll be drawn before anything else. This works fine if you're always updating the whole screen anyway.

thanks you so much this worked!