tilemap – Scrolling background in PyGame with Entity Component System


I know this type of question has been asked to death for ‘normal’ games, however I am struggling with relating a scrolling background to an entity component system (ECS) approach.

Generic samples here and here and here.

What I struggle with is how to take a fairly procedural view of this problem and abstract it out to an ECS. I’m trying to get a roguelike game up and running in PyGame using Esper. I do have a proof of concept up and running where I generate a large map, however I’m having issues relating how to add a camera here as an entity that other objects would relate to. I’m guessing that I need to refactor my movement code to be based around a camera, and have each separate entity update their relative position as a result.

At present, the background is being directly drawn and needs to be abstracted to an Entity in this system, but I was somewhat holding off on that until I figured out the camera. Here is a snippet of the Renderable class that each drawn entity has:

class Renderable:
    def __init__(self, image, posx, posy, depth=0):
        self.image = image
        self.depth = depth
        self.x = posx
        self.y = posy
 
        self.curr_row, self.curr_col = convert_to_cells(posx, posy)
 
        self.w = image.get_width()
        self.h = image.get_height()
        self.rect = pygame.Rect(self.x,self.y,self.w,self.h)

And here are the Movement processor and Render processors:

class MovementProcessor(esper.Processor):
    def __init__(self, minx, maxx, miny, maxy, game_map):
        super().__init__()
        self.minx = minx
        self.maxx = maxx
        self.miny = miny
        self.maxy = maxy
        self.game_map = game_map
 
    def process(self):
        # This will iterate over every Entity that has BOTH of these components:
        for ent, (vel, rend) in self.world.get_components(Velocity, Renderable):
            # Check if movement is valid:
            newx = rend.x + int(vel.x * TILE_SIZE)
            newy = rend.y + int(vel.y * TILE_SIZE)
            new_row, new_col = convert_to_cells(newx, newy)
 
            if self.game_map.data(new_row)(new_col) > 1:
              # Update the Renderable Component's position by it's Velocity:
              rend.x = newx
              rend.y = newy
              rend.curr_row = new_row
              rend.curr_col = new_col
 
class RenderProcessor(esper.Processor):
    def __init__(self, window, clear_color=(0, 0, 0)):
        super().__init__()
        self.window = window
        self.clear_color = clear_color
 
    def process(self):
        # Clear the window:
        self.window.fill(self.clear_color)
 
        # This will iterate over every Entity that has this Component, and blit it:
        for ent, rend in self.world.get_component(Renderable):
            self.window.blit(rend.image, (rend.x, rend.y))
 
        # Flip the framebuffers
        pygame.display.flip()

Later on in my main loop I iterate over my tilemap (Map class) and simply draw it to a surface, add some debugging overlays, and then assign that surface to the entity associated with the background (bg_entity):

        # Blit background directly -- this needs to be abstracted out
        for row in range(len(game_map.data)):
          for col in range(len(game_map.data(0))):
            rect = pygame.Rect(col*TILE_SIZE,row*TILE_SIZE,TILE_SIZE,TILE_SIZE)

            # Blit a wall sprite
            if game_map.data(row)(col) in (0,1):
              bg_surface.blit(walls(game_map.data(row)(col)),rect)

            # Blit a floor/grass sprite
            elif game_map.data(row)(col) in (2,3,4,5):
              bg_surface.blit(floors(game_map.data(row)(col)-2),rect)

Here is the full listing:

https://pastebin.com/N7c4TXXB