Dungeon 17
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:
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.
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:
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:
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:
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:
Notice the black and top and right. Since we’re offsetting downward, we get that effect. Now to bump that up:
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:
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:
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:
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:
- Never write code that knows what the current scaling, translation, or rotation are;
- All objects draw themselves as if they were at location 0,0, typically centered there.
- 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.
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:
That looks good. And after a few test runs, here’s a view that rather clearly shows that the tiles all line up:
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:
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:
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.
-
Lee Dorsey, ca. 1965. Music and lyrics by Allen Toussaint. ↩