Camera Fumbling
This morning I’m going to put a Camera2D into my dungeon and see if I can figure out how to zoom and pan and such. I am hopeful but also expect confusion.
I read a bit about the Camera2D, but feel the need for more explanation than I was able to find. There might be something in the Arcade Discord, but to me, finding anything in a Discord is like trying to find that Vivaldi thing with the flutes. As the clerk at the record store told me: “Vivaldi is a jungle”.
My idea here is pretty weak. Clearly in an actual game, we’ll want a large view of a small area of the dungeon, where you are, and perhaps a small view of the whole dungeon, showing a mini-map of where you’ve been. My fantasy is that we could just have two differently-zoomed views of the same structure. The zoomed-in one would scroll as we move, must like the side-scroller we looked at yesterday.
Seems easy enough in theory. Let’s get started.
Installing the Camera
I think we “just” need an instance variable holding a Camera2D, and to give it a value in its zoom property. We’ll have to tell it where to center itself, we’ll have to move its center as Dot explores, and we’ll want to constrain it so that it never tries to draw outside the bounds of the dungeon.
Here goes:
class DungeonView(arcade.View):
def __init__(self, dungeon, testing=False):
if not testing:
super().__init__()
self.dungeon = dungeon
self.key_lock = None
self.shape_list = None
self.camera = None
if testing:
return
def setup(self):
self.camera = arcade.Camera2D()
self.shape_list = arcade.shape_list.ShapeElementList()
self.create_rooms(self.shape_list)
def on_draw(self):
draw_start_time = timeit.default_timer()
self.clear()
with self.camera.activate():
self.shape_list.draw()
self.draw_contents()
Run main to see if that draws OK. It does. Set zoom and try again.
def setup(self):
self.camera = arcade.Camera2D()
self.camera.zoom = 4
self.shape_list = arcade.shape_list.ShapeElementList()
self.create_rooms(self.shape_list)
The result is delightful!

On my screen, that is a very playable size. It’s 16x14 cells, and could of course be wider and possibly high enough to handle 16 high at this scale.
We’ll commit this. We can always back up if we need to.
Next, I think we’ll start Dot at the center and see if we can make the camera track her. That will surely allow us to get drawing outside the dungeon if we move her close enough to the edge. In main, instead of setting Dot far far away I set her at 32,28. She duly shows up in the middle of that cross.
Now how do we move the camera? I’ll sneak a look at one of the examples. I try this:
def on_draw(self):
draw_start_time = timeit.default_timer()
self.clear()
cx, cy = self.dungeon.player_cell.xy
self.camera.position = (cx*cell_size, cy*cell_size)
with self.camera.activate():
self.shape_list.draw()
self.draw_contents()
I initially forgot to multiply by cell_size, which left me in the dark. With cell_size, this works as intended. Dot is in the middle and as I hit the arrow keys, she moves to the next cell.
And, as expected, if I move her too far up or to the side, empty space starts scrolling in:

What we’d like is for the scrolling to stop with that bar that Dot’s on at the top of the screen, and then she’d walk up to the top without scrolling in the black.
We have some examples that do constraints, but so far I don’t quite see what they are doing. Here’s one:
def center_camera_to_player(self):
# Move the camera to center on the player
self.camera_sprites.position = arcade.math.smerp_2d(
self.camera_sprites.position,
self.player_sprite.position,
self.window.delta_time,
FOLLOW_DECAY_CONST,
)
# Constrain the camera's position to the camera bounds.
self.camera_sprites.view_data.position = arcade.camera.grips.constrain_xy(
self.camera_sprites.view_data, self.camera_bounds
)
What is camera_bounds? Maybe we can copy this without understanding, in the hope of understanding after we have it in place.
# Use the tilemap's size to correctly set the camera's bounds.
# Because how how shallow the map is we don't offset the bounds height
self.camera_bounds = arcade.LRBT(
self.window.width/2.0,
tile_map.width * GRID_PIXEL_SIZE - self.window.width/2.0,
self.window.height/2.0,
tile_map.height * GRID_PIXEL_SIZE
)
That is a rectangle the same height as the window and half as wide, starting at the center of the x axis and extending to the right side of the window. I don’t understand that. I’ll put similar code in and fiddle, I guess.
I try the full window size and get the same behavior as before, no surprise.
self.camera_bounds = arcade.LRBT(
0, 64*cell_size, 0, 56*cell_size)
We have 16 cells high and 14 wide. Let’s try starting at 8 ending at 64-8, etc.
w = 64
h = 56
self.camera_bounds = arcade.LRBT(
8*cell_size,
w*cell_size - 8*cell_size,
7*cell_size, h*cell_size - 7*cell_size)
That works perfectly!! The window scrolls to keep Dot centered, until we scroll in a boundary cell, at which point scrolling stops and Dot can walk the rest of the way to the boundary.
The bounds are a rectangle inset 8 cells on left and right, 7 cells up and down. Now I need to figure out how I knew that. Ah, OK. The grid is 64x56, and the window is sized to that. Zoom is 4, so we’ll see 64/4 or 16 wide, 56/4 or 14 high. To keep from scrolling into space, we need to reserve half that much space on each side.
So we can generalize the code a bit, resulting in this setup:
def setup(self):
self.camera = arcade.Camera2D()
zoom = 4
self.camera.zoom = zoom
w = 64
h = 56
width = w*cell_size
height = h*cell_size
width_in_cells = w//zoom
height_in_cells = h//zoom
width_margin =width_in_cells//2*cell_size
height_margin = height_in_cells//2*cell_size
self.camera_bounds = arcade.LRBT(
width_margin,
width - width_margin,
height_margin,
height - height_margin)
self.shape_list = arcade.shape_list.ShapeElementList()
self.create_rooms(self.shape_list)
We should extract that camera stuff, resulting in:
def setup(self):
self.setup_camera()
self.shape_list = arcade.shape_list.ShapeElementList()
self.create_rooms(self.shape_list)
def setup_camera(self):
self.camera = arcade.Camera2D()
zoom = 4
self.camera.zoom = zoom
w = 64
h = 56
width = w * cell_size
height = h * cell_size
width_in_cells = w // zoom
height_in_cells = h // zoom
width_margin = width_in_cells // 2 * cell_size
height_margin = height_in_cells // 2 * cell_size
self.camera_bounds = arcade.LRBT(
width_margin,
width - width_margin,
height_margin,
height - height_margin)
Commit that: Game now displays at zoom 4, with scrolling.
That’ll do, pig, that’ll do. Let’s sum up.
Summary
That went far better than it might have. I think that my mental model of the camera bounds is roughly right, we set them with margins on all four sides, such that the margin is half the width / height of the screen. Maybe there’s more to it than that, because if that were the whole story, I wonder why the documentation and variable names don’t make that more clear. But it seems right, at least now, for our purposes.
We have discovered that the view needs to know the dungeon layout size, and even the Layout doesn’t retain that information, so we’ll need to sort that out. Right now we have the magic 64 and 56 in the code.
I think we could turn Dot into a sprite of some kind and put a cute picture on the screen. Maybe we’ll do that for fun.
If the game is to become real, we’ll want to tile the floor with something that looks like flooring, we’ll want to do something about walls, and so ad infinitum. The wall issue is particularly troubling to me. Is a wall a thing of zero thickness, or should we be making them tile-sized, actually taking up space? In due time. To every thing there is a season.
We done good today. Time for a treat. See you next time!