I’m fairly satisfied with room and hallway generation. It’s time to work on what the screen looks like in play.

We probably don’t want to show the whole map to the player at any time, certainly not during game play. Instead, we want a zoomed-in view, centered on the player. In version 1 of this program, I had the screen centered on the player and scrolled the game around her. We could do that but we have a finite game board and, as we’ll see in some pictures that I’ll surely take, the tiles can scroll to the point where we see screen background.

I see at least two options for that. It would be easy enough to make the game board produce tiles outside the actual range of the rooms, and it wouldn’t be terribly hard to limit the display to keep the edges from showing.

I do know approximately how hard that is, just a couple of lines of code. When I got here, a few paragraphs ago, I was thinking I’d do that. Now I’m thinking I like the virtual tiles idea better.

Anyway, let’s get started.

First, I think I’ll add in a flag for display mode, so that when I touch the screen, it toggles between full map view and zoomed in view. Along the way, I think I should extend the current batch of tiles out to the edge of the screen. I’m going to assume iPad dimensions for now.

When we create the GameRunner, it immediately produces its tiles. Then we have room creation with createRandomRooms:

function GameRunner:init(count)
    self.tileCount = count or 60
    self.tileSize = 16
    self.tiles = {}
    for x = 1,self.tileCount do
        self.tiles[x] = {}
        for y = 1,self.tileCount do
            local tile = Tile:edge(x,y)
            self:setTile(tile)
        end
    end
end

function GameRunner:createRandomRooms(count)
    self.rooms = {}
    while count > 0 do
        count = count -1
        local timeout = 100
        local placed = false
        while not placed do
            timeout = timeout - 1
            local r = Room:random(60,60, 4,15, self)
            if self:hasRoomFor(r) then
                placed = true
                r:paint()
                table.insert(self.rooms,r)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
    self:connectRooms()
end

Since the iPad screen is a rectangle, not square, we can’t use the count variable in both cases. No one even uses the count on init, so I’ll remove it for now.

Here’s what I’ve got:

full screen

To accomplish that, we have this:

function GameRunner:init()
    self.tileSize = 16
    self.tileCountX = math.floor(WIDTH/self.tileSize)
    self.tileCountY = math.floor(HEIGHT/self.tileSize)
    self.tiles = {}
    for x = 1,self.tileCountX do
        self.tiles[x] = {}
        for y = 1,self.tileCountY do
            local tile = Tile:edge(x,y)
            self:setTile(tile)
        end
    end
end

function GameRunner:createRandomRooms(count)
    self.rooms = {}
    while count > 0 do
        count = count -1
        local timeout = 100
        local placed = false
        while not placed do
            timeout = timeout - 1
            local r = Room:random(self.tileCountX,self.tileCountY, 4,15, self)
            if self:hasRoomFor(r) then
                placed = true
                r:paint()
                table.insert(self.rooms,r)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
    self:connectRooms()
end

Because the screen starts at zero and arrays start at one, I changed tile to do this:

function Tile:draw()
    pushMatrix()
    pushStyle()
    fill(self:fillColor())
    rect(self:gx(),self:gy(),TileSize)
    popStyle()
    popMatrix()
end

function Tile:gx()
    return (self.x-1)*TileSize
end

function Tile:gy()
    return (self.y-1)*TileSize
end

The change was to use x-1 and y-1 to get the graphic coordinates for the tile–its lower left corner– with the lowest leftmost tile at zero-zero.

I expect that this is going to bite us when we place other objects, but we’ll see. There is an alternative to this change. We could translate all the drawing one tile down and to the left instead. I’m a bit concerned about how that would mesh with scaling, so for now, we’ll let it be.

Now for the toggle.

function setup()
    if CodeaUnit then 
        codeaTestsVisible(true)
        runCodeaUnitTests() 
    end
    Runner = GameRunner()
    Runner:createRandomRooms(12)
    --Runner:createTestRooms()
    DisplayToggle = false
end

function draw()
    if CodeaUnit then showCodeaUnitTests() end
    Runner:draw()
end

function touched(aTouch)
    DisplayToggle = not DisplayToggle
end

Now I wonder whether we can do the zooming from this outer level, leaving the game to just do its thing without regard to where the screen is set. I don’t see why not. Let’s begin with a zoom:

function draw()
    pushMatrix()
    if CodeaUnit then showCodeaUnitTests() end
    Runner:draw()
    if DisplayToggle then
        scale(3)
    end
    popMatrix()
end

Strictly speaking, there’s no need to push and pop here, since we are at the top of the drawing stack, but it seems to me to be the proper thing to do. Let’s see what happens.

LOL, it would help if I scaled before drawing. Also I tuned up the touched a bit:

function draw()
    pushMatrix()
    if CodeaUnit then showCodeaUnitTests() end
    if DisplayToggle then
        scale(3)
    end
    Runner:draw()
    popMatrix()
end

function touched(aTouch)
    if aTouch.state == ENDED then
        DisplayToggle = not DisplayToggle
    end
end

The change to touched, once I got it right, ensures that I get just one event per touch. The function fires continually whenever one or more touches are on the screen.

So we get this:

out

in

Clearly we’re zooming such that 0,0 stays fixed. I always have to figure this out again. Years ago, I’d have had it at my fingertips, but no more. I drew this picture in Paper™:

paper

That was enough to convince me that to zoom to the center of the screen, we need to move one-third of a screen to the left and down, since we are scaling at scale 3, like this:

function draw()
    pushMatrix()
    if CodeaUnit then showCodeaUnitTests() end
    if DisplayToggle then
        scale(3)
        local tx,ty = -WIDTH//3, -HEIGHT//3
        translate(tx,ty)
    end
    Runner:draw()
    popMatrix()
end

However, my reasoning is not correct. Suppose we zoom at zoom 4, would dividing by 4 work? In fact it does not. I had this working, I thought, in D1. Let’s go grab that code:

function focusOnRoomOne()
    scale(4)
    local appliedScale = modelMatrix()[1]
    local w = WIDTH/2/appliedScale
    local h = HEIGHT/2/appliedScale
    local x,y = Avatar:screenLocation()
    translate(w-x, h-y)
end

The name of this is bad, it now focuses on the Avatar. But that aside, I thought it worked. Let’s plug it in and play. Here’s how I’ve modified it:

function focus(x,y, zoom)
    scale(zoom)
    local appliedScale = modelMatrix()[1]
    local w = WIDTH/2/appliedScale
    local h = HEIGHT/2/appliedScale
    translate(w-x, h-y)
end

Now I can call with any point, and any scale. Plugging into draw:

function draw()
    pushMatrix()
    if CodeaUnit then showCodeaUnitTests() end
    if DisplayToggle then
        focus(WIDTH//2,HEIGHT//2, 3)
    end
    Runner:draw()
    popMatrix()
end

So far so good. Now let’s see if we can zoom to where we touch. I modify touched:

function touched(aTouch)
    if aTouch.state == ENDED then
        DisplayToggle = not DisplayToggle
        TouchX = aTouch.pos.x
        TouchY = aTouch.pos.y
    end
end

Now in draw:

function draw()
    pushMatrix()
    if CodeaUnit then showCodeaUnitTests() end
    if DisplayToggle then
        focus(TouchX,TouchY, 3)
    end
    Runner:draw()
    popMatrix()
end

This seems to work:

large

small1

small2

small3

It also zooms correctly at different scales. I’m going to back away slowly and commit the code: zoom to touch point.

Player Position

One of these days I’ll work out how that zooming works. For now, let’s let it be.

I’m going to add a very simple Player class to mark player position and check zooming to it.

Player = class()

function Player:init(tileX,tileY, runner)
    self.tileX,self.tileY = tileX,tileY
    self.runner = runner
end

function Player:draw()
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    local gx,gy = self.runner:graphicXY(self.tileX,self.tileY)
    sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,self:gx(),self:gy(), 16,16)
end

I just decided that GameRunner is going to know how to convert tile coordinates into graphical ones.

function GameRunner:graphicXY(tileX,tileY)
    return (tileX-1)*self.tileSize,(tileY-1)*self.tileSize
end

I’ll use that in Tile as well, when I get to it.

Now let’s make an avatar, put her somewhere, and draw her. I think she belongs to the GameRunner and belongs at the center of Room 1.

function GameRunner:createRandomRooms(count)
    self.rooms = {}
    while count > 0 do
        count = count -1
        local timeout = 100
        local placed = false
        while not placed do
            timeout = timeout - 1
            local r = Room:random(self.tileCountX,self.tileCountY, 4,15, self)
            if self:hasRoomFor(r) then
                placed = true
                r:paint()
                table.insert(self.rooms,r)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
    self:connectRooms()
    local r1 = self.rooms[1]
    local rcx,rcy = r1:center()
    self.avatar = Player(rcx,rcy,self)
end

And in draw:

function GameRunner:draw()
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw()
        end
    end
    self.avatar:draw()
end

I have high hopes for this. I have even higher hopes after this:

function Player:draw()
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    local gx,gy = self.runner:graphicXY(self.tileX,self.tileY)
    sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,gx,gy, 16,16)
end

I had only half-converted Player:draw to the graphicXY scheme. Now we get this:

tiny

zoomed

There she is. She’s a bit scrunched, because the dimensions of that sprite are … let me look … 101x171. Here she is at 20x17. Size looks nice, actually, but she’s a bit off centered. I think we should adjust her down and over a bit:

good

This looks good and the code is:

function Player:draw()
    local dx = -2
    local dy = -3
    pushMatrix()
    pushStyle()
    spriteMode(CORNER)
    local gx,gy = self.runner:graphicXY(self.tileX,self.tileY)
    sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,gx+dx,gy+dy, 20,34)
end

Commit: princess in room 1.

Summing Up

It’s Saturday, so I think that’s enough progress for now. We have some interesting capability sketched in here now. We can draw the princess at any tile x and y, and we can zoom the screen around any point. There remains the question of the grey area around the tiles, but I think I can fix that with some fake tiles, perhaps. If not, I’m sure there’s a way to pin the scrolling, but I like the fake tile idea better, it’s less messing with the math.

And I really should see if I can understand and explain how the zooming actually works. But it does. :)

Overall, things are moving forward nicely. So far, I’ve had far fewer of those confusing moments around which way things are aligned and converting from screen to tile coordinates. The tiling idea is proving itself daily.

See you next time!

D2.zip