Dungeon 27: Focus
I’m fairly satisfied with room and hallway generation. It’s time to work on what the screen looks like in play.
We probably don’t want to show the whole map to the player at any time, certainly not during game play. Instead, we want a zoomed-in view, centered on the player. In version 1 of this program, I had the screen centered on the player and scrolled the game around her. We could do that but we have a finite game board and, as we’ll see in some pictures that I’ll surely take, the tiles can scroll to the point where we see screen background.
I see at least two options for that. It would be easy enough to make the game board produce tiles outside the actual range of the rooms, and it wouldn’t be terribly hard to limit the display to keep the edges from showing.
I do know approximately how hard that is, just a couple of lines of code. When I got here, a few paragraphs ago, I was thinking I’d do that. Now I’m thinking I like the virtual tiles idea better.
Anyway, let’s get started.
First, I think I’ll add in a flag for display mode, so that when I touch the screen, it toggles between full map view and zoomed in view. Along the way, I think I should extend the current batch of tiles out to the edge of the screen. I’m going to assume iPad dimensions for now.
When we create the GameRunner, it immediately produces its tiles. Then we have room creation with createRandomRooms
:
function GameRunner:init(count)
self.tileCount = count or 60
self.tileSize = 16
self.tiles = {}
for x = 1,self.tileCount do
self.tiles[x] = {}
for y = 1,self.tileCount do
local tile = Tile:edge(x,y)
self:setTile(tile)
end
end
end
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(60,60, 4,15, self)
if self:hasRoomFor(r) then
placed = true
r:paint()
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
self:connectRooms()
end
Since the iPad screen is a rectangle, not square, we can’t use the count variable in both cases. No one even uses the count on init, so I’ll remove it for now.
Here’s what I’ve got:
To accomplish that, we have this:
function GameRunner:init()
self.tileSize = 16
self.tileCountX = math.floor(WIDTH/self.tileSize)
self.tileCountY = math.floor(HEIGHT/self.tileSize)
self.tiles = {}
for x = 1,self.tileCountX do
self.tiles[x] = {}
for y = 1,self.tileCountY do
local tile = Tile:edge(x,y)
self:setTile(tile)
end
end
end
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(self.tileCountX,self.tileCountY, 4,15, self)
if self:hasRoomFor(r) then
placed = true
r:paint()
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
self:connectRooms()
end
Because the screen starts at zero and arrays start at one, I changed tile to do this:
function Tile:draw()
pushMatrix()
pushStyle()
fill(self:fillColor())
rect(self:gx(),self:gy(),TileSize)
popStyle()
popMatrix()
end
function Tile:gx()
return (self.x-1)*TileSize
end
function Tile:gy()
return (self.y-1)*TileSize
end
The change was to use x-1 and y-1 to get the graphic coordinates for the tile–its lower left corner– with the lowest leftmost tile at zero-zero.
I expect that this is going to bite us when we place other objects, but we’ll see. There is an alternative to this change. We could translate all the drawing one tile down and to the left instead. I’m a bit concerned about how that would mesh with scaling, so for now, we’ll let it be.
Now for the toggle.
function setup()
if CodeaUnit then
codeaTestsVisible(true)
runCodeaUnitTests()
end
Runner = GameRunner()
Runner:createRandomRooms(12)
--Runner:createTestRooms()
DisplayToggle = false
end
function draw()
if CodeaUnit then showCodeaUnitTests() end
Runner:draw()
end
function touched(aTouch)
DisplayToggle = not DisplayToggle
end
Now I wonder whether we can do the zooming from this outer level, leaving the game to just do its thing without regard to where the screen is set. I don’t see why not. Let’s begin with a zoom:
function draw()
pushMatrix()
if CodeaUnit then showCodeaUnitTests() end
Runner:draw()
if DisplayToggle then
scale(3)
end
popMatrix()
end
Strictly speaking, there’s no need to push and pop here, since we are at the top of the drawing stack, but it seems to me to be the proper thing to do. Let’s see what happens.
LOL, it would help if I scaled before drawing. Also I tuned up the touched a bit:
function draw()
pushMatrix()
if CodeaUnit then showCodeaUnitTests() end
if DisplayToggle then
scale(3)
end
Runner:draw()
popMatrix()
end
function touched(aTouch)
if aTouch.state == ENDED then
DisplayToggle = not DisplayToggle
end
end
The change to touched
, once I got it right, ensures that I get just one event per touch. The function fires continually whenever one or more touches are on the screen.
So we get this:
Clearly we’re zooming such that 0,0 stays fixed. I always have to figure this out again. Years ago, I’d have had it at my fingertips, but no more. I drew this picture in Paper™:
That was enough to convince me that to zoom to the center of the screen, we need to move one-third of a screen to the left and down, since we are scaling at scale 3, like this:
function draw()
pushMatrix()
if CodeaUnit then showCodeaUnitTests() end
if DisplayToggle then
scale(3)
local tx,ty = -WIDTH//3, -HEIGHT//3
translate(tx,ty)
end
Runner:draw()
popMatrix()
end
However, my reasoning is not correct. Suppose we zoom at zoom 4, would dividing by 4 work? In fact it does not. I had this working, I thought, in D1. Let’s go grab that code:
function focusOnRoomOne()
scale(4)
local appliedScale = modelMatrix()[1]
local w = WIDTH/2/appliedScale
local h = HEIGHT/2/appliedScale
local x,y = Avatar:screenLocation()
translate(w-x, h-y)
end
The name of this is bad, it now focuses on the Avatar. But that aside, I thought it worked. Let’s plug it in and play. Here’s how I’ve modified it:
function focus(x,y, zoom)
scale(zoom)
local appliedScale = modelMatrix()[1]
local w = WIDTH/2/appliedScale
local h = HEIGHT/2/appliedScale
translate(w-x, h-y)
end
Now I can call with any point, and any scale. Plugging into draw:
function draw()
pushMatrix()
if CodeaUnit then showCodeaUnitTests() end
if DisplayToggle then
focus(WIDTH//2,HEIGHT//2, 3)
end
Runner:draw()
popMatrix()
end
So far so good. Now let’s see if we can zoom to where we touch. I modify touched
:
function touched(aTouch)
if aTouch.state == ENDED then
DisplayToggle = not DisplayToggle
TouchX = aTouch.pos.x
TouchY = aTouch.pos.y
end
end
Now in draw
:
function draw()
pushMatrix()
if CodeaUnit then showCodeaUnitTests() end
if DisplayToggle then
focus(TouchX,TouchY, 3)
end
Runner:draw()
popMatrix()
end
This seems to work:
It also zooms correctly at different scales. I’m going to back away slowly and commit the code: zoom to touch point.
Player Position
One of these days I’ll work out how that zooming works. For now, let’s let it be.
I’m going to add a very simple Player class to mark player position and check zooming to it.
Player = class()
function Player:init(tileX,tileY, runner)
self.tileX,self.tileY = tileX,tileY
self.runner = runner
end
function Player:draw()
pushMatrix()
pushStyle()
spriteMode(CORNER)
local gx,gy = self.runner:graphicXY(self.tileX,self.tileY)
sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,self:gx(),self:gy(), 16,16)
end
I just decided that GameRunner is going to know how to convert tile coordinates into graphical ones.
function GameRunner:graphicXY(tileX,tileY)
return (tileX-1)*self.tileSize,(tileY-1)*self.tileSize
end
I’ll use that in Tile as well, when I get to it.
Now let’s make an avatar, put her somewhere, and draw her. I think she belongs to the GameRunner and belongs at the center of Room 1.
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(self.tileCountX,self.tileCountY, 4,15, self)
if self:hasRoomFor(r) then
placed = true
r:paint()
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
self:connectRooms()
local r1 = self.rooms[1]
local rcx,rcy = r1:center()
self.avatar = Player(rcx,rcy,self)
end
And in draw:
function GameRunner:draw()
fill(0)
stroke(255)
strokeWidth(1)
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
tile:draw()
end
end
self.avatar:draw()
end
I have high hopes for this. I have even higher hopes after this:
function Player:draw()
pushMatrix()
pushStyle()
spriteMode(CORNER)
local gx,gy = self.runner:graphicXY(self.tileX,self.tileY)
sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,gx,gy, 16,16)
end
I had only half-converted Player:draw
to the graphicXY
scheme. Now we get this:
There she is. She’s a bit scrunched, because the dimensions of that sprite are … let me look … 101x171. Here she is at 20x17. Size looks nice, actually, but she’s a bit off centered. I think we should adjust her down and over a bit:
This looks good and the code is:
function Player:draw()
local dx = -2
local dy = -3
pushMatrix()
pushStyle()
spriteMode(CORNER)
local gx,gy = self.runner:graphicXY(self.tileX,self.tileY)
sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,gx+dx,gy+dy, 20,34)
end
Commit: princess in room 1.
Summing Up
It’s Saturday, so I think that’s enough progress for now. We have some interesting capability sketched in here now. We can draw the princess at any tile x and y, and we can zoom the screen around any point. There remains the question of the grey area around the tiles, but I think I can fix that with some fake tiles, perhaps. If not, I’m sure there’s a way to pin the scrolling, but I like the fake tile idea better, it’s less messing with the math.
And I really should see if I can understand and explain how the zooming actually works. But it does. :)
Overall, things are moving forward nicely. So far, I’ve had far fewer of those confusing moments around which way things are aligned and converting from screen to tile coordinates. The tiling idea is proving itself daily.
See you next time!