Two nice little things. A short report. Thanks to GeePaw for one of them. A weird explanation for the other.

I fiddled with two ideas on my other iPad yesterday, amid hanging with my dear wife and being exposed to the television she was watching, and eating the munchies which she kindly prepared from time to time. And I came up with two nice things.

Halls with Walls

The first addresses this issue: presently, hallways are just one tile wide, with black “edge” tiles outside:

narrow halls

That may be OK, but I thought I’d prefer them to be outlined with wall tiles, like rooms are. So I stole yet another idea from GeePaw Hill. When he draws his room connections, which he does an entirely different way from the one here, at one point he searches adjacent tiles to a possible path point, to see if it has collided with anything.

It occurred to me that I could examine my map to find room tiles that have edge tiles adjacent, and replace those edge tiles with wall tiles. So I did this:

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

function Tile:fillColor()
    if self.kind == TileEdge and self:hasRoomNeighbor() then
        self.kind = TileWall
    end
    local c = self:fillColors()[self.kind]
    return c or color(255,0,0)
end

function Tile:fillColors()
    if not FillColors then
        FillColors = {wall=color(128,128,128),   room=color(255),   edge=color(0) }
    end
    return FillColors
end

function Tile:hasRoomNeighbor()
    local ck = { {1,0},{1,1},{0,1},{-1,1}, {-1,0},{-1,-1},{0,-1},{1,-1}}
    for i,p in ipairs(ck) do
        local tx,ty = self.x +p[1], self.y + p[2]
        local tile = Runner:getTile(tx,ty)
        if tile:isRoom() then return true end
    end
    return false
end

The “trick” is in that last function, hasRoomNeighbor, which just checks all the possible tile positions around the current one, and answers whether any is of type room. We call it with edge tiles and if the answer is yes, we turn the edge tile into a wall tile.

I also changed the way the colors are represented, by symbol names “wall” instead of those integers. I just liked it better.

This code may look beyond the limits of the dungeon’s tiles, so to let it inquire without interruption, I modified this:

function GameRunner:getTile(x,y)
    if x<=0 or x>self.tileCountX or y<=0 or y>self.tileCountY then
        return Tile:edge(x,y)
    end
    return self.tiles[x][y]
end

Now we just return an edge tile if we’re probing into outer space. That’s even what we’d actually want if we were going to allow that in general.

With those changes in place, we get nice grey walls around all our hallways:

with walls

Commit: halls have walls.

Scrolling Limits

The other issue has to do with displaying the zoomed map, which is that centering the princess can cause the map to slide off the screen, leaving a gap. When baby’s in a corner:

corner

We get this effect, which isn’t all that attractive:

corner zoom

I fixed that with some code that I’ll try to explain. I confess that I did it by trial and error, which I’ll describe below. The change is all in a new version of one function. Here it is in the old form:

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

This is called when we want to zoom, and all it does is zoom and then translate from x,y to the center (w,h) of the scaled screen. That puts whatever’s at x,y at screen center. Presently x and y are wherever I touch the screen. In due time it’ll be at the avatar position.

The new function looks like this:

function focus(x,y, zoom)
    local tx,ty
    scale(zoom)
    local appliedScale = modelMatrix()[1]
    local w = WIDTH/2/appliedScale
    local h = HEIGHT/2/appliedScale
    tx,ty = w-x, h-y
    tx,ty = math.min(0,tx), math.min(0,ty)
    local mul = (appliedScale-1)/appliedScale
    tmx = -WIDTH*mul
    tmy = -HEIGHT*mul
    tx = math.max(tmx,tx)
    ty = math.max(tmy,ty)
    translate(tx,ty)
end

I’ll try to explain in a moment but first here’s what happens. With the fix, when we zoom this:

corner2

We get this:

corner2zoom

The screen doesn’t scroll to show blank space. Instead, it limits the amount of the translation, with that long and involved code. We will talk about that shortly.

There’s another little change that I made to make things slightly better. 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

Becomes 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+1 do
        self.tiles[x] = {}
        for y = 1,self.tileCountY+1 do
            local tile = Tile:edge(x,y)
            self:setTile(tile)
        end
    end
end

Note that I am creating extra tiles at the ends of the tile list. Since the rest of the program thinks the limits are tileCountX and tileCountY, they’ll never be used. But they make sure that if the screen size isn’t divisible by tileSize, we’ll get partial tiles drawn out to the screen edge. You can see it the fringe in the pictures above, because for some reason the iPad screen is not a convenient power of anything. Repeating:

corner2

And zoomed:

corner2zoom

You can see the fringe there to the right. So that’s a bit nicer and we’ll commit that to: scaling does not go off screen.

Since I did the work on my old iPad, I had to resort to sneaker-net techniques to move the code over. I hope I”ve got it all. I think I could probably configure Working Copy to point to a shared location, and I could certainly hook it to GitHub or some secondary repo like that, but I’ve not gone to the trouble of doing that. If I do much more actual stuff on the old iPad, it may come to that.

Let me explain …

No, there is too much. Let me sum up.

I swear that I used to understand translation and scaling and even rotation cold, back in my math days. But much of the jelly in my head has drained out over the years, so I worked this out by trial and error.

Consider a picture:

paper

Here we have our full screen, and a rectangle on it representing the area we’ll see at scale. Looks about like scale 3 here. Now imagine that the princess is down by 0,0 and we (initially) want to bring her to center screen. We’ll translate the viewpoint down and to the left by whatever the lengths of those red lines is, and voila! there she is. However, if we do that, the 0,0 point will be at screen center, and thus 3/4 of the screen will be nothing but background.

It was clear right away that I shouldn’t allow the scrolling to go beyond zero, which is where this code comes from:

function focus(x,y, zoom)
    local tx,ty
    scale(zoom)
    local appliedScale = modelMatrix()[1]
    local w = WIDTH/2/appliedScale
    local h = HEIGHT/2/appliedScale
    tx,ty = w-x, h-y
    tx,ty = math.min(0,tx), math.min(0,ty)

Min works because of the way the signs work. With that code in place, the screen never scrolls left or below zero and we see nice pictures like this:

corner4

Which zooms without showing a gap, at the cost–of course–of not centering the room:

corner4zoom

The left and top were more difficult. It started out messy and wound up almost good:

function focus(x,y, zoom)
    local tx,ty
    scale(zoom)
    local appliedScale = modelMatrix()[1]
    local w = WIDTH/2/appliedScale
    local h = HEIGHT/2/appliedScale
    tx,ty = w-x, h-y
    tx,ty = math.min(0,tx), math.min(0,ty)

    local mul = (appliedScale-1)/appliedScale
    tmx = -WIDTH*mul
    tmy = -HEIGHT*mul
    tx = math.max(tmx,tx)
    ty = math.max(tmy,ty)
    translate(tx,ty)
end

Above the blank line, we’re dealing with keeping the left and bottom margins from reaching below zero. The code below the blank line deals with keeping the top and right margins from going above the screen limits.

(This code works because we know that the tiles exactly will the screen. It occurs to me now that doing this code in terms of tiles might make it more clear. It’s always nice when we have these little talks.)

The magic is in the multiplier:

    local mul = (appliedScale-1)/appliedScale

What in the world is that? Let me first explain how I got it. I added a bunch of prints to this code, saying what the computed values were of the translation I did, and various computations that I thought were possible useful. Then I’d tap the screen on the right and look at the numbers.

Then–I really did this–I’d divide those numbers into screen size, divide them by the phase of the moon, anything that I thought might work, because I knew there would be a simple relationship but I couldn’t retrieve it from my enfeebled brain.

I finally got some code that clearly worked. I regret not saving it, sort of, but I didn’t. Then I refactored it, on screen and on paper, until the relationship dropped out:

    (appliedScale-1)/appliedScale

I was boggled. You mean at scale 2 our magic number is 1/2 but at scale three it is 2/3 and scale 4 it is 3/4? What in the heck is that?

Finally it hit me. Look again at our drawing:

paper

In that picture, our scale is 3. So how far to the left can we slide the picture before the right edge comes into screen view? Two thirds! Scaling to scale N leaves N screen’s worth of display to deal with. We dare not slide the Nth one, because it’ll bring the edge into view. So we get (n-1)/n.

Weird but true.

The rest was just hammering the code into place. I’ll clean it up a bit along the way somewhere. The old iPad has no keyboard, so editing is a serious pain.

Anyway, that’s today’s story and I’m sticking to it. See you next time!

D2.zip