Scrolling

Hi All,

There are many ways to scroll backgrounds from simple sprite drawing to 3D skybox rotation. I have tried my hand at them all but still find myself not knowing the best way to build code for each separate, with different requirements, projects.

One instance I have is drilling a large 2D map which is populated with objects that change move and interact - sprite or mesh?

Another instance involves a psuedo 3D scrolling terrain which is large but also wraps on the edges. It uses a 3D object for the hero and the plain scrolls in all directions.

Finally, not got to this stage yet, but a full infinite scrolling terrain with 3D hero and multi level shaded landscape.

Some of the recent posts from @dave1707 have whetted my appetite again and I have been digging out the old half finished projects.

Just wondering if some of our better programmers could fire in a little help to point me and other users in the direction of best options, and things to consider when trying to code them.

Hi All,

Just a more specific query, if you have a large mesh terrain which would display well over the limits of the screen - can you select a subset of the mesh for display? Objective to minimise resources and keep tight control on screen output.

This can be done with sprites and large images.

@Bri_G I hacked one of my games to show scrolling over a terrain that measures 5,000,000 by 5,000,000 pixels. You can go even higher depending on the map you use. If the map gets spread to thin, it doesn’t look too good. I tried 1,000,000,000 by 1,000,000,000 pixels and the FPS was still around 60. I don’t think you need a terrain that large. At the speed I have the ship going (100 pixels per second) it would take about 115 days to go 1,000,000,000 pixels. So that’s probably a large enough terrain for a game without having to resort to subsets.

PS. Placing objects on the terrain can probably be done in subsets.

displayMode(FULLSCREEN)

function setup() 
    fill(255)
    hgx=Gravity.x
    speed,ey,ang=2,45,0
    cameraX,cameraZ=0,0
    scene = craft.scene()
    scene.camera.position = vec3(cameraX,0,cameraZ)
    scene.camera.eulerAngles=vec3(0,ey,0)
    scene.sun.rotation = quat.eulerAngles(45,0,45)
    scene.ambientColor = color(90,90,90)
    skyMaterial = scene.sky.material
    skyMaterial.sky = color(252, 255, 0, 255)
    skyMaterial.horizon = color(0, 203, 255, 255)    
    createFloor()
end

function update(dt)
    scene:update(dt)
    scene.camera.position = vec3(cameraX,1,cameraZ)
    scene.camera.eulerAngles=vec3(0,ey,0)
end

function draw()
    background(0)
    update(DeltaTime)
    scene:draw()
    ey=ey-ang
    xVel=speed*math.sin(math.rad(ey))
    zVel=speed*math.cos(math.rad(ey)) 
    xDir=math.sin(math.rad(ey))
    zDir=math.cos(math.rad(ey)) 
    cameraX=cameraX+xVel
    cameraZ=cameraZ+zVel    
    gx=Gravity.x
    ang=ang+(gx-hgx)*4
    hgx=gx
    pushMatrix()
    translate(WIDTH/2,HEIGHT/2-100)
    rotate(ang*-30)
    sprite("Tyrian Remastered:Boss A",0,0,300)
    translate()
    popMatrix()
    text(1/DeltaTime,WIDTH/2,HEIGHT-25)
    text((cameraX//1).."  "..(cameraZ//1),WIDTH/2,HEIGHT-50)
end

function createFloor(x,z)
    c1=scene:entity()
    w=c1:add(craft.rigidbody,STATIC)
    c1.model = craft.model.cube(vec3(5000000,1,5000000))
    c1.position=vec3(x,0,z)
    c1.material = craft.material("Materials:Standard")
    c1.material.map = readImage("Surfaces:Basic Bricks AO")
    c1.material.offsetRepeat=vec4(0,0,5000,5000)
end

@dave1707 - very impressive but it slightly misses the point. You have stretched one icon over the full size of the texture. In a large terrain Iwould like to fill a texture with multiple small images. Whilst trying to generate that I came across a real oddity - explain this:


displayMode(FULLSCREEN)
function setup() 
    txture()
end

function draw()

end

function txture()
    --
    plain = image(256*48,256*48)
    tl = 0
    spriteMode(CENTER)
    setContext(plain)
    for y = 0, 255 do
        for x = 0, 255 do
            sprite("Cargo Bot:Crate Blue 2",x*42+21,y*42+21,42,42)
            tl = tl+1
            if tl == 27 then tl = 0 end
        end
    end
    setContext()
end

Note my iPad has a lot of memory.

@Bri_G this is an infinite scrolling sprite canvas modulated by noise. Turn on DebugDraw to see the clipping window version

-- Scroller

function setup()
    displayMode(STANDARD)
    
    parameter.boolean("DebugDraw", false)
    
    pos = vec2(WIDTH/2, HEIGHT/2)
    
    print("Drag the screen to scroll the map")
end

function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(5)
    
    translate(pos.x, pos.y)
    
    local tileSize = 128
    local grid = vec2(WIDTH/tileSize, HEIGHT/tileSize)
    
    if DebugDraw then
        grid = vec2(512/tileSize, 512/tileSize)
        translate(WIDTH/4, HEIGHT/4)
    end
    
    local boundsStart = -pos + (grid * tileSize)/2
    
    spriteMode(CENTER)
    stroke(200, 29, 29, 255)
    noFill()
    
    local gridStart = vec2(math.floor(boundsStart.x/tileSize), math.floor(boundsStart.y/tileSize))
    local tileStart = gridStart * tileSize
    local p = vec2()
    
    for x = -grid.x/2, grid.x/2 + 1 do
        for y = -grid.y/2, grid.y/2 + 1 do
            p = tileStart + vec2(tileSize * x, tileSize * y)
 
            local shade = math.max((noise(100 + p.x * 0.05, 100 + p.y * 0.05, 100) + 1.0)/2.0, 0)
            shade = math.min(shade + 0.5, 1.0)
            tint(255 * shade, 255 * shade, 255 * shade)            
            
            sprite("Blocks:Grass Top", p.x, p.y)        
        end
    end    
    
    if DebugDraw then
        rectMode(CENTER)
        rect(boundsStart.x, boundsStart.y, grid.x * tileSize, grid.y * tileSize)
    end
end

function touched(touch)
    if touch.state == MOVING then
        pos = pos + vec2(touch.deltaX, touch.deltaY)
    end
end

For my terrain i use the terrain class developed by @ignatz. It uses a mesh, so one can have hills and valleys in the terrain. I modified it to use the @loopspace pseudoMesh instead of Mesh, this way it can be converted to a craft model and react to the craft lighting and physics.

@Bri_G Here’s another version. I didn’t try to solve every issue with this, but just to give an idea. It starts out with 1 terrain area, but it creates a random new area as you fly over a blank area. The more you fly the larger your terrain gets. Still not sure if this helps any, but was interesting to write.

displayMode(FULLSCREEN)

function setup() 
    map={"Surfaces:Basic Bricks AO","Surfaces:Basic Bricks Color",
    "Surfaces:Basic Bricks Normal","Surfaces:Basic Bricks Roughness",
    "Surfaces:Desert Cliff AO","Surfaces:Desert Cliff Color",
    "Surfaces:Desert Cliff Height","Surfaces:Desert Cliff Normal",
    "Surfaces:Desert Cliff Roughness"}
    
    size=20    
    fill(255)
    hgx=Gravity.x
    speed=.2
    ey,ang=45,0
    cameraX,cameraZ=0,0
    
    scene = craft.scene()
    scene.camera.position = vec3(cameraX,0,cameraZ)
    scene.camera.eulerAngles=vec3(0,ey,0)
    scene.sun.rotation = quat.eulerAngles(45,0,45)
    scene.ambientColor = color(90,90,90)
    
    skyMaterial = scene.sky.material
    skyMaterial.sky = color(0, 185, 255, 255)
    skyMaterial.horizon = color(116, 226, 198, 255)    
    
    mat={}
    createFloor(vec3(0,0,0))   
    table.insert(mat,vec2(cameraX//size,cameraZ//size))
end

function update(dt)
    scene:update(dt)
    scene.camera.position = vec3(cameraX,1,cameraZ)
    scene.camera.eulerAngles=vec3(0,ey,0)
end

function draw()
    background(0)
    update(DeltaTime)
    scene:draw()
    ey=ey-ang
    xVel=speed*math.sin(math.rad(ey))
    zVel=speed*math.cos(math.rad(ey)) 
    xDir=math.sin(math.rad(ey))
    zDir=math.cos(math.rad(ey)) 
    cameraX=cameraX+xVel
    cameraZ=cameraZ+zVel    
    gx=Gravity.x
    ang=ang+(gx-hgx)*4
    hgx=gx
    pushMatrix()
    translate(WIDTH/2,HEIGHT/2-300)
    rotate(ang*-30)
    sprite("Tyrian Remastered:Boss A",0,0,300)
    translate()
    popMatrix()
    text(1/DeltaTime,WIDTH/2,HEIGHT-25)
    text((cameraX//1).."  "..(cameraZ//1),WIDTH/2,HEIGHT-50)
    
    px=cameraX//size
    py=cameraZ//size
    found=false
    for z=1,#mat do
        if px==mat[z].x and py==mat[z].y then
            found=true
            break
        end
    end 
    if not found then
        table.insert(mat,vec2(px,py))
        createFloor(vec3((px)*size,0,(py)*size))   
    end
end

function createFloor(pos)
    local c1=scene:entity()
    c1.model = craft.model.cube(vec3(size,.1,size))
    c1.position=vec3(pos.x+size/2,pos.y,pos.z+size/2)
    c1.material = craft.material("Materials:Standard")
    c1.material.map = readImage(map[math.random(9)])
    c1.material.offsetRepeat=vec4(0,0,size,size)
end

@Simeon - thanks for that neat demo, surprising how little code it took to achieve that. Few things I can apply that to, keep me busy for a while. Thanks again.

@dave1707 - that’s really neat, never seen that done before, it has given me a few ideas. Could be a problem if it keeps on expanding - how easy would it be to duplicate the one you are about to leave beyond the one you are about to move into? That way you could have infinite terrain in just a few graphic areas.

@Bri_G That’s what I was doing in a previous version. I was keeping the eight grids around me and deleting the terrain that I didn’t need anymore. But I figured it was better to keep everything. I’m not sure how many grids can be created before the fps starts to drop.

@Simeon - just noted a peculiarity in your scrolling demo. When I run the demo and scroll, say right to left, there is a flickering on with the new tiles being added on the right. Replace Standard mode with Overlay and the flickering disappears. The Overlay mode is drawing more of the tiled surface but appears faster. Is there an issue with calculations in the Standard mode eliminating the tiles below the parameter window?

@Bri_G odd, I don’t see any flickering in STANDARD or FULLSCREEN modes. They are supposed to go all the way to the edges of the screen (you shouldn’t be able to see the tiles appear and disappear unless “DebugDraw” is on).

It could be my iPad screen size (Pro 11") is more neatly divisible by the tileSize.

Try changing line 27 to this:

    local grid = vec2(math.floor(WIDTH/tileSize)+1, math.floor(HEIGHT/tileSize)+1)

Edit: Or try +2 if that’s not enough

@Simeon - bingo, got that in one. The modification made that smooth. Is there any advantage in changing the size of the parameter window?

Size of the parameter window? As in whether Codea is in Fullscreen or Standard size?

@Bri_G Here’s the other scroll I had. It’s set for a 3x3 grid terrain. It creates new terrain in front of you and deletes the terrain behind you to keep you in the middle. It can be changed to 5x5 or 7x7, etc. Because I’m scrolling fast, you can see a slight delay as new terrain is created and the old terrain deleted. If you move at a slower speed, it might not be as noticeable. You can also change the size of each grid. I guess this can be considered an infinite terrain.

PS. This eventually crashes, but I don’t know if it me or a problem with Codea not releasing memory.

displayMode(FULLSCREEN)

function setup() 
    map={"Surfaces:Basic Bricks AO","Surfaces:Basic Bricks Color",
    "Surfaces:Basic Bricks Normal","Surfaces:Basic Bricks Roughness",
    "Surfaces:Desert Cliff AO","Surfaces:Desert Cliff Color",
    "Surfaces:Desert Cliff Height","Surfaces:Desert Cliff Normal",
    "Surfaces:Desert Cliff Roughness","Surfaces:Stone Brick Color",
    "Surfaces:Stone Brick Height","Surfaces:Stone Brick Metalness",
    "Surfaces:Stone Brick Normal","Surfaces:Stone Brick Roughness",}
    
    terrainSize=1  -- 1=3x3 grid  2=5x5 grid 3=7x7 grid
    size=30 -- size of each grid   
    hgx=Gravity.x
    speed,ey,ang=0,0,0
    cameraX,cameraZ=0,0
    
    scene = craft.scene()
    scene.camera.position = vec3(cameraX,0,cameraZ)
    scene.camera.eulerAngles=vec3(0,ey,0)
    scene.sun.rotation = quat.eulerAngles(45,0,45)
    scene.ambientColor = color(90,90,90)
    
    skyMaterial = scene.sky.material
    skyMaterial.sky = color(0, 185, 255, 255)
    skyMaterial.horizon = color(116, 226, 198, 255)    

    tab={}
    px,py=0,0
    newPosition(vec3(0,0,0))
    fill(255)
end

function update(dt)
    scene:update(dt)
    scene.camera.position = vec3(cameraX,1,cameraZ)
    scene.camera.eulerAngles=vec3(0,ey,0)
end

function draw()
    background(0)
    update(DeltaTime)
    scene:draw()
    ey=ey-ang
    local xVel=speed*math.sin(math.rad(ey))
    local zVel=speed*math.cos(math.rad(ey)) 
    local xDir=math.sin(math.rad(ey))
    local zDir=math.cos(math.rad(ey)) 
    cameraX=cameraX+xVel
    cameraZ=cameraZ+zVel    
    local gx=Gravity.x
    ang=ang+(gx-hgx)*4
    hgx=gx
    
    pushMatrix()
    translate(WIDTH/2,HEIGHT/2-300)
    rotate(ang*-30)
    sprite("Tyrian Remastered:Boss A",0,0,300)
    translate()
    popMatrix()
    
    px=cameraX//size
    py=cameraZ//size   
    text(px.."  "..py,WIDTH/2,HEIGHT-25)
    if speed==0 then
        text("tap screen to start",WIDTH/2,HEIGHT-50)
    else
        text("tap screen to stop",WIDTH/2,HEIGHT-50)
    end
    newPosition()
end

function newPosition()
    r=math.random(1000000)
    for x=-terrainSize,terrainSize do
        for y=-terrainSize,terrainSize do
            a=string.format("%d%d",px+x,py+y)
            if tab[a]==nil then
                local c=createFloor(vec3(px+x,0,py+y))
                tab[a]={x=px+x,y=py+y,r=r,c=c}
            else
                tab[a].r=r
            end
        end
    end 
    if hx~=px or hy~=py then
        oldPosition(hr)
    end
    hr=r  
    hx,hy=px,py
end

function oldPosition(r)
    for a,b in pairs(tab) do
        if b.r==r then
            tab[a].c:destroy()     
            tab[a]=nil    
        end        
    end
end

function createFloor(pos)
    local c1=scene:entity()
    c1.model = craft.model.cube(vec3(size,.1,size))
    c1.position=vec3(pos.x*size,pos.y,pos.z*size)
    c1.material = craft.material("Materials:Standard")
    c1.material.map = readImage(map[math.random(#map)])
    c1.material.offsetRepeat=vec4(0,0,size,size)
    return c1
end

function touched(t)
    if t.state==BEGAN then
        if speed>0 then
            speed=0
        else
            speed=.1
        end
    end
end

@Simeon - forget my last note the issue I was discussing was due, as you pointed out, to the matching of integral tile sizes and will vary from tile size to tile size.

I always set up in Overlay mode as then you only have two possible set-up orientations to deal with. Which just made me think - in the past someone mentioned setting up their own template - is this still possible? And, if so how?

Also, you might consider letting people build their own templates, which would all list below the naming field for new projects, so that you can select it when starting a new project. I think this has been suggested before.

@Bri_G good idea. Maybe I’ll make it if you put projects in a folder called “Template” then they show up in the template picker

@dave1707 I’m so going to see if I can reproduce this crash with your new project

@dave1707 - just ran your new terrain demo, really smooth - going to see if I can play a few tunes around that. Oh, ran it 20 times for few minutes and stopped it without crashing. Probably memory, which of your machines are you seeing it on and is it consistent across them?

@Bri_G I’m using a 16gb iPad Air and that’s what I see all the crashes on. I have a 128gb iPad Pro, but I rarely use it. When I try those programs on it, it doesnt crash. Maybe I just don’t wait long enough.