A new, possibly better idea about room tiling. That may be enough for the day. We’ll see.

Yesterday I thought it would be interesting to have the big map show the tiled floor, and perhaps even the other room contents. So I did a quick experiment where I tiled every room, by pasting the ExpandedRoom code into the main Room draw. The result was interesting:

all tiled

I liked the effect, but it taught me some things. Suppose we decided that expanded view showed the room you’re in, but also any adjacent rooms you’ve already visited, to the extent that they’d fit on the screen. Recall that I drew the room walls outside the main tiling of the floor.1

But our design deals with “adjacent” rooms, which are rooms within a few pixels of each other along some wall. So there’s no room for thick walls that extend outside a given room, because they’ll extend into the adjacent rooms. No big deal, we’ll put walls inside the room boundaries somehow. I’m sure we can sort that out.

But when drawing the whole map as it expands, adjusting rooms for no overlap, the program visibly slows down. That’s becasue we tile the floors like this:

    local w = self.w//8
    local h = self.h//8
    for xi = 0,w-1 do
        for yi = 0,h-1 do
            local x = xi*8 - self.w//2
            local y = yi*8 - self.h//2
            if xi==-1 or xi==w or yi==-1 or yi==h then
                sprite(asset.builtin.Blocks.Greystone,x,y,8,8)
            else
                
                sprite(asset.builtin.Blocks.Stone,x,y,8,8)
            end
            rect(x,y,9,9)
        end
    end

So for a typical room, we have perhaps 100 or more tiles to draw, each one as a separate sprite call. With 50 rooms on screen, that’s a lot of tiles to draw, and it slows even my fastest possible iPad. So we’ve gotta do something.

At first, I thought there might be a way to create a single flooring sprite, large enough for the largest possible room, and then slice it somehow at display time. Unfortunately, Codea doesn’t have the ability to crop a sprite. You can scale, but not crop. So, Plan B.

Plan B

Codea has the notion of an “image”, which is an array of pixels. You can set Codea’s context to draw into the image just as if you were drawing on the screen. So let’s give each room its own image, right-sized for it, tiled once and then saved. It seems to me that this should be pretty straightforward.

Again, I see no useful way to TDD this. I wish I had a clever pair with me who had a good idea about that, but it should be easy enough to go ahead.

And, after a bit more hackery than I can be proud of, I’ve got this:

function Room:withNumber(n)
    local r = Room()
    r.number = n
    r:createFloor()
    return r
end

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,self.h)
    setContext()
    popStyle()
    popMatrix()
    return self.flooring
end

function Room:draw()
    pushMatrix()
    fill(255)
    stroke(255)
    strokeWidth(0)
    textMode(CENTER)
    self:drawGeometry()
    popMatrix()
    self:drawDoors()
end

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

The self:debugFrame() draws a red rectangle around the room borders. I used it to be sure that the sprite was aligned properly with the room, which has to be done visually.

So whats going on here? When we create our room using the withNumber method, we call createFloor, which creates an image with the dimensions of the room, and tiles it with our standard floor tile and little rectangles. That code is essentially lifted from the ExpandedRoom, which we’ll address in a moment.

When the room is drawn, we “just” draw the room’s flooring as a single sprite (instead of 50 or 100 separate sprites). The effect is this:

moving

The rooms are now drawn with little bitty floors. The code isn’t lovely yet. We’ll get to that. But first, let’s see if we can make the ExpandedRoom class use our new flooring. That should just come down to calling the room’s draw method, if I’ve done this right. (That is, of course, always open to question.)

Here’s what I thought should work:

function ExpandedRoom:draw()
    background(40, 40, 50)
    self:drawGeometry()
    self:drawDescription()
end

function ExpandedRoom:drawGeometry()
    pushMatrix()
    pushStyle()
    translate(WIDTH/2, HEIGHT/2)
    scale(8)
    self.room:draw() -- <---
    spriteMode(CORNER)
    rectMode(CORNER)
    noFill()
    stroke(0)
    strokeWidth(1)
    --self:drawDebug()
    self:drawDoors()
    popStyle()
    popMatrix()
    self:drawPopulation()
end

Contrary to my hopes and expectations, this does not work:

no floor

I see no evidence of a floor anywhere. Also I wish I had taken a checkpoint. Let’s try this differently, in smaller steps.

I should mention that I’m not sure the ExpandedRoom is quite the thing, but I wanted to work with it for now.

function ExpandedRoom:drawGeometry()
    pushMatrix()
    pushStyle()
    translate(WIDTH/2, HEIGHT/2)
    scale(8)
    spriteMode(CENTER)
    rectMode(CENTER)
    noFill()
    stroke(0)
    strokeWidth(1)
    sprite(self.room.flooring,0,0)
    --self:drawDebug()
    self:drawDoors()
    popStyle()
    popMatrix()
    self:drawPopulation()
end

This actually draws correctly:

room ok

If you look carefully at that image, you can see that there is a light boundary on the top and right. The dark border rectangle doesn’t quite reach the edge. I suspect that’s because of an alignment problem in our base sprite. The indices of images go from 1 to whatever, and I’m drawing at 0 to whatever. Let’s see if changing createFloor will fix that.

I did this:

function Room:createFloor()
    pushMatrix()
    pushStyle()
    self.flooring = image(self.w,self.h)
    setContext(self.flooring)
    rectMode(CORNER)
    spriteMode(CORNER)
    stroke(0)
    strokeWidth(1)
    noFill()
...
    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

I tried the final rectangle at (1,1, w,h) and then it missed out the left and bottom. So I cheated and made it go from 0,0 to w+1,h+1 and it covers properly. It’s all about that trying to draw the lines inside code, which has been confusing Codea users since the early 1700s. Now I get this:

wall ok

Now I wonder why the cut off parts of the tiles aren’t symmetric. Let me turn off that rectangle and see the tiles.

no wall

It’s not quite right. Some quick attempts at fiddling don’t make it better. I’m declaring it good enough for now.

However, I believe I should be able to call back to Room’s draw method rather than drawing the sprite in ExpandedRoom. There’s so much junk in the methods that I’m not sure why it does what it does. Let’s remove as much as we can.

In ExpandedRoom, this looks good:

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

Now we “should” be able to replace the sprite call with

    self.room:draw()

When we do, nothing draws. Oh, I wonder if we should be calling drawGeometry. Let’s look at Room.

function Room:draw()
    pushMatrix()
    fill(255)
    stroke(255)
    strokeWidth(0)
    textMode(CENTER)
    self:drawGeometry()
    popMatrix()
    self:drawDoors()
end

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

Ah, we’re surely translating off into space with that translate call. We could move that up to draw but honestly I think we’re wasting our time here. For now, we’ll draw the sprite and move on to more interesting things.

Let’s commit: all rooms have tiled floors.

Let’s also wrap it up until tomorrow.

Wrapping Up

An experiment led to an idea that seems nearly good to me: What if rooms had their flooring built in and could display it at whatever scale they’re drawn.

I’ve got somewhat hacked code in place to give every room its own floor image, which it displays as needed. There’s some issue with displaying indirectly in expanded mode, but I’m sure that with a clear head we can find and fix that. Meanwhile we can display the same tiling directly.

I’m sure that if we display two adjacent rooms, the tiling won’t line up, because each room’s tiling is positioned on the room’s own coordinates. If we wait to draw the tiling until the rooms are all positioned (which is not the case now), we could adjust the room’s tiles to be aligned on a general grid rather than a room-based grid. That would make all the tiles line up even between rooms.

The code right now is a bit nasty: the graphical functions do enough pushing and popping and centering and cornering to make them unclear, and unable to be reused at scale. Some refactoring should take care of that.

Summing up, we’ve made a small step toward a playable game, and I think it’s a useful step even though imperfect.

See you next time!

D1.zip


  1. (V.O., Morgan Freeman if available) As usual, Ron says “I” when he’s talking about a mistake and “we”” when things are going fairly well. Ron just doesn’t want anyone to feel badly about his mistakes. He certainly doesn’t.