Part 3 - The generic Entity, the Engine and the render function

action.py

The action.py file remains the same:

class Action:
    pass


class EscapeAction(Action):
    pass


class MovementAction(Action):
    def __init__(self, dx: int, dy: int):
        super().__init__()

        self.dx = dx
        self.dy = dy

input_handlers.py

Event handling is moved to input_handlers.py:

import sdl2

from actions import Action, EscapeAction, MovementAction


def event_handler(event) -> None:
    action = None
    if event.type == sdl2.SDL_QUIT:
        action = EscapeAction()
    elif event.type == sdl2.SDL_KEYDOWN:
        if event.key.keysym.sym == sdl2.SDLK_UP:
            action = MovementAction(dx=0, dy=-1)
        elif event.key.keysym.sym == sdl2.SDLK_DOWN:
            action = MovementAction(dx=0, dy=1)
        elif event.key.keysym.sym == sdl2.SDLK_LEFT:
            action = MovementAction(dx=-1, dy=0)
        elif event.key.keysym.sym == sdl2.SDLK_RIGHT:
            action = MovementAction(dx=1, dy=0)
        elif event.key.keysym.sym == sdl2.SDLK_ESCAPE:
            action = EscapeAction()

    return action

entity.py

A new file is created for players, npcs, enemies, items - entity.py:

from typing import Tuple

import sdl2.ext


class Entity:
    """
    A generic object to represent players, enemies, items, etc.
    """

    def __init__(
        self,
        x: int,
        y: int,
        char: str,
        color: Tuple[int, int, int],
        renderer,
        font_size=20,
        tile_size=20,
    ):
        self.x = x
        self.y = y
        self.char = char
        self.color = sdl2.ext.Color(*color)
        self.font_size = font_size
        self.bg_color = sdl2.ext.Color(0, 0, 0, 255)
        self.tile_size = tile_size
        self.renderer = renderer

        font_manager = sdl2.ext.FontManager(
            font_path="C:\\Windows\\Fonts\\arial.ttf",
            size=font_size,
            color=self.color,
            bg_color=self.bg_color,
        )
        factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
        self.text = factory.from_text(self.char, fontmanager=font_manager)

        self.x_offset = -self.text.size[0] // 2
        self.y_offset = -self.text.size[1] // 2
        self.text_width = self.text.size[0]
        self.text_height = self.text.size[1]

        self.pixel_x = self.x * self.tile_size + self.x_offset
        self.pixel_y = self.y * self.tile_size + self.y_offset

    def move(self, dx: int, dy: int) -> None:
        self.x += dx
        self.y += dy

        self.pixel_x = self.x * self.tile_size + self.x_offset
        self.pixel_y = self.y * self.tile_size + self.y_offset

In more detail:

Tile coordinates of the entity:

self.x = x
self.y = y

Character and color for the character:

self.char = char
self.color = sdl2.ext.Color(*color)

Creation of the text sprite:

font_manager = sdl2.ext.FontManager(
    font_path="C:\\Windows\\Fonts\\arial.ttf",
    size=font_size,
    color=self.color,
    bg_color=self.bg_color,
)
factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
self.text = factory.from_text(self.char, fontmanager=font_manager)

Calculation of the pixel values: position, sprite offset, pixel text width and height:

self.x_offset = -self.text.size[0] // 2
self.y_offset = -self.text.size[1] // 2
self.text_width = self.text.size[0]
self.text_height = self.text.size[1]

self.pixel_x = self.x * self.tile_size + self.x_offset
self.pixel_y = self.y * self.tile_size + self.y_offset

In the move function:

def move(self, dx: int, dy: int) -> None:

first the tile coordinates are updated:

self.x += dx
self.y += dy

then the pixel coordinates are updated:

self.pixel_x = self.x * self.tile_size + self.x_offset
self.pixel_y = self.y * self.tile_size + self.y_offset

engine.py

The logic for handling events and rendering is moved to engine.py:

from typing import Set, Iterable, Any, List

import sdl2.ext

from actions import EscapeAction, MovementAction
from entity import Entity
from input_handlers import event_handler


BLACK = sdl2.ext.Color(0, 0, 0)


class Engine:
    def __init__(self, entities: List[Entity], player: Entity):
        self.entities = entities
        self.event_handler = event_handler
        self.player = player

    def handle_events(self, events: Iterable[Any]) -> None:
        for event in events:
            action = event_handler(event)

            if action is None:
                continue

            if isinstance(action, MovementAction):
                self.player.move(dx=action.dx, dy=action.dy)

            elif isinstance(action, EscapeAction):
                sdl2.ext.quit()
                raise SystemExit()

    def render(self, renderer, screen_width: int, screen_height: int) -> None:
        for entity in self.entities:
            renderer.copy(
                entity.text,
                dstrect=(
                    entity.pixel_x + screen_width // 2,
                    entity.pixel_y + screen_height // 2,
                    entity.text_width,
                    entity.text_height,
                ),
            )

        renderer.present()

        renderer.clear(BLACK)

main.py

And the main.py file now is very small:

import sdl2
import sdl2.ext


from entity import Entity
from engine import Engine


def run() -> None:
    sdl2.ext.init()

    screen_width = 640
    screen_height = 480

    window = sdl2.ext.Window(
        "PySDL2 Roguelike Tutorial", size=(screen_width, screen_height)
    )
    window.show()

    renderer = sdl2.ext.Renderer(window)

    player = Entity(0, 0, "@", (255, 255, 255), renderer)
    npc = Entity(0 - 5, 0, "@", (255, 255, 0), renderer)
    entities = [npc, player]

    engine = Engine(entities=entities, player=player)

    while True:
        engine.render(renderer, screen_width, screen_height)

        events = sdl2.ext.get_events()

        engine.handle_events(events)


if __name__ == "__main__":
    run()