I don’t usually predict an outcome but today I think something good is going to happen. Let’s find out.

When Ward first spoke about technical debt, he described how, when you move the design from where it is to where you now see that it should be, the system tends to contract and become simpler. I think that’s about to happen here.

Time was, each Tile had a contents collection. That collection was buffered so that when it was being looped over, additions didn’t mess things up. And each item in any tile’s contents had a pointer back to that tile. Today, we have a single collection, DungeonContents, the sole instance of DungeonContentsCollection, which has a mapping from each dungeon object to the tile it resides in. This change has allowed us to remove a member variable from every tile, and from every object that resides in the dungeon. The total savings is at least 10,000, perhaps 15,000 words of memory. So that’s nice.

Today we’re going after some other issues, including:

Ad-Doc Drawing
The code that draws dungeon objects is more than a little ad hoc. It has to be done in a certain order, and because of that, the GameRunner has several collections of objects, and it draws each of those collections, almost always in the proper order.
Z Order
The “right” way to draw things in Codea is to draw them in increasing “z” order, so that objects that are supposed to appear on top will do so, even though OpenGL doesn’t handle stacked transparency well.
Drawing Location
Drawn objects have to know where to draw, and that is generally at the center of the tile in which they reside. They draw differently–usually not at all–on the tiny map that shows the player where they’ve been. The objects all have their own draw methods, which are all quite similar but not entirely so.
Unseen Objects
If the tile an object is “on” is not drawn, that tile’s contents should also not be drawn. At present, this is checked late, and in more than one place.

My Cunning Plan

Let me take you through my vague thoughts during the interval between the cat waking me up, and my actually getting out of bed.

The DungeonContents (DC henceforth) implicitly knows all the tiles that have contents needing to be drawn, because each object points to its tile. The DC doesn’t know this very well, but it would be easy to create the inverse collection. We’d loop over the DC, creating a table for each tile found, putting the contents in there.

Most of those tables would be of size one, but some would be larger, such as a tile with an open chest with a health bottle in it, or a tile with Spikes and the Player, and so on. There would be very few with more than one item, but there can be some.

Creating this table would be easy, but it might be a bit costly to do it 60 or 120 times a second, and even if we can afford it, it seems like temporal duplication, since generally nothing will have changed. So we would prefer just to have whatever “drawing table” would be ideal, keeping it up to date when things change, which is rarely.

What would we really like to have to draw? Well, I’m glad I asked. I think we’d like to draw things by z-order, lowest z first. So we probably would like something like a table of

{ z_order, object, tile }

Maybe we’d find that the ideal table would be a bit different, perhaps some kind of grouping by tile. Until we really write the drawing loop, we can’t really know.

We should also consider another possibility. Drawing would be almost right if we just draw everything in the DC. That might be a good starting point for what we’re about to do.

I just thought of that. Thinking is good. We’ll try that.

We’re going to need every object to implement a z_level method. We’ll need a way to enforce that, or default it.

I don’t know whether you can call that a plan or not. Anyway, it’s a sketch, and it’s what we’ll be working toward this morning. It is just past 0900 right now.

Let’s Get To It

I think it starts getting good right away. Here are just some of GameRunner’s drawing methods:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawMapContents()
    self:drawButtons()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
    self:drawMonstersOnSomeMaps()
    self:drawMessages()
end

function GameRunner:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self:drawMap(false)
    popMatrix()
end

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

function GameRunner:drawMapContents()
    pushMatrix()
    self:scaleForLocalMap()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:drawContents(false)
        end
    end
    popMatrix()
end

function GameRunner:drawMessages()
    pushMatrix()
    self:scaleForLocalMap()
    self.cofloater:draw(self:playerGraphicCenter())
    popMatrix()
end

function GameRunner:drawMonstersOnSomeMaps()
    pushMatrix()
    self:scaleForLocalMap()
    self.monsters:draw()
    popMatrix()
    -- don't show them on the small map for now
end

function GameRunner:drawPlayerOnBothMaps()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(true)
    popMatrix()
end

function GameRunner:drawTinyMapOnTopOfLargeMap()
    OperatingMode:drawTinyMap(self)
end

There’s more. Our mission will be to get rid of some of this complexity using the DC as source for all the things that reside in the dungeon. We’ll still draw Inventory, Buttons, and so on, separately. At least for now, and probably always.

These methods could be done using the DC:

    self:drawMapContents()
    self:drawPlayerOnBothMaps()
    self:drawMonstersOnSomeMaps()

What about this method?

function GameRunner:drawMapContents()
    pushMatrix()
    self:scaleForLocalMap()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:drawContents(false)
        end
    end
    popMatrix()
end

Note that it iterates over all the tiles. 5000+ of them. Then it calls:

function Tile:drawContents(tiny)
    if not self.currentlyVisible then return end
    local center = self:graphicCenter()
    for k,c in pairs(self:getContents()) do
        c:draw(tiny, center)
    end
end

Now there’s a special bit of handling of the monsters and players in there:

function Monster:draw()
    -- ignored
end

function Monster:drawExplicit()
    local r,g,b,a = tint()
    if r==0 and g==0 and b==0 then return end
    self:drawMonster()
    self.behavior.drawSheet(self)
end

function Player:draw()
    -- ignored
end

function Player:drawExplicit(tiny)
    OperatingMode:drawInLargeAndSmallScale(tiny,self)
end

function Player:drawInLargeAndSmallScale(tiny)
    local sx,sy
    local dx = 0
    local dy = 30
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local center = self:graphicCenter() + vec2(dx,dy)
    translate(center.x,center.y)
    if self:isDead() then tint(0)    end
    sx,sy = self:setForMap(tiny)
    self:drawSprite(sx,sy)
    popStyle()
    popMatrix()
    if not tiny then
        self.attributeSheet:draw()
    end
end

Those objects don’t respond to draw, but to drawExplicit, which is called in the latter two methods listed above. That helps them be drawn on top.

Messy, and weird.

Let’s begin by just drawing all the dungeon contents by implementing draw on DC. If we leave the Monster and Player as they are, those two later calls will still draw them for now.

Let’s change this:

function GameRunner:drawMapContents()
    pushMatrix()
    self:scaleForLocalMap()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:drawContents(false)
        end
    end
    popMatrix()
end

To this:

function GameRunner:drawMapContents()
    pushMatrix()
    self:scaleForLocalMap()
    DungeonContents:draw()
    popMatrix()
end

And now:

function DungeonContentsCollection:draw()
    for object,tile in pairs(self.contentMap) do
        if tile.currentlyVisible then
            local gc = tile:graphicCenter()
            object:draw(false,gc)
        end
    end
end

The “false” flag is the “tiny” flag, allowing objects to decide whether they are going to display on the tiny map. I am not loving that, but it is as it is for now. Now we also ripped the .currentlyVisible flag out of the tile. Is there a cover method for that?

There is not. Let’s have one, and use it:

function Tile:isVisible()
    return self.currentlyVisible
end

function DungeonContentsCollection:draw()
    for object,tile in pairs(self.contentMap) do
        if tile:isVisible() then
            local gc = tile:graphicCenter()
            object:draw(false,gc)
        end
    end
end

This all works just fine, although we can be certain that the stacking isn’t right. Commit: DungeonContents draws dungeon contents, other than Player and Monsters.

Sweet. Since we used the same calling sequence for those.

Are We There Yet?

Well, we could be. The game is working essentially just as it was, since we’re not making proper use of z-level. While we’re thinking of it, let’s see what use we do make. There’s just one that matters, in drawing attribute sheets:

    zLevel(10-m:manhattanDistanceFromPlayer())

This draws the Player’s sheet at level 10, and ensures that the nearest monster’s sheet is on top. We’re not concerned about this, we’re concerned about in-dungeon objects, the things in DungeonContents.

So we’re free to make use of z level. What do we presently know?

  • We’re drawing contents on top of tiles, so z level need not accommodate z-level room for tiles.
  • Player should be on top. She likes it that way. She gets z-level 10.
  • Monsters next: 9.
  • Chests go under whatever they contain: make them 1.
  • Everything else can probably have any level other than those. We’ll give them level 5, to leave room for change.

Before we start using z level, we need to have it. I have a concern, which is that, of course, we have to implement our new z level function on every dungeon-residing object. And we have no really good way to do that.

I am tempted to build a new superclass from which everything in the dungeon inherits, and to put a default zlevel function on it. But … is that bad? We could make it abstract, and implement zLevel() as an error, which would force us quickly to push zLevel down to all the subclasses. But why not default it, allowing an override as needed?

I’m gonna do it. I welcome discussion and debate, on twitter, via email or on the ObjectMentoring slack.

I’ll put it in the Entity tab, and I’m going to make Entity inherit from it as well.

-- Entity
-- RJ 20210201 - superclasses for Monster and Player
-- and all in-dungeon objects.

DungeonObject = class()

function DungeonObject:zLevel()
    return 5
end

Entity = class(DungeonObject)

Now we would like for these guys to draw themselves at the right level. We can build that into their draw methods. (Do you sense any duplication here? I do.)

Having taken a short walk down the hall, I’m feeling some cart-horse inversion. We don’t really need the z-level yet, given where we are. And maybe we’ll put it into each of the individual drawing methods. No, wait. If we’re doing to do our objects in z order when there’s more than one per tile, we will need that order.

In our DC:draw method, we don’t really want to loop over the DC objects. We want to loop over tiles, drawing each tile’s contents in increasing z order. Let’s give that idea a name and implement it in a horrible way:

function DungeonContentsCollection:draw()
    for object,tile in pairs(self.contentMap) do
        if tile:isVisible() then
            local gc = tile:graphicCenter()
            object:draw(false,gc)
        end
    end
end

This becomes:

function DungeonContentsCollection:draw()
    for tile,info in pairs(self:drawingOrder()) do
        if tile:isVisible() then
            local gc = tile:graphicCenter()
            for level,object in ipairs(info) do
                object:draw(false, gc, level)
            end
        end
    end
end

I’m positing a third argument on the draw, containing the level to use. And of course the info thing contains the object and its level. Now we just have to create such a thing. That shouldn’t be difficult:

No, that’s not right. Trying to make it happen taught me that. I think we have to TDD this collection. I wish I had committed after putting in the new object. Let me back out this change to draw.

OK, got it. Commit: added DungeonObject superclass.

Now to TDD the new collection. What do we want it to be?

It wants to be a mapping from tile to a collection of the objects to be drawn at that tile location. And the collection wants to have elements that know the z level and the object, and it should be sorted in increasing z level order.

We could just use a table like {zLevel=3, object=whoever}. Or we could have a tiny object. I favor the tiny object but let’s see what we can do here.

Here I’m using TDD to figure out what I want, as well as how to get it. I’ve got this test sketch so far:

        _:test("DrawingOrder", function()
            local dc = DungeonContentsCollection()
            -- init dc
            local ord = dc:drawingOrder()
            local entry = ord[tile]
            _:expect(#entry).is(3)
            local level = -1
            for i,e in ipairs(entry) do
                _:expect(e.level<level).is(true)
                level = e.level
            end
        end)

We expect our new order thing to be a mapping from tile to an entry, and we expect the entry elements each to have a level, and we expect them to be in increasing order by level.

Now I think I can complete the setup. My next cut is this:

        _:test("DrawingOrder", function()
            local dc, ord
            dc = DungeonContentsCollection()
            dc:moveObjectToTile(DO(3),39)
            dc:moveObjectToTile(DO(3),99)
            dc:moveObjectToTile(DO(2),39)
            dc:moveObjectToTile(DO(3),98)
            dc:moveObjectToTile(DO(1),39)
            ord = dc:drawingOrder()
            local entry = ord[39]
            _:expect(#entry).is(3)
            local level = -1
            for i,e in ipairs(entry) do
                _:expect(e.level<level).is(true)
                level = e.level
            end
        end)
        
    end)
end

local DO = class()

function DO:init(level)
    self.level = level
end

I’ve built a little helper class to stand in for dungeon objects, and I’m just using integers as tiles. The collection doesn’t care.

Now I expect a fail on drawingOrder.

4: DrawingOrder -- DungeonContentsCollection:74: attempt to call a nil value (method 'drawingOrder')

I write this much of the new function and realize I don’t quite like the test:

function DungeonContentsCollection:drawingOrder()
    local order = {} -- maps tile to array sorted by object level.
    
end

Let’s not have an object with level and then the objects. Let’s just have the objects sorted by level. Change the test a bit:

        _:test("DrawingOrder", function()
            local dc, ord
            dc = DungeonContentsCollection()
            dc:moveObjectToTile(DO(3),39)
            dc:moveObjectToTile(DO(3),99)
            dc:moveObjectToTile(DO(2),39)
            dc:moveObjectToTile(DO(3),98)
            dc:moveObjectToTile(DO(1),39)
            ord = dc:drawingOrder()
            local entry = ord[39]
            _:expect(#entry).is(3)
            local level = -1
            for i,dungeonObject in ipairs(entry) do
                _:expect(dungeonObject:zLevel()<level).is(true)
                level = dungeonObject:zLevel()
            end
        end)

I can enhance the test a bit more. Let’s check to see that the three items are the three we expect:

        _:test("DrawingOrder", function()
            local dc, ord
            dc = DungeonContentsCollection()
            local do3 = DO(3)
            local do2 = DO(2)
            local do1 = DO(1)
            dc:moveObjectToTile(do3,39)
            dc:moveObjectToTile(DO(3),99)
            dc:moveObjectToTile(do2,39)
            dc:moveObjectToTile(DO(3),98)
            dc:moveObjectToTile(do1,39)
            ord = dc:drawingOrder()
            local entry = ord[39]
            _:expect(#entry).is(3)
            _:expect(entry).has(do1)
            _:expect(entry).has(do2)
            _:expect(entry).has(do3)
            local level = -1
            for i,dungeonObject in ipairs(entry) do
                _:expect(dungeonObject:zLevel()<level).is(true)
                level = dungeonObject:zLevel()
            end
        end)

Now we can get the contents right but not the sort. That lets us take two steps rather than one.

function DungeonContentsCollection:drawingOrder()
    local order = {} -- maps tile to array sorted by object level.
    for object,tile in pairs(self.contentMap) do
        local ord = order[tile] or {}
        table.insert(ord,object)
        order[tile] = ord
    end
end

I think this gets the contents right. Let’s not explain it, let’s test it and explain why it works (or fails, as it may well do).

4: DrawingOrder -- DungeonContentsCollection:78: attempt to index a nil value (local 'ord')

That message tells me we didn’t return our collection. I only make that mistake about two times out of three.

4: DrawingOrder  -- Actual: false, Expected: true
4: DrawingOrder  -- Actual: false, Expected: true

That’ll be the level. Let me add a comment to that expectation.

            for i,dungeonObject in ipairs(entry) do
                _:expect(dungeonObject:zLevel()<level, "level not increasing").is(true)
                level = dungeonObject:zLevel()
            end
4: DrawingOrder level not increasing -- Actual: false, Expected: true
4: DrawingOrder level not increasing -- Actual: false, Expected: true

Perfect. Now the order. Let’s be nasty:

function DungeonContentsCollection:drawingOrder()
    local order = {} -- maps tile to array sorted by object level.
    for object,tile in pairs(self.contentMap) do
        local ord = order[tile] or {}
        table.insert(ord,object)
        if #ord > 1 then
            table.sort(ord, function(a,b) return a:zLevel() < b:zLevel() end)
        end
        order[tile] = ord
    end
    return order
end

We’ll just sort the table if it has more than one element. Nice of me to put in that optimization, wasn’t it?

I expect this to run. Curiously, it does not, with three messages:

4: DrawingOrder level not increasing -- Actual: false, Expected: true
4: DrawingOrder level not increasing -- Actual: false, Expected: true
4: DrawingOrder level not increasing -- Actual: false, Expected: true

We could create a more informative message, or just print to see what’s up. Let’s print.

object 	1	1
object 	2	2
object 	3	3
4: DrawingOrder level not increasing -- Actual: false, Expected: true
4: DrawingOrder level not increasing -- Actual: false, Expected: true
4: DrawingOrder level not increasing -- Actual: false, Expected: true

OK, that’s curious. The levels look OK to me. What’s wrong here. Review the test. Perhaps a local missing or something?

LOL. How about this:

                _:expect(dungeonObject:zLevel()<level, "level not increasing").is(true)

I believe the gentleman wanted > there.

And the test runs. We now have the objects in the dungeon able to be sorted by zLevel. But they have to have one. We’ll get right to that, but first, while we’re here, commit this: DC:drawingOrder() function works.

Now what? We can implement zLevel on everyone preemptively, or we can change our DC:draw method to call drawingOrder() and have it break. Let’s do that.

I have the prior version of that method right up above, but it’s not quite consistent with our current implementation.

I’m nominating this:

function DungeonContentsCollection:draw()
    for tile,entries in pairs(self:drawingOrder()) do
        if tile:isVisible() then
            local gc = tile:graphicCenter()
            for i,object in ipairs(entries) do
                object:draw(false, gc, object:zLevel())
            end
        end
    end
end

This should explode looking for zLevel on someone.

DungeonContentsCollection:116: attempt to call a nil value (method 'zLevel')
stack traceback:
	DungeonContentsCollection:116: in function <DungeonContentsCollection:116>
	[C]: in function 'table.sort'
	DungeonContentsCollection:116: in method 'drawingOrder'
	DungeonContentsCollection:100: in method 'draw'
	GameRunner:324: in method 'drawMapContents'
	GameRunner:279: in method 'draw'
	Main:86: in function 'draw'

I love it when a plan comes together. Now to provide zLevel. We’re going to make all those dungeon objects inherit from DungeonObject.

WayDown = class(DungeonObject)
Loot = class(DungeonObject)
Key = class(DungeonObject)
Chest = class(DungeonObject)
Spikes = class(DungeonObject)
Decor = class(DungeonObject)
Lever = class(DungeonObject)
Announcer = class(DungeonObject)
AnnouncerProxy = class(DungeonObject)

Hm lots. Too many, perhaps. But I think that’s all of them. They will now pick up the default zLevel. Game should run.

Ah that glorious word “should”. Here’s what happened:

DungeonContentsCollection:116: attempt to call a nil value (method 'zLevel')
stack traceback:
	DungeonContentsCollection:116: in function <DungeonContentsCollection:116>
	[C]: in function 'table.sort'
	DungeonContentsCollection:116: in method 'drawingOrder'
	DungeonContentsCollection:100: in method 'draw'
	GameRunner:324: in method 'drawMapContents'
	GameRunner:279: in method 'draw'
	Main:86: in function 'draw'

I want to know what that object is. Let’s invade the drawingOrder function.

function DungeonContentsCollection:drawingOrder()
    local order = {} -- maps tile to array sorted by object level.
    for object,tile in pairs(self.contentMap) do
        assert(object.zLevel, tostring(object).." doesn't have zLevel")
        local ord = order[tile] or {}
...

Still not enough info, it just prints a table id. Whatever it is, it doesn’t understand toString.

I am reminded of Pathfinder. What is it? OK, generic monster with cool behavior. He’s not the issue even if he were there, and he isn’t.

OK, I’ll dump the thing.

        if not object.zLevel then
            print(object, " has no zLevel")
            for k,v in pairs(object) do
                print(k)
            end
        end

The object in hand prints nothing. It has no member variables and no methods. That’s weird. I’ll trap it on insertion.

function DungeonContentsCollection:moveObjectToTile(object,tile)
    if not object.zLevel then
        error("no zLevel, who the heck are you?")
    end
    self.contentMap[object] = tile
end

Almost everything that prints out is inside a test. Those are OK. Then we get this:

DungeonContentsCollection:145: no zLevel, who the heck are you?
stack traceback:
	[C]: in function 'error'
	DungeonContentsCollection:145: in method 'moveObjectToTile'
	Tile:107: in method 'addContents'
	Tile:397: in method 'moveObject'
	WayDown:7: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in global 'WayDown'
	GameRunner:477: in method 'placeWayDown'
	GameRunner:155: in method 'createLevel'
	Main:42: in function 'setup'

Maybe we can’t check this way, since zLevel is in the superclass. Change the check:

function DungeonContentsCollection:moveObjectToTile(object,tile)
    object:zLevel()
    self.contentMap[object] = tile
end

I get this:

DungeonContentsCollection:144: attempt to call a nil value (method 'zLevel')
stack traceback:
	DungeonContentsCollection:144: in method 'moveObjectToTile'
	Tile:107: in method 'addContents'
	Tile:397: in method 'moveObject'
	WayDown:7: in field 'init'
	... false
    end

    setmetatable(c, mt)
    return c
end:24: in global 'WayDown'
	GameRunner:477: in method 'placeWayDown'
	GameRunner:155: in method 'createLevel'
	Main:42: in function 'setup'

New message, same place, WayDown. But why?

I think I know. I think it’s a compiler thing. DungeonObject needs to be defined to the left of the classes that use it. Let’s try that.

Tests still fail, game runs at a quick check. First back out the error a bit. No, let’s make the tests meet the criterion.

function DungeonContentsCollection:moveObjectToTile(object,tile)
    assert(object:zLevel(),"dungeon object must implement zLevel")
    self.contentMap[object] = tile
end

Fails include:

2: Dungeon helps monster moves -- DungeonContentsCollection:138: attempt to call a nil value (method 'zLevel')

Ah. This is nasty. I’m going to remove the check, because otherwise I have to move around lots of tabs and that leads to problems. Comment out the assert.

OK, the game works as intended. Commit: dungeon contents drawn in zLevel order, inefficiently.

Now let’s set the zLevels for our items. Where’s that list? Oh yes:

  • We’re drawing contents on top of tiles, so z level need not accommodate z-level room for tiles.
  • Player should be on top. She likes it that way. She gets z-level 10.
  • Monsters next: 9.
  • Chests go under whatever they contain: make them 1.
  • Everything else can probably have any level other than those. We’ll give them level 5, to leave room for change.

OK, then:

function Chest:zLevel()
    return 1
end

This should make the Health appear on top of the Chest. And it does.

health

But so far we have only done the order. That suffices but we intend to use the level. So let’s go further:

function Chest:draw(tiny, center, level)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    zLevel(level)
    sprite(Sprites:sprite(self.pic),center.x,center.y, 96,127)
    popStyle()
    popMatrix()
end

We’ll need to do the other guys as well. Think about duplication as you observe these.

function Loot:draw(tiny, center, level)
    pushStyle()
    spriteMode(CENTER)
    zLevel(level)
    sprite(Sprites:sprite(self.icon),center.x,center.y+10)
    popStyle()
end

Heck, that’s intolerable on the face of it. We have the level up above. Revert those. Change this:

function DungeonContentsCollection:draw()
    for tile,entries in pairs(self:drawingOrder()) do
        if tile:isVisible() then
            local gc = tile:graphicCenter()
            for i,object in ipairs(entries) do
                object:draw(false, gc, object:zLevel())
            end
        end
    end
end

To this:

function DungeonContentsCollection:draw()
    pushMatrix()
    pushStyle()
    for tile,entries in pairs(self:drawingOrder()) do
        if tile:isVisible() then
            local gc = tile:graphicCenter()
            for i,object in ipairs(entries) do
                zLevel(object:zLevel())
                spriteMode(CENTER)
                object:draw(false, gc)
            end
        end
    end
    popStyle()
    popMatrix()
end

We’ll see what else we can push in there to reduce the size of the individual draw functions. This should have no visible effect.

Weirdness. It does have effect. It makes some objects disappear and reappear as the player moves around.

I don’t have the time nor energy to chase this. Fact is, if you draw in increasing zLevel order, you don’t have to actually set the zLevel variable, everything just works. Let’s just not do that. But we will keep the spriteMode, and look for other things to move up.

That works. Commit: DC:draw sets spriteMode(CENTER).

Now I can trim down the draw methods:

function WayDown:draw(tiny, center)
    sprite(Sprites:sprite(asset.steps_down),center.x,center.y,64,64)
end

function Loot:draw(tiny, center)
    sprite(Sprites:sprite(self.icon),center.x,center.y+10)
end

function Key:draw(tiny, center)
    if tiny then return end
    sprite(asset.builtin.Planet_Cute.Key,center.x,center.y, 50,50)
end

function Chest:draw(tiny, center)
    if tiny then return end
    sprite(Sprites:sprite(self.pic),center.x,center.y, 96,127)
end

function Spikes:draw(tiny, center)
    sprite(self:sprite(),center.x,center.y,64,64)
end

function Decor:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    translate(center.x, center.y)
    scale(self.scaleX, 1)
    sprite(self.sprite,0,0, 50,50)
    popMatrix()
end

function Lever:draw(tiny, center)
    pushMatrix()
    translate(center.x,center.y)
    LeverSprites[self.position]:draw()
    popMatrix()
end

Most of those had at least three, usually at least five lines removed from their previous implementations, since the matrix and style are saved and the sprietMode set.

All is well. Loot from chests is drawn after the chest, so comes out on top. I might explore why setting the zLevel didn’t work. Certainly should have, as I understand it.

Commit: move drawing function up to DC from drawn dungeon objects.

I think we’re good. Let’s sum up.

Sum(x): x == “up”

By moving the drawing of dungeon objects to the DungeonContents collection, We simplified the drawing code, which used to draw various subgroups of the objects.

We can now remove:

function GameRunner:drawKeys()
    for i,k in ipairs(self.keys) do
        k:draw()
    end
end

function Tile:drawContents(tiny)
    if not self.currentlyVisible then return end
    local center = self:graphicCenter()
    for k,c in pairs(self:getContents()) do
        c:draw(tiny, center)
    end
end

We’ll soon draw the monsters and players from this same function, simplifying the system further.

TDD FTW!

I should mention here the excellent result that I got from deciding to use TDD on the drawingOrder() method. I had a structure in mind that might have worked, but as I wrote the test, I could already see that that structure didn’t give me what I needed. So I simplified my imaginary “spec” and came up with a better structure than the one I had in mind.

We have removed 3, or 5, or more lines from several drawing methods. The drawing methods for dungeon objects are beginning to look very similar. We may be able to reduce duplication there as well. Some of them are already identical.

And, aside from the odd zLevel thing, it has all gone quite smoothly, all things considered. Certainly no screaming or tearing of hair, no kicking of feet demanding not to have to go to bed.

Regarding the zLevel, I have removed all other zLevel calls from the program and set zLevel in the DC:draw method to a constant 5, and the glitch appears. Without the call, it does not. I think zLevel is borked. I’ve put a note on the Codea forum.

Efficiency?

What about efficiency? Our drawingOrder() table is calculated on every move, scanning all the dungeon objects and arranging them by zLevel, by tile. That’s surely wasteful, in that, generally speaking, none of the tile tables will change, except that one will go away and a new one will appear. So this is not as efficient as it might be. That said, the game responds just fine.

We’ll deal with that anon, perhaps tomorrow.

Overall result

What we are seeing is the nice consolidation that can happen when we refactor from our current design to a better one.

And note: it has all proceeded in tiny steps. Here’s a view of the commit schedule as seen by WorkingCopy:

commits

Most of the commits are less than 30 minutes apart, and that includes article-writing time. At each of those commit points, the system was ready to ship, with no surprise defects. (There were some points where I accepted an oddity, but there were few if any actual breakages.)

This whole effort could have been done spread over any time period required, if for some reason we couldn’t afford the eight or ten hours it took to do it so far.

Incremental design; discovery of concerns; better idea; installation of idea; reap benefits of idea. Plus the design benefits of TDD.

All in a few days, in steps all less than an hour, usually much less.

This is the way.

See you next time!


D2.zip