Dungeon 179
I have yet another idea about how to handle switching between developer and player modes. This might be what Bruce Onder was talking about.
I want to begin by making a table of all the things we currently do differently in Dev and Player modes.
Object | Behavior | Player | Dev |
---|---|---|---|
GameRunner | draw tiny map | draw | no |
GameRunner | scale large map | 1 | ~= 0.25 |
Player | draw explicit | yes | no if tiny |
AttributeSheet | draw | yes | no |
I’ll prepare the ground a bit by breaking out those two scaling methods, as I did before but reverted:
function GameRunner:scaleForLocalMap()
if DevMap then
self:scaleForDeveloper()
else
self:scaleForPlayer()
end
end
function GameRunner:scaleForDeveloper()
self:getDungeon():setVisibility()
translate(0,0)
local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
local s = math.min(vs,hs)
scale(s)
end
function GameRunner:scaleForPlayer()
local center = self.player:graphicCorner()
translate(WIDTH/2-center.x, HEIGHT/2-center.y)
scale(1)
end
My coding standards call for that extraction anyway, but I think we’ll find it useful if my cunning plan works at all.
Commit: refactor GameRunner:scaleForLocalMap
What is your cunning plan, anyway?
I propose to build two strategy objects, one representing player mode and one representing developer mode. In every instance where we want a difference between dev and player mode, the relevant object will send a message to the currently-live strategy object, which will dispatch back to do the right thing.
This is almost certainly what Bruce Onder was talking about in his tweet:
What do you think of this idea: pull out the drawing stuff into its own rendering object, initialized with a “null object” which draws the game in default mode (mini map, zoomed in level).
Then when the ShowMap is disabled, call the renderer, replacing with the NoMap object?
If it’s not exactly what he meant, it certainly rhymes with it. So all credit to him in what follows. Except for the part about actually doing it.
I think this could be done with an array of functions, and that might save us a couple of classes. But I like classes. Let’s stick with the OO style. Maybe someday we’ll do another project that’s all functions. (But probably not.)
New tab, OperatingModes. I type in two little classes:
PlayerMode = class()
function PlayerMode:scaleForLocalMap(sender)
sender:scaleForPlayer()
end
DevMode = class()
function DevMode:scaleForLocalMap(sender)
sender:scaleForDeveloper()
end
Now I think I want to make a well-known object, at least to start, and to set it when the mode switches.
OperatingMode = PlayerMode()
parameter.boolean("ShowMap",false)
parameter.boolean("DevMap", false, function(mode)
if mode then
OperatingMode = DevMode()
else
OperatingMode = PlayerMode()
end
end)
Now I should be able to do this:
function GameRunner:scaleForLocalMap()
OperatingMode:scaleForLocalMap(self)
end
So that works. Life is good. Next let’s do this one:
function GameRunner:drawTinyMapOnTopOfLargeMap()
if DevMap then return end
pushMatrix()
self:scaleForTinyMap()
Runner:drawMap(true)
popMatrix()
end
We have that long name to remind us that it has to be drawn late in the process. We’ll keep that but we need to extract the works here.
function GameRunner:drawTinyMapOnTopOfLargeMap()
OperatingMode:drawTinyMap(self)
end
function GameRunner:drawTinyMap()
pushMatrix()
self:scaleForTinyMap()
Runner:drawMap(true)
popMatrix()
end
And:
function PlayerMode:drawTinyMap(sender)
sender:drawTinyMap()
end
function DevMode:drawTinyMap()
end
This should be fine. And it is. Next:
function Player:drawExplicit(tiny)
if tiny and DevMap then return end
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
OK, I’m going by rote here but this one is a bit tricky. Extract the guts as always:
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
And:
function PlayerMode:drawInLargeAndSmallScale(tiny, sender)
sender:drawInLargeAndSmallScale(tiny)
end
function DevMode:drawInLargeAndSmallScale(tiny, sender)
if not tiny then
sender:drawInLargeAndSmallScale(tiny)
end
end
Again, it works just fine. I’m not fond of having to check the tiny flag, but we need to do it unless we change how the Player draws herself twice.
One more:
function AttributeSheet:draw()
if DevMap then return end
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
...
Same drill:
function AttributeSheet:draw()
OperatingMode:drawSheet(self)
end
function AttributeSheet:drawSheet()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
...
And:
function PlayerMode:drawSheet(sender)
sender:drawSheet()
end
function DevMode:drawSheet()
-- no action
end
I decided to add a comment to the empty ones, and was able to resist “this method intentionally blank”.
I expect this to work. And it does:
There is one odd effect, which is that the edge tiles, which we normally do not see, show up after we leave dev mode. We could fix that, but I don’t think it’s worth it. The small map is entirely filled in after leaving dev mode as well, because since the whole dungeon has been seen, all the rooms show on the small map. Those are the rules.
I think this is good. Commit: OperatingMode, PlayerMode, DevMode handling all switching between modes.
Summary
Well, that went smoothly, didn’t it? The need to pass the tiny
flag is a bit naff, but at this moment I don’t see a good way around it. And it’s somewhat irritating having a global name for OperatingMode, but you have to admit that it makes for a rather clean interface. We could hide it in Runner, but the path from AttributeSheet to the runner is at least two rails.
I am perilously close to allowing everyone to access the Runner as a global. I think it might actually make the code easier to understand.
Nonetheless, globals are always worth some concern, as they tend to make changes difficult. We’ll see whether this one bugs us.
So what are these DevMode PlayerMode things? They’re built as class instances, but they don’t really have methods of their own, they just forward a message back to the sender, or do nothing. In essence, this is the Strategy Pattern in operation, and it seems to have served nicely.
The very nice thing about this implementation is that it seems just right for the current situation: four cases, four tiny methods. However, it should serve nicely for any additional switching we may need to do for developer convenience.
There are two things we might do now. One is that the ShowMap toggle is just about wasted, since going into Dev Mode and out will display the small map. So let’s remove that. We delete:
parameter.boolean("ShowMap",false)
And this:
function Tile:drawMapCell(center)
if self.kind == TileRoom and (ShowMap or self.seen) then
pushMatrix()
pushStyle()
rectMode(CENTER)
translate(center.x, center.y)
fill(255)
rect(0,0,TileSize,TileSize)
popStyle()
popMatrix()
end
end
Becomes this:
function Tile:drawMapCell(center)
if self.kind == TileRoom and self.seen then
pushMatrix()
pushStyle()
rectMode(CENTER)
translate(center.x, center.y)
fill(255)
rect(0,0,TileSize,TileSize)
popStyle()
popMatrix()
end
end
Test and commit: Remove ShowMap feature.
The second is that we could remove the if statement from this:
parameter.boolean("DevMap", false, function(mode)
if mode then
OperatingMode = DevMode()
else
OperatingMode = PlayerMode()
end
end)
How? Well, we could make a table indexed by true/false, containing class names, and instantiate the appropriate one. That would take more lines of code and be less clear. There is a time and a place for a conditional, and for now, I’m happy with this one.
So. Good-looking solution. Thanks to Bruce for the idea, which may or may not be the one he intended. All mistakes, of course, are Chet’s.