Dungeon 72
Let’s try the wall data. This calls for a slightly different scheme, I think.
I whiled away the time on the Zoom Ensemble last night, somewhat distracted, by filling in the table that defines, for each combination of surrounding tiles, what kind of wall tile to use. I probably missed some opportunities to learn and contribute, for which I apologize, but I did complete the tedious task of defining a table of 256 elements.
Briefly, what I did was enhance that little program I showed yesterday, to add one to the current byte being displayed when I touched above the tic-tac-toe thing, and subtract one when I touched below. Whenever that happened, a save function was called that packed the whole array into a string, which I saved in a tab in the project, and also in a project storage location that Codea provides. Then, once I was creating that string, I added code to read, compile, and execute it, an to store the result back in the array. That inits the array to the last saved state, so that I can stop and come back if need be.
I’d not used the ability to compile and execute “live” before, so I pulled the temple down on top of myself a few times before I got it sorted. Good fun. Anyway, now I have some data that will surely be close, and what I need now is to use it.
My Cunning Plan
The current program draws walls around rooms as soon as they are allocated, not outside the room, but using its outermost floor tiles. Then after the hallways are drawn, there’s a separate loop that goes over all the tiles, deciding whether to convert “edge” tiles into “wall” tiles. Since all wall tiles are presently assumed to be the same, that works to put walls around all the hallways.
Here’s that code:
function GameRunner:convertEdgesToWalls()
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
tile:convertEdgeToWall()
end
end
end
function Tile:convertEdgeToWall()
if self.kind == TileEdge and self:hasRoomNeighbor() then
self.kind = TileWall
end
end
My new plan is to skip the room outlining altogether, and to use a more powerful version of this function to do all the walls. I believe that left to its own devices, this code would draw our simple walls correctly even now.
Let’s test that. First, I’ll turn off all wall creation. Here’s where the room walls get done now:
function Room:paint()
-- leave wall on all four sides
for x = self.x1,self.x2 do
for y = self.y1,self.y2 do
self.runner:setTile(self:correctTile(x,y))
end
end
end
function Room:correctTile(x,y)
if x == self.x1 or x == self.x2 or y == self.y1 or y == self.y2 then
return Tile:wall(x,y, self.runner)
else
return Tile:room(x,y, self.runner)
end
end
Note that the room tiles have not been given their particular picture at this point, but they probably should be. In any case, we want correctTile
to return a room type tile always:
function Room:correctTile(x,y)
if false and (x == self.x1 or x == self.x2 or y == self.y1 or y == self.y2) then
return Tile:wall(x,y, self.runner)
else
return Tile:room(x,y, self.runner)
end
end
Now I want to see what happens with no walls, so I’ll also turn off the other wall creation.
function GameRunner:createLevel(count)
self:createRandomRooms(count)
self:connectRooms()
--self:convertEdgesToWalls()
local r1 = self.rooms[1]
local rcx,rcy = r1:center()
...
This should give me rooms with no walls, just dark edges everywhere. Let’s see what it actually does.
The gray tiles are “edge” tiles. We don’t usually see those, because the visibility logic stops drawing at a wall. Since there are no walls, we see them.
Now let’s put the convertEdgesToWalls
back. I’m hoping this will put walls everywhere they are needed.
And, miraculously, or by dint of superior programming, we do get walls just as before. The rooms are now larger by two tiles in each direction, but since their size is random, we’d only notice that on the average.
Commit: walls drawn only by convertEdgeToWall
.
Now for the somewhat larger task of installing the new wall table.
I should confess that I have some tests turned off, as they get in the way of visual testing. I’ll try to remember to turn them back on.
The table that I plan to use to set the wall tiles looks like this:
temp = {
asset.documents.Dropbox.Pit_Column_59,
asset.documents.Dropbox.Wall_56,
asset.documents.Dropbox.Wall_56,
asset.documents.Dropbox.Pit_Column_58,
asset.documents.Dropbox.Pit_Column_64,
asset.documents.Dropbox.Wall_56,
asset.documents.Dropbox.Wall_56,
asset.documents.Dropbox.Wall_50,
asset.documents.Dropbox.Wall_50,
asset.documents.Dropbox.Wall_53,
...
}
It’s 256 items long, starting from 1. The index of an item is a bit-wise representation of the surroundings of the tile in question, zero if the surrounding tile is not room, one if it is room. (As I write this, I’m wondering “what if it’s wall”, and worried that some configurations may not work. If they don’t, then my scheme may also not work.
But for now, we’re going to put this in and see how it goes.
The bits go like this:
32 | 64 | 128 |
8 | - | 16 |
1 | 2 | 4 |
The cell in the middle isn’t counted: it’s the one we are deciding about.
So I need to create that binary number in the convertEdgeToWall
, and assign the suitable tile.
I can’t quite see my way clear to TDDing at this level. My sketch by intention is this:
function Tile:convertEdgeToWall()
if self.kind ~= TileEdge then return end
local byte = self:getSurroundingInfo()
if byte == 0 then return end -- all neighbors are non-room
self.kind = TileWall
self.tile = self:getWallTile(byte)
end
The getSurroundingInfo
is intended to compute the appropriate byte based on the table above and surrounding tiles. Then getWallTile
will fetch the tile from the table.
We can stub these to see if it’s close to reasonable.
function Tile:getSurroundingInfo()
if self:hasRoomNeighbor() then
return 1
else
return 0
end
end
And then …
function Tile:getWallTile(byte)
return asset.Wall_1
end
I don’t even know what Wall_1
is, but it should be interesting.
Well, it explodes, because we’re supposed to find a sprite thing in there.
function Tile:getWallTile(byte)
--sprite(xxx)
return AdjustedSprite(asset.Wall_1, p256)
end
That seems to have worked. The rooms are much larger. I’m not sure if I like that, but it’s easily adjusted.
Now for the hard part, computing that byte.
Let’s glance at hasRoomNeighbor
. It’ll help me describe my plan.
function Tile:hasRoomNeighbor()
local ck = { vec2(1,0),vec2(1,1),vec2(0,1),vec2(-1,1), vec2(-1,0),vec2(-1,-1),vec2(0,-1),vec2(1,-1)}
for i,p in ipairs(ck) do
local pos = self:pos() + p
local tile = self.runner:getTile(pos)
if tile:isRoom() then return true end
end
return false
end
“Actually …” this is so close to useful that I think I’ll use it. My basic idea is to iterate over a list going from top right, right to left, and down, to compute whether a tile was room or not, setting the bit and shifting. To make that work, we need the list i the right order.
I’ll copy and paste this into my new function. Modified, I have this so far:
function Tile:getSurroundingInfo()
local byte = 0
local ck = { vec2(1,0),vec2(1,1),vec2(0,1),vec2(-1,1), vec2(-1,0),vec2(-1,-1),vec2(0,-1),vec2(1,-1)}
for i,p in ipairs(ck) do
local pos = self:pos() + p
local tile = self.runner:getTile(pos)
if tile:isRoom() then
byte = byte|1
end
byte = byte<<1
end
return byte
end
However, the ck
array needs to be in the right order:
function Tile:getSurroundingInfo()
local byte = 0
local ck = { vec2(1,1),vec2(0,1),vec2(-1,1), vec2(1,0),vec2(-1,0), vec2(1,-1),vec2(0,-1),vec2(-1,-1) }
for i,p in ipairs(ck) do
local pos = self:pos() + p
local tile = self.runner:getTile(pos)
if tile:isRoom() then
byte = byte|1
end
byte = byte<<1
end
return byte
end
I do think this could have done with some testing, but the setup is daunting. We’re going to have to just gut it out, I think.
As written, this should still work, just returning a more complex non-zero byte. And it seems to. Now to plug in the horrid table and use it.
I’ll have to fetch it from the tab of the creating program. Could import it, I suppose, but it’s not really set up for that.
It’s not in quite the right format, as it refers to documents.Dropbox
and we have the tiles inside us. But, you know, I think I’ll leave it looking at the dropbox files for now, just in case the local ones don’t match.
It came to me, as I took a brief stroll around the house, that I’m shifting the byte after setting it. That will make it twice what it should be. We should shift, then do the or.
function Tile:getSurroundingInfo()
local byte = 0
local ck = { vec2(1,1),vec2(0,1),vec2(-1,1), vec2(1,0),vec2(-1,0), vec2(1,-1),vec2(0,-1),vec2(-1,-1) }
for i,p in ipairs(ck) do
byte = byte<<1
local pos = self:pos() + p
local tile = self.runner:getTile(pos)
if tile:isRoom() then
byte = byte|1
end
end
return byte
end
And now the big test:
function Tile:getWallTile(byte)
local tile = WallTiles[byte]
return AdjustedSprite(tile, p256)
end
This can’t possibly work. But mysteriously, it seems to be working just fine. Good enough to commit: Computing correct walls.
I do think the rooms are too large now. Let’s adjust them downward.
Here’s where that gets decided:
r = Room:random(self.tileCountX,self.tileCountY, 4,15, self)
The values 4,15 are the inputs to this function:
function Room:random(spaceX, spaceY, minSize, maxSize, runner)
local w = math.random(minSize,maxSize)
local h = math.random(minSize,maxSize)
local x = math.random(1,spaceX-w)
local y = math.random(1,spaceY-h)
return Room(x,y,w,h,runner, false)
end
So if we want them two smaller in each dimension …
r = Room:random(self.tileCountX,self.tileCountY, 4,13, self)
Let me try that … yes, I think that’s better. Now those torches are weird, let’s remove those:
function Tile:getSprites(pos, tiny)
if not self.tile then
if self:isRoom() then
self.tile = self:getRandomFloorSprite()
else
self.tile = TileSprites[self.kind]
end
end
local result = {self.tile}
if not tiny and self:isWall() and pos.x%3 == pos.y%3 then
table.insert(result,TileWallMarker)
end
return result
end
We’ll just remove that if. I was planning to comment it out in case I want some new kind of torch logic, but let’s suppose we’re smart enough to do what we really need. Commit: remove torches.
I think I’ll conclude this here. The results look good and so far I’ve not seen it do anything bizarre. Let’s sum up and look at some more pictures.
Summary
If you think about what we had to do, this was a pretty big deal. We had an elementary scheme for showing walls, which only had one form of display. We painted walls around the outsides of rooms as we drew them, and had s separate pass over the whole world to find edge tiles adjacent to room tiles, which just marked them as wall.
We replaced that with a scheme that doesn’t paint room walls separately, and that scans the tile space, computing the surroundings of each tile in detail, and based on the exact configuration of room tiles surrounding the edge tile, returns the best wall tile to use in that position.
You’d think that was a big deal. But what we did was
- Write a graphical tool to produce a table of tile configuration to wall picture. That took a couple of hours.
- Used the tool to build a table of 256 possible tile configurations, and assigned the best tile to that. That took two hours of chat time. (There may be a few poor choices in there, but I’ve not seen any yet.)
- Dumped the table to text, imported it into the Dung program. Part of the hours above
- Computed the corresponding table index from the real tile configuration. About an hour this morning.
- Looked up and assigned the suitable wall tile.
And it worked. Maybe five hours of work, in my usual style of writing articles at the same time. And it went it quite easily.
Why did it go in quite easily? Small methods, doing just one thing. That meant that there were specific tiny places where we could plug in the new logic.
This didn’t happen because I’m smart–although it is possible that I am. It happened because I’m careful to keep my code as well-factored as my patience can manage, and in this production code, I’ve managed to be pretty patient.
Would this work for you? I’d bet yes. It takes practice, care, patience … and practice. And more practice.
But it’s fun practice. That’s why I do this.
Herewith, some pics of the dungeon’s sweet walls, and zips of the program if you want to browse.
Note, however, the zips will not run: you don’t have all the assets yet. I’m not sure if my license lets me give them to you, but I probably will.
Here’s one pic of note:
See the little white tile out in the mud? I’m not sure if that’s a mistake in my data or not. It depends on some of the unseen tiles. There are some missing configurations of wall tiles, so I had to make some compromises. On the other hand, maybe that’s a tombstone. Who knows?
Based on exploring around that area, I think it’s a flaw in my table. I’ll run the configurator and check. If so, it’s an easy fix.
A few more shots. See you next time!