Dot, the Intrepid Adventurer
I’ve added in Dot and it’s clear we have work to do. Result: 1000X faster.
I was just messing about while in an online chat and put in Dot:
def on_draw(self):
self.clear()
self.draw_rooms()
self.draw_contents()
def on_key_press(self, symbol: int, modifiers: int) -> bool | None:
if self.key_lock is not None:
return
self.key_lock = symbol
if symbol == arcade.key.RIGHT:
self.dungeon.move_adventurer_east()
elif symbol == arcade.key.LEFT:
self.dungeon.move_adventurer_west()
elif symbol == arcade.key.UP:
self.dungeon.move_adventurer_north()
elif symbol == arcade.key.DOWN:
self.dungeon.move_adventurer_south()
else:
self.key_lock = None
def on_key_release(self, symbol: int, modifiers: int) -> bool | None:
if symbol == self.key_lock:
self.key_lock = None
def draw_contents(self):
adventurer_cell = self.dungeon.player_cell
cx = adventurer_cell.x*cell_size + cell_size//2
cy = adventurer_cell.y*cell_size + cell_size//2
arcade.draw_circle_filled(cx, cy, cell_size//2, arcade.color.RED)
# pygame.draw.circle(self.screen, "red", [cx, cy], cell_size//2)
x,y = adventurer_cell.x, adventurer_cell.y
txt = f'({x},{y})'
# font = pygame.font.SysFont("monospace", 20)
# surf = font.render(txt, True, 'white')
# self.screen.blit(surf, (cx, cy - cell_size))
As you can see, I just commented out the pygame stuff and put in a circle in arcade style. Since arcade has the origin where God intended it to be, in the lower left, I had to change move_adventurer_north and ...south, inverting the sign of the movement.
It was no surprise when Dot showed up just where she belonged, and she moves as intended. However, she moves V_E_R_Y__ S_L_O_W_L_Y. I assume that is because we are computing and drawing our rectangles and lines on every draw cycle. Let’s put in a timer such as they had in the samples.
def on_draw(self):
draw_start_time = timeit.default_timer()
self.clear()
self.draw_rooms()
self.draw_contents()
draw_time = timeit.default_timer() - draw_start_time
output = f"Drawing time: {draw_time:.3f} seconds per frame."
arcade.draw_text(output, 20, screen_height - 40, arcade.color.WHITE, 18)
This reports about 1/8 second per frame. Not 1/60 which is what we’re supposed to get.
Our mission will be to save all the rectangles and lines in a sprite list or whatever it’s called. It’s a shape list. And it goes like this:
main.py
...
view = DungeonView(dungeon)
view.setup() # <=== added
window.show_view(view)
arcade.run()
class DungeonView:
def setup(self):
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()
# self.draw_rooms() # removed
self.shape_list.draw() # added
self.draw_contents()
draw_time = timeit.default_timer() - draw_start_time
output = f"Drawing time: {draw_time:.6f} seconds per frame."
arcade.draw_text(output, 20, screen_height - 40, arcade.color.WHITE, 18)
class RoomView:
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
self.create_colored_cell(cell, shape_list)
self.create_border(cell, shape_list)
def create_colored_cell(self, cell, shape_list: ShapeElementList):
dx = cell.x * cell_size + cell_size //2
dy = cell.y * cell_size + cell_size //2
shape = arcade.shape_list.create_rectangle_filled(dx, dy, cell_size, cell_size, arcade.color.GRAY)
shape_list.append(shape)
def create_border(self, cell, shape_list):
borders: BorderList = self.layout.get_borders(cell)
for border in borders:
start, finish = border.line_coordinates(cell.xy, cell_size)
color_name = border.select_boundary(self.boundary_colors)
if color_name == "black":
color = arcade.color.BLACK
elif color_name == "green":
color = arcade.color.GREEN
elif color_name == "white":
color = arcade.color.WHITE
else:
color = arcade.color.RED
shape = arcade.shape_list.create_line(*start, *finish, color)
shape_list.append(shape)
Everything looks the same, Dot moves freely, and the elapsed time per draw is about 0.0001 or 0.0002 seconds, well inside the 0.01667 we need for 60 frames per second.
I think we’re about done here but let’s move those colors into the color dictionary.
class RoomView:
boundary_colors = {"open":arcade.color.BLACK,
"partition":arcade.color.GREEN,
"door":arcade.color.WHITE,
"wall":arcade.color.RED}
def create_border(self, cell, shape_list):
borders: BorderList = self.layout.get_borders(cell)
for border in borders:
start, finish = border.line_coordinates(cell.xy, cell_size)
color = border.select_boundary(self.boundary_colors)
shape = arcade.shape_list.create_line(*start, *finish, color)
shape_list.append(shape)
Works. Commit: convert to shape list.
Remove unneeded draw methods. I temporarily skipped the key tests because they were wired to pygame’s handlers. We’ll update those shortly.
Summary
So in just a little time, we have converted from calculating the dungeon layout on every cycle, at an expense of over 1/8th of a second, to storing the rectangles and lines in a shape list and drawing the list all at once, reducing the time by three orders of magnitude, which is fairly good for an afternoon’s jaunt.
Arcade seems easy to use. I did have to change the rectangle calls to use the rectangle center, as there is no equivalent to LBWH for shape lists, but center orientation is perhaps better anyway. We’ll look at that to see if there is any consistency or simplicity to be gained.
I think we are fully converted from pygame to Arcade. Took a couple of hours including writing the articles and making the pictures.
I am satisfied with the day. See you next time!
Just for completeness, here are the new DungeonView and RoomView files.
class DungeonView(arcade.View):
def __init__(self, dungeon, testing=False):
super().__init__()
self.dungeon = dungeon
self.key_lock = None
self.shape_list = None
if testing:
return
def setup(self):
self.shape_list = arcade.shape_list.ShapeElementList()
self.create_rooms(self.shape_list)
def create_rooms(self, shape_list):
for room in self.dungeon.rooms:
RoomView(self.dungeon, room).create(shape_list)
def on_draw(self):
draw_start_time = timeit.default_timer()
self.clear()
# self.draw_rooms()
self.shape_list.draw()
self.draw_contents()
def on_key_press(self, symbol: int, modifiers: int) -> bool | None:
if self.key_lock is not None:
return
self.key_lock = symbol
if symbol == arcade.key.RIGHT:
self.dungeon.move_adventurer_east()
elif symbol == arcade.key.LEFT:
self.dungeon.move_adventurer_west()
elif symbol == arcade.key.UP:
self.dungeon.move_adventurer_north()
elif symbol == arcade.key.DOWN:
self.dungeon.move_adventurer_south()
else:
self.key_lock = None
def on_key_release(self, symbol: int, modifiers: int) -> bool | None:
if symbol == self.key_lock:
self.key_lock = None
def draw_contents(self):
adventurer_cell = self.dungeon.player_cell
cx = adventurer_cell.x*cell_size + cell_size//2
cy = adventurer_cell.y*cell_size + cell_size//2
arcade.draw_circle_filled(cx, cy, cell_size//2, arcade.color.RED)
class RoomView:
boundary_colors = {"open":arcade.color.BLACK,
"partition":arcade.color.GREEN,
"door":arcade.color.WHITE,
"wall":arcade.color.RED}
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
self.create_colored_cell(cell, shape_list)
self.create_border(cell, shape_list)
def create_border(self, cell, shape_list):
borders: BorderList = self.layout.get_borders(cell)
for border in borders:
start, finish = border.line_coordinates(cell.xy, cell_size)
color = border.select_boundary(self.boundary_colors)
shape = arcade.shape_list.create_line(*start, *finish, color)
shape_list.append(shape)
def create_colored_cell(self, cell, shape_list: ShapeElementList):
dx = cell.x * cell_size + cell_size //2
dy = cell.y * cell_size + cell_size //2
shape = arcade.shape_list.create_rectangle_filled(dx, dy, cell_size, cell_size, arcade.color.GRAY)
shape_list.append(shape)