More tiling, trying to make all the floors align.

Yesterday I did a rough tiling of the floors, setting up images for each room, so that they’d be drawn with one large sprite rather than n-squared little ones. That allows showing the big map with flooring:

big map

However, the tiles are arranged with their origin in their lower left corner. That means that if we display adjacent rooms, their flooring won’t line up. No self-respecting dungeonmaster would tolerate a shoddy job like that. So today I want to align all the flooring on a master grid. I’ll also need to display more than one room aligned, to be sure it works. Right now, with a very messy scaling hack, we can see that they don’t line up yet.

bad alignment

That picture is just scaling all the rooms up, but not adjusting their positions as well. We’ll need to do better than that, just to see what’s going on, and I suspect that will require some better organization of the display process. We’ll see.

First, the alignment. I drew this picture:

paper drawing

And I coded a little experiment:

function setup()
    local center = vec2(331.5, 431.5)
    local wh = vec2(24,36)
    local vL = center - wh
    local vH = center + wh
    local vStart = smush(vL)
    local vEnd = smush(vH)
    print(vL,"",vH,"",vStart,"",vEnd, "", vStart/8, vEnd/8)
end

function smush(aVector)
    return vec2(aVector.x//8*8, aVector.y//8*8)
end

That prints:

(307.500000, 395.500000) (355.500000, 467.500000)
(304.000000, 392.000000) (352.000000, 464.000000)
(38.000000, 49.000000)	(44.000000, 58.000000)

Which makes me think that tiling from vStart through vEnd would do the job for us. Our current floor creation will be found in Room:

function Room:createFloor()
    pushMatrix()
    pushStyle()
    self.flooring = image(self.w,self.h)
    setContext(self.flooring)
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(0)
    strokeWidth(1)
    noFill()
    local w = self.w
    local h = self.h
    for x = 1,w,8 do
        for y = 1,h,8 do
            sprite(asset.builtin.Blocks.Stone,x,y,8,8)
            rect(x,y,9,9)
        end
    end
    stroke(100,100,100)
    strokeWidth(4)
    rect(0,0 ,self.w+1,self.h+1) -- grotesque hackery but fills the image
    setContext()
    popStyle()
    popMatrix()
    return self.flooring
end

There’s not much good to say about that, other than that it manages to create an image. At the time I wrote it, that was about all I was ready for. Now we get to change it. The code of interest is this bit:

    local w = self.w
    local h = self.h
    for x = 1,w,8 do
        for y = 1,h,8 do
            sprite(asset.builtin.Blocks.Stone,x,y,8,8)
            rect(x,y,9,9)
        end
    end

There is a possible concern. I’ve determined that to write a sprite into the lower left corner of an image, you have to set it to (0,0), not (1,1), despite the fact that the image’s indices start at (1,1). Yes, that’s bizarre. But it does suggest to me that the code above, which we’re replacing, isn’t quite right, and that it should be:

    for x = 0,w-1,8 do
        for y = 0,h-1,8 do

And when I change that, the rooms do turn out to have symmetrical flooring, which was not the case before:

sym floor

So that’s interesting. You may be wondering why I tried that, given that I’m going to replace that code. There were at least two reasons.

First, since I had learned something about sprites and images, that code looked wrong, and I wanted to verify my learning. That seemed to me to be worth the few moments it took to do it.

Second, although my little print this morning was working in world coordinates for the room, I like the simplicity of the 1,w or even 0,w-1 construction, and I am wondering whether we could retain that, and offset the individual coordinates at the last minute. If we do that, we’ll probably have to bump up the high index to cover the far edges.

I’m going to try that: I think the code may wind up more clear.

Uh oh. I have forgotten a key fact. For this to work at all, we have to have the room in its final position. Right now, we build the flooring when we create the room. To align the flooring, we’ll have to do it when the moving is done. Wow, I’m glad I remembered that.

But now I have more ideas in the air than I like. Let’s change that quickly and come back to this thinking.

function drawDungeonCreation()
    background(40, 40, 50)
    if CodeaUnit then showCodeaUnitTests() end
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    for i,r in ipairs(Rooms) do
        r:draw()
    end
    if AdjustCount > 0 then
        if ElapsedTime - AdjustTime > AdjustDelay then
            AdjustTime = ElapsedTime
            AdjustCount = Room:adjustAll(Rooms)
            if AdjustCount == 0 then
                Room:setAllToInteger(Rooms)
                Rooms[1]:colorNeighborsIn(Rooms, color(255,0,0,100))
            end
        end
    end
    popStyle()
    popMatrix()
end

That’s the code that draws the big map. The AdjustCount stuff is the code that allows the map animation as the rooms jostle for position. When that’s done, AdjustCount is zero, and the code in the if runs. We can put a new method there:

            if AdjustCount == 0 then
                Room:setAllToInteger(Rooms)
                Room:createFloors(Rooms)
                Rooms[1]:colorNeighborsIn(Rooms, color(255,0,0,100))
            end

And then I’ll remove the current call and build this method:

function Room:createFloors(rooms)
    for k,r in pairs(rooms) do
        r:createFloor()
    end
end

I expect this to show the rooms moving with no flooring and then for flooring to suddenly appear.

Well, that would happen if we didn’t try to draw the sprite before we have it:

function Room:drawGeometry()
    pushMatrix()
    pushStyle()
    translate(self.x,self.y)
    scale(Scale)
    spriteMode(CORNER)
    rectMode(CORNER)
    noFill()
    stroke(0)
    strokeWidth(1)
    spriteMode(CENTER)
    sprite(self.flooring, 0,0)
    --self:debugFrame()
    popStyle()
    popMatrix()
end

I decide to do this:

    if self.flooring then
        sprite(self.flooring, 0,0)
    else
        self:debugFrame()
    end

That seems to do the trick:

jostle

Commit: floors created after jostling. And this is a good time to get my chai.

Mission Accomplished

Well, at least I’m back with my chai, not a very challenging mission but there you are.

Let’s extract that loop into a method, just to make it easier to look at:

function Room:createFloor()
    pushMatrix()
    pushStyle()
    self.flooring = image(self.w,self.h)
    setContext(self.flooring)
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(0)
    strokeWidth(1)
    noFill()
    self:applyFlooring()
    stroke(100,100,100)
    strokeWidth(4)
    rect(0,0 ,self.w+1,self.h+1) -- grotesque hackery but fills the image
    setContext()
    popStyle()
    popMatrix()
    return self.flooring
end

function Room:applyFlooring()
    local w = self.w
    local h = self.h
    for x = 0,w-1,8 do
        for y = 0,h-1,8 do
            sprite(asset.builtin.Blocks.Stone,x,y,8,8)
            rect(x,y,9,9)
        end
    end
end

Now we just need a constant offset, based on the difference between our corners and corners congruent to zero mod 8. (This is the tile size. We’ll have an issue if we ever want a different size of tile. Same in your bathroom, really. For now, no worries.)

Since our rooms are now guaranteed to be divisible by 8 in x and y, we could work from center, but let’s not.

function Room:applyFlooring()
    local w = self.w
    local h = self.h
    local xL = self.x - w/2
    local yL = self.y - h/2
    local xS = xL//8*8
    local yS = yL//8*8
    local xd = xS - xL
    local yd = yS - yL
    for x = 0,w-1,8 do
        for y = 0,h-1,8 do
            sprite(asset.builtin.Blocks.Stone,x+xd,y+yd,8,8)
            rect(x,y,9,9)
        end
    end
end

I intentionally didn’t go up to h and w, because I wanted to see this picture:

miissed edge

Notice the black and top and right. Since we’re offsetting downward, we get that effect. Now to bump that up:

covered edge

We’re covering the right and top, but there’s that little glitch at the bottom … that’s why I was letting it origin at 1,1, isn’t it? I’d forgotten that tweak. Here, let’s just add one to our coordinates:

function Room:applyFlooring()
    local w = self.w
    local h = self.h
    local xL = self.x - w/2
    local yL = self.y - h/2
    local xS = xL//8*8
    local yS = yL//8*8
    local xd = xS - xL
    local yd = yS - yL
    for x = 0,w,8 do
        for y = 0,h,8 do
            sprite(asset.builtin.Blocks.Stone,x+xd+1,y+yd+1,8,8)
            rect(x,y,9,9)
        end
    end
end

I’m reverting. I’ve lost the thread somewhere. Let’s go back and do it again. I think part of the issue is that I need to offset the little rectangle as well as the sprite.

OK, back to here:

function Room:createFloor()
    pushMatrix()
    pushStyle()
    self.flooring = image(self.w,self.h)
    setContext(self.flooring)
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(0)
    strokeWidth(1)
    noFill()
    local w = self.w
    local h = self.h
    for x = 0,w-1,8 do
        for y = 0,h-1,8 do
            sprite(asset.builtin.Blocks.Stone,x,y,8,8)
            rect(x,y,9,9)
        end
    end
    stroke(100,100,100)
    strokeWidth(4)
    rect(0,0 ,self.w+1,self.h+1) -- grotesque hackery but fills the image
    setContext()
    popStyle()
    popMatrix()
    return self.flooring
end

Extract the tiling operation:

function Room:createFloor()
    pushMatrix()
    pushStyle()
    self.flooring = image(self.w,self.h)
    setContext(self.flooring)
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(0)
    strokeWidth(1)
    noFill()
    self:applyFlooring()
    stroke(100,100,100)
    strokeWidth(4)
    rect(0,0 ,self.w+1,self.h+1) -- grotesque hackery but fills the image
    setContext()
    popStyle()
    popMatrix()
    return self.flooring
end

function Room:applyFlooring()
    local w = self.w
    local h = self.h
    for x = 0,w-1,8 do
        for y = 0,h-1,8 do
            sprite(asset.builtin.Blocks.Stone,x,y,8,8)
            rect(x,y,9,9)
        end
    end
end

Now with the zero in there, we get the colored boundary on the left and bottom. The 1,1 starting point was in there to fudge that. But we can’t fudge it now, because we’re starting our sprites to the left and down, so there will always be pixels there. We may have to fiddle with the outer rectangle or something. We’ll leave that for now.

Compute the x and y deltas and adjust the loop:

function Room:applyFlooring()
    local w = self.w
    local h = self.h
    local xL = self.x - w//2
    local yL = self.y - h//2
    local xS = xL//8*8
    local yS = yL//8*8
    local xD = xL-xS
    local yD = yL-yS
    for x = 0,w,8 do
        for y = 0,h,8 do
            sprite(asset.builtin.Blocks.Stone,x-xD,y-yD,8,8)
            rect(x-xD,y-yD,9,9)
        end
    end
end

Final picture, perhaps good:

final

The wall is definitely off on the lower left, but that was expected. I’m not sure about the offsetting, as it looks very symmetrical. I’ll run some different rooms. OK, they’re coming out offset. Here’s an example:

asym

Now for that rectangle around the walls. I was thinking I might have to draw separate lines but I tried this:

    strokeWidth(4)
    rect(-1,-1 ,self.w+2,self.h+2) -- grotesque hackery but fills the image

That fills the room to the edge with walls:

good wall

Maybe it’d be better a bit wider? No, let’s go with this. I believe we have offset flooring now. Commit: offset flooring.

Now Then …

I’d like to see this in operation. However, the current expanded room display only knows how to display one room, and it displays it at the center of the screen:

function ExpandedRoom:drawGeometry()
    pushMatrix()
    pushStyle()
    translate(WIDTH/2, HEIGHT/2)
    scale(8)
    spriteMode(CENTER)
    sprite(self.room.flooring,0,0) -- should ideally call self.room:drawGeometry?
    self:drawDoors()
    popStyle()
    popMatrix()
    self:drawPopulation()
end

The good news about using scale and translate is that it’s easy to get simple things to happen simply. Less good is that they can be tricky to think about when you apply more than one at a time, unless you do it all in a very regular, organized, structured kind of way.

What if we were to have this method not do the translate, but instead draw the room at its official x and y location … with a translation done outside this method that puts this room at the center? Then if we drew some adjacent rooms with the same settings, they ought to display adjacently.

So we want to say:

    sprite(self.room.flooring,self.x,self.y)

For that to work, we should translate to:

    translate(WIDTH/2-self.x, HEIGHT/2-self.y)

Unless I have it backward. No, what I have is that I forgot that I have to get x and y from self.room.

    local x,y = self.room.x, self.room.y
    translate(WIDTH/2 - y, HEIGHT/2 - y)
    scale(8)
    spriteMode(CENTER)
    sprite(self.room.flooring, x,y)

And the scale has to go in there. As I say, the structuring needs to be good and ours isn’t.

function ExpandedRoom:drawGeometry()
    pushMatrix()
    pushStyle()
    local x,y = self.room.x, self.room.y
    translate(WIDTH/2 - 8*x, HEIGHT/2 - 8*y)
    scale(8)
    spriteMode(CENTER)
    sprite(self.room.flooring, x,y) -- should ideally call self.room:drawGeometry?
    self:drawDoors()
    popStyle()
    popMatrix()
    self:drawPopulation()
end

This does display correctly, so I can move the translate outside:

function ExpandedRoom:draw()
    background(40, 40, 50)
    pushMatrix()
    translate(WIDTH/2 - 8*self.room.x, HEIGHT/2 - 8*self.room.y)
    self:drawGeometry()
    popMatrix()
    self:drawDescription()
end

This works but breaks drawPopulation, my little hack that draws the princess and the treasure chest. I’m going to let that slide until I sort out the current task. I want to draw the surrounding rooms right at this point.

First try:

ARRGH. This code is bad. There are too many people relying on secret knowledge about the screen translation and scaling. I’m going to revert again, back to “offset flooring”, which is the place where we have properly applied the tiling, and do the display more nearly right.

OK, I think the revert took. Now then, again.

Reasonable Transforms

What are reasonable graphical transforms by my definition? It has been a long time since I had this at my fingertips but I think these are basic principles:

  1. Never write code that knows what the current scaling, translation, or rotation are;
  2. All objects draw themselves as if they were at location 0,0, typically centered there.
  3. Objects can use transformations to draw themselves if they choose, but must undo any transforms that they use.

If we get this right, we should be able to draw the expanded map by simply scaling and translating just ahead of our normal draw loop.

Now it “happens” that we are starting all our rooms inside screen bounds, and when we jostle them they haven’t been seen to go outside. The main map drawing is done at scale 1 and translation to WIDTH/2, HEIGHT/2. We would like the scaled drawing to be done at scale 8, and with the translation such that the target room (room 1 for now) is at screen center.

Now I think we should just get rid of the ExpandedRoom altogether, and scale the whole map such that it looks big enough, with the room of interest centered. So I’ll be working toward that.

An Experiment

To clarify my thinking without making me get out my copy of Foley and van Dam to read about transformations, I wrote a little experimental program:

-- translate

function setup()
    parameter.number("Scale",1,10,1)
    parameter.integer("Item",0,3,0)
    discs = {}
    red = color(255,0,0)
    grn = color(0,255,0)
    blu = color(0,0,255)
    C = {x=WIDTH/2,y=HEIGHT/2}
    D1 = Disc(400,400,red)
    D2 = Disc(500,500,grn)
    D3 = Disc(600,600,blu)
    items = {C,D1,D2,D3}
end

function getScale()
    return modelMatrix()[1]
end

function getTrans()
    local m = modelMatrix()
    return m[13],m[14]
end

function draw()
    local item = items[Item+1]
    local x = item.x
    local y = item.y
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    ellipseMode(CENTER)
    background(40, 40, 50)
    scale(Scale)
    local currentScale = getScale()
    translate(WIDTH/currentScale/2-x,HEIGHT/currentScale/2-y)
    D1:draw()
    D2:draw()
    D3:draw()
    popStyle()
    popMatrix()
    rectMode(CENTER)
    rect(WIDTH/2, HEIGHT/2, 10,10)
end

Disc = class()

function Disc:init(x,y, color)
    self.x = x
    self.y = y
    self.color = color
end

function Disc:draw()
    pushStyle()
    pushMatrix()
    translate(self.x,self.y)
    fill(self.color)
    stroke(255)
    strokeWidth(2)
    ellipse(0,0,40,40)
    popMatrix()
    popStyle()
end

This program has a little class called Disc. Each instance is created with a color and coordinates on the screen. They draw themselves by translating to that location and drawing an ellipse. (They could just as well draw the ellipse at that location, but doing the translation leaves other options for a more complicated drawing.)

The program takes two parameters, Scale and Item. Item selects either one of the discs, when its value is 1, 2, or 3, and it selects a “fake disc” when the value is 0. (I chose indices that way rather than the more Codea-like 1-4 because zero reminded me of “nothing” and I think of it as not selecting a disc.

Note this code:

    scale(Scale)
    local currentScale = getScale()
    translate(WIDTH/currentScale/2-x,HEIGHT/currentScale/2-y)

We zoom the screen to whatever the Scale parameter says, then set local currentScale to the value of getScale(), which is:

function getScale()
    return modelMatrix()[1]
end

I had to reach fairly deep in Codea’s bag of tricks for this one. modelMatrix is a transform matrix representing Codea’s current drawing mode. We’ll not go into the details, but if you’re familiar with 3D transforms, this is a pretty typical matrix, although it is mirrored around the major axis for a reason I’ll come to in a moment.

Either way, the main diagonal has the scale on x, then y, then z. Since we only use uniform scaling, I can get the current scale by fetching the first element.

Why did I do that instead of using the value of Scale that I had just set into the scale function? Because I want to be able to find out the scale without any rigmarole to save the current scaling. I felt it was better to go to the official source.

There’s also a function getTrans that I wrote, which can be used to find out any translation that may be in effect. It looks like this:

function getTrans()
    local m = modelMatrix()
    return m[13],m[14]
end

Codea has arranged that the x, y, z translation is in cells 13,14,15 of the matrix. Normally in a transform matrix, we’d find those values in cells 4, 8, and 12. For our (relative) convenience, Codea reflects the matrix around the main diagonal. We don’t care. For our purposes we might want the values, but we’re not going to be inverting and applying actual matrices any time soon. Or ever, if I have any say about it.

So, all this was just to be sure that I have things right for expanding the display while looking at a given room. That’s what this is about:

    translate(WIDTH/currentScale/2-x,HEIGHT/currentScale/2-y)

Normally the screen view is centered at WIDTH/2, HEIGHT/2: with no transforms in place, the center of the screen is at the center of the screen. So if we want to put the object that is drawn at (x,y) at the center, we know that it will do translate(x,y) or equivalent before it draws. So if we first translate from center X,Y to X-x, Y-y, his call to translate will cancel out and voila! we’ve got him at the center.

Except that if we’re scaled by currentScale we have to divide currentScale out.

If this is not obvious, believe me, you’re not alone. I used to use these transforms in anger for years and I still have trouble thinking about them sometimes. Anyway, as you can see in the following video, the code we have can track any of our discs without a problem.

tracking

So with all that in hand, I think I can get back to displaying rooms. Let’s return to the dungeon.

Workin in a Coal Mine

(Oops, about to slip down.)1

Did I mention that I stopped working on this article about 10 AM Thursday and today is Friday? Well, it’s true. So I have no idea whatsoever what I had in mind to do that long ago.

The basic objective is to change the zoomed view so that the target room (Room 1 for now) is centered, but that we draw the adjacent rooms around it. I’m thinking to do this without the ExpandedRoom at all, instead by adjusting the view in the main draw. That code is:

function draw()
    if DisplayExpanded then
        ExpandedRoom:drawRoom()
    else
        drawDungeonCreation()
    end
end

function drawDungeonCreation()
    background(40, 40, 50)
    if CodeaUnit then showCodeaUnitTests() end
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    for i,r in ipairs(Rooms) do
        r:draw()
    end
    if AdjustCount > 0 then
        if ElapsedTime - AdjustTime > AdjustDelay then
            AdjustTime = ElapsedTime
            AdjustCount = Room:adjustAll(Rooms)
            if AdjustCount == 0 then
                Room:setAllToInteger(Rooms)
                Room:createFloors(Rooms)
                Rooms[1]:colorNeighborsIn(Rooms, color(255,0,0,100))
            end
        end
    end
    --[[
    for i,l in pairs(AllDoors) do
        l:draw()
    end
    ]]--
    popStyle()
    popMatrix()
end

Let’s tidy that up a bit before we fiddle with it.

function drawDungeonCreation()
    background(40, 40, 50)
    if CodeaUnit then showCodeaUnitTests() end
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    for i,r in ipairs(Rooms) do
        r:draw()
    end
    adjustPositions()
    popStyle()
    popMatrix()
end

function adjustPositions()
    if AdjustCount > 0 then
        if ElapsedTime - AdjustTime > AdjustDelay then
            AdjustTime = ElapsedTime
            AdjustCount = Room:adjustAll(Rooms)
            if AdjustCount == 0 then
                Room:setAllToInteger(Rooms)
                Room:createFloors(Rooms)
                Rooms[1]:colorNeighborsIn(Rooms, color(255,0,0,100))
            end
        end
    end
end

Now the actual drawing bit is somewhat better. Now for the main draw:

function draw()
    if DisplayExpanded then
        focusOnRoomOne()
    end
    drawDungeonCreation()
end

We’ll just have the one way of drawing, presently named “creation” but we can change that when we have things working and can see what we’re up to.

I figure we just have to position and scale in the focus function:

function focusOnRoomOne()
    scale(8)
    local w = WIDTH/2/8
    local h = HEIGHT/2/8
    translate(w-Rooms[1].x, h-Rooms[1].y)
end

Yes, after working out all that generality, I just typed in the values. We’ll get some generality shortly, I think, but we don’t need it now. Here’s the result:

rooms align

That looks good. And after a few test runs, here’s a view that rather clearly shows that the tiles all line up:

tiles aligned

Now let’s tidy up some mess. I think I’ll just removed the whole ExpandedRoom class for now. It’s not used in our zoomed view, and it’s not really capable of anything interesting. That goes smoothly, there was only the one reference to it, in the draw, and we removed that.

I see some redundant calls to things like rectMode and textMode. Let’s follow the convention that we set such things right where we need them, using push and pop to make sure the effects are local. We’ll not try to set something like that outside just to save time.

I’ll spare you the deletes and moves of those.

I’d better commit this, hadn’t I? Commit: rooms expand in place.

I’m not fond of this code that creates the floor sprite for a room:

function Room:createFloor()
    pushMatrix()
    pushStyle()
    self.flooring = image(self.w,self.h)
    setContext(self.flooring)
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(0)
    strokeWidth(1)
    noFill()
    self:applyFlooring()
    stroke(100,100,100)
    strokeWidth(4)
    rect(-1,-1 ,self.w+2,self.h+2) -- grotesque hackery but fills the image
    setContext()
    popStyle()
    popMatrix()
    return self.flooring
end

function Room:applyFlooring()
    local w = self.w
    local h = self.h
    local xL = self.x - w//2
    local yL = self.y - h//2
    local xS = xL//8*8
    local yS = yL//8*8
    local xD = xL-xS
    local yD = yL-yS
    for x = 0,w,8 do
        for y = 0,h,8 do
            sprite(asset.builtin.Blocks.Stone,x-xD,y-yD,8,8)
            rect(x-xD,y-yD,9,9)
        end
    end
end

All those locals there in the applyFlooring were intended to help me understand what’s going on. At the time, those names meant something to me. They mean less now, a day or two later.

Let’s group them by coordinate and see if that helps give us an idea:

    local w = self.w
    local xL = self.x - w//2
    local xS = xL//8*8
    local xD = xL-xS

and similarly for Y. So what’s going on there?

Well, xL is the x coordinate of the lower left corner of the room. And xS is the multiple of 8 coordinate less than or equal to xS. That’s the point where the tiling needs to start so that it’s aligned everywhere. And xD is the difference between those, some small number of pixels.

So then we draw the little sprites into our image:

    for x = 0,w,8 do
        for y = 0,h,8 do
            sprite(asset.builtin.Blocks.Stone,x-xD,y-yD,8,8)
            rect(x-xD,y-yD,9,9)
        end
    end

So the first one goes at 0-xD, or 0-(xL-xS) (and xL>=xS, so we’re backing off from 0 a bit, namely the x offset of the basic sprite. Let’s rename those last variables, maybe x and y Offset, or Adjustment?

function Room:applyFlooring()
    local w = self.w
    local xL = self.x - w//2
    local xS = xL//8*8
    local xOffset = xL-xS
    
    local h = self.h
    local yL = self.y - h//2
    local yS = yL//8*8
    local yOffset = yL-yS
    
    for x = 0,w,8 do
        for y = 0,h,8 do
            sprite(asset.builtin.Blocks.Stone,x-xOffset,y-yOffset,8,8)
            rect(x-xOffset,y-yOffset,9,9)
        end
    end
end

Let’s extract those four-line bits. Note that they’re the same function, applied to different arguments. No, too tricky, we refer to w and h down in the loop. Let’s rename a bit more. Is this any better?

    local w = self.w
    local xLow = self.x - w//2
    local xSpriteAligned = xLow//8*8
    local xOffset = xLow-xSpriteAligned

Maybe xSpriteLow? And I added a comment:

function Room:applyFlooring()
    local w = self.w
    local xLow = self.x - w//2
    local xSpriteLow = xLow//8*8 -- align sprite on multiples of 8
    local xOffset = xLow-xSpriteLow

Ah, the function is something like alignSpriteOnMultiplesOf8.

I’m going to pull this out.

Ron, why are you even doing this? It works!

I’m glad you asked. I’m doing this for two reasons. First, its obscure and I want to learn how to make it less so. Second, if this were a real project, we could be sure someone would be back here asking for a 13-bit sprite or something and we’d need to understand it. Yes, it’s mostly just drill now, or practice. But to me, it’s valuable practice.

I’m going to pull both those code patches out together, and pay the price of calling for w and y again:

function Room:applyFlooring()
    local xOffset,yOffset = self:alignmentAdjustment()
    
    for x = 0,self.w,8 do
        for y = 0,self.h,8 do
            sprite(asset.builtin.Blocks.Stone,x-xOffset,y-yOffset,8,8)
            rect(x-xOffset,y-yOffset,9,9)
        end
    end
end

function Room: alignmentAdjustment()
    local w = self.w
    local xLow = self.x - w//2
    local xSpriteLow = xLow//8*8 -- align sprite on multiples of 8
    local xOffset = xLow-xSpriteLow
    
    local h = self.h
    local yL = self.y - h//2
    local yS = yL//8*8
    local yOffset = yL-yS
    
    return xOffset,yOffset
end

Now the 8 is pretty magic. Let’s name it:

function Room: alignmentAdjustment()
    local alignOn = 8
    local w = self.w
    local xLow = self.x - w//2
    local xSpriteLow = xLow//alignOn*alignOn
    local xOffset = xLow-xSpriteLow
    
    local h = self.h
    local yL = self.y - h//2
    local yS = yL//alignOn*alignOn
    local yOffset = yL-yS
    
    return xOffset,yOffset
end

There’s a possibly better way to do that //x*x trick:

function Room: alignmentAdjustment()
    local alignOn = 8
    local w = self.w
    local xLow = self.x - w//2
    local xSpriteLow = xLow - xLow%alignOn
    local xOffset = xLow-xSpriteLow
    
    local h = self.h
    local yL = self.y - h//2
    local yS = yL - yL%alignOn
    local yOffset = yL-yS
    
    return xOffset,yOffset
end

Renaming …

function Room: alignmentAdjustment()
    local alignOn = 8
    local w = self.w
    local xLow = self.x - w//2
    local xSprite = xLow - xLow%alignOn
    local xOffset = xLow-xSprite
    
    local h = self.h
    local yLow = self.y - h//2
    local ySprite = yLow - yLow%alignOn
    local yOffset = yLow-ySprite
    
    return xOffset,yOffset
end

Inline the w and h locals:

function Room: alignmentAdjustment()
    local alignOn = 8
    
    local xLow = self.x - self.w//2
    local xSprite = xLow - xLow%alignOn
    local xOffset = xLow-xSprite
    
    local yLow = self.y - self.h//2
    local ySprite = yLow - yLow%alignOn
    local yOffset = yLow-ySprite
    
    return xOffset,yOffset
end

Now, finally, I notice this:

    local xSprite = xLow - xLow%alignOn
    local xOffset = xLow-xSprite

xLow cancels out there:

    local xOffset = xLow - xLow + xLow%alignOn

Yes! “Obviously” the offset we need is just the remainder after dividing our coordinate by the tile size. Presumably I should have seen that days ago, but I didn’t.

Continuing by rote:

function Room: alignmentAdjustment()
    local alignOn = 8
    
    local xLow = self.x - self.w//2
    local xOffset = xLow%alignOn
    
    local yLow = self.y - self.h//2
    local yOffset = yLow%alignOn
    
    return xOffset,yOffset
end

Now I am tempted to draw some conclusion about the center coordinates vs the corners, but I don’t want this code to rely on anything that arcane. And I’m not sure whether aligning the tiles by centers would still make things line up or not. Seems like it would have to, wouldn’t it?

A quick experiment convinces me that it doesn’t work. I’m not sure why, and I don’t care. We’ll work from the corners.

Which reminds me, we have this:

function Room:corners()
    local hw = self:halfWidth()
    local hh = self:halfHeight()
    return self.x - hw, self.y - hh, self.x + hw, self.y + hh
end

So we can do this:

function Room: alignmentAdjustment()
    local alignOn = 8
    
    local xLow,yLow = self:corners()
    local xOffset = xLow%alignOn
    local yOffset = yLow%alignOn
    
    return xOffset,yOffset
end

And now to inline those two calculations:

function Room: alignmentAdjustment()
    local alignOn = 8
    local xLow,yLow = self:corners()
    return xLow%alignOn, yLow%alignOn
end

So that’s rather nicer than it was:

function Room: alignmentAdjustment()
    local w = self.w
    local xLow = self.x - w//2
    local xSpriteLow = xLow//8*8 -- align sprite on multiples of 8
    local xOffset = xLow-xSpriteLow
    
    local h = self.h
    local yL = self.y - h//2
    local yS = yL//8*8
    local yOffset = yL-yS
    
    return xOffset,yOffset
end

And that result is why I did that.

But there is bad news. Somewhere along the way, the doors have become aligned improperly, and I didn’t notice. If you’re not looking at them, you might not notice, but it’s pretty obvious:

doors wrong

Bummer. I’ll revert, see if they’re good in the reverted version, and then redo this change, which is conveniently right here in the article. But first a quick look to see if I can see what happened.

Ah, fortunately I spotted something in the diff. I removed what seemed like a redundant rectMode(CENTER) from room drawing, but the door drawing relies on it. So this is the fix:

function Room:drawDoors()
    pushMatrix()
    pushStyle()
    fill(255)
    stroke(255)
    rectMode(CENTER)
    for i,door in pairs(self.doors) do
        x,y = door:getDoorCoordinates(self)
        rect(x,y,10,10) 
    end
    popStyle()
    popMatrix()
end

Commit: refactored floor tile alignment for clarity.

Now let’s get outa here and publish. I think readers may be down to zero now. If you read this, please wave or something.

Summing Up

I started yesterday with the goal of aligning all the floors in the whole dungeon. That went pretty smoothly, partly because I’d thought about it a bit and drawn a sketch to visualize the calculation needed. (It turns out I visualized it correctly but in a longer form than was possible.)

After that worked, I wanted to display multiple rooms at the same time, and was beginning to realize that the ExpandedRoom idea was a non-starter. And in working on that code, I realized that my use of transforms was a bit messy, and it seemed I might need to sort it out a lot. By then the morning time had elapsed, and I paused for the day.

Later, I did the experiment with the dots and figured out that I could view the map from anywhere, at any scale, with a rather simple call to the model matrix.

Oh, that reminds me, I never generalized that code:

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

There, I changed it to read the matrix. It seems redundant here but I think it’s better. Also I tried scale 4, which draws a nice segment of the map:

scale 4

Commit: fetch scaling from model matrix in expanded view.

Once that zoomed view was in place, I set out to improve the code a bit. I removed a few “redundant” calls to things like rectMode and got at least one of them wrong. I’m not sure how I could have tested for that, but I certainly could have paid better attention to the picture.

Then I went after what seemed like a perfectly working function that managed the uniform tiling and in a long series of simple tests, reduced 9 lines to 3, while adding some clarity (in my view). Was it worth it? It was to me, but I’m here to entertain myself. A reader should ask herself which version she’d like to look at at 3 AM on a trouble call.

All in all, a reasonably productive two day effort, with a better code base, including removal of a 78-line object that’s no longer needed.

Tomorrow, or Monday, as the case may be, we’ll do something else. I hope to see you then. And don’t forget to wave at me on Twitter.

D1.zip


  1. Lee Dorsey, ca. 1965. Music and lyrics by Allen Toussaint.