diff --git a/apps/pygame_sdl2_demo.py b/apps/pygame_sdl2_demo.py index 7ad7578..67082dd 100644 --- a/apps/pygame_sdl2_demo.py +++ b/apps/pygame_sdl2_demo.py @@ -1,15 +1,17 @@ """ This is tested on pygame 2.0.1 and python 3.9.6. -Leif Theden "bitcraft", 2012-2024 +Leif Theden "bitcraft", 2012-2025 Rendering demo for the TMXLoader. """ -import dataclasses + import logging +from dataclasses import dataclass +from pathlib import Path +from typing import Optional import pygame -import pygame._sdl2 from pygame._sdl2 import Renderer, Window from pygame.locals import * @@ -20,7 +22,7 @@ logger = logging.getLogger(__name__) -@dataclasses.dataclass +@dataclass class GameContext: window: Window renderer: Renderer @@ -29,10 +31,9 @@ class GameContext: class TiledRenderer(object): """ Super simple way to render a tiled map - """ - def __init__(self, ctx: GameContext, filename) -> None: + def __init__(self, ctx: GameContext, filename: str) -> None: self.ctx = ctx self.tmx_data = tm = load_pygame_sdl2(ctx.renderer, filename) self.pixel_size = tm.width * tm.tilewidth, tm.height * tm.tileheight @@ -46,17 +47,15 @@ def render_map(self) -> None: Scrolling is a often requested feature, but pytmx is a map loader, not a renderer! If you'd like to have a scrolling map renderer, please see my pyscroll project. - """ # iterate over all the visible layers, then draw them for layer in self.tmx_data.visible_layers: if isinstance(layer, TiledTileLayer): self.render_tile_layer(layer) - def render_tile_layer(self, layer) -> None: + def render_tile_layer(self, layer: TiledTileLayer) -> None: """ Render all TiledTiles in this layer - """ tw = self.tmx_data.tilewidth th = self.tmx_data.tileheight @@ -69,7 +68,7 @@ def render_tile_layer(self, layer) -> None: image.srcrect, (x, y, tw, th), image.angle, - None, + image.center, image.flipx, image.flipy, ) @@ -78,28 +77,25 @@ def render_tile_layer(self, layer) -> None: class SimpleTest: """ Basic app to display a rendered Tiled map - """ - def __init__(self, ctx: GameContext, filename) -> None: + def __init__(self, ctx: GameContext, filename: Path) -> None: self.ctx = ctx - self.map_renderer = None - self.running = False - self.exit_status = 0 + self.map_renderer: Optional[TiledRenderer] = None + self.running: bool = False + self.exit_status: int = 0 self.load_map(filename) - def load_map(self, filename) -> None: + def load_map(self, filename: Path) -> None: """ Create a renderer, load data, and print some debug info - """ - self.map_renderer = TiledRenderer(self.ctx, filename) + self.map_renderer = TiledRenderer(self.ctx, filename.as_posix()) logger.info("Objects in map:") for obj in self.map_renderer.tmx_data.objects: - logger.info(obj) - for k, v in obj.properties.items(): - logger.info("%s\t%s", k, v) + logger.info("Object: %s", obj) + logger.debug("Properties: %s", vars(obj)) logger.info("GID (tile) properties:") for k, v in self.map_renderer.tmx_data.tile_properties.items(): @@ -112,10 +108,10 @@ def load_map(self, filename) -> None: def draw(self) -> None: """ Draw our map to some surface (probably the display) - """ - self.map_renderer.render_map() - self.ctx.renderer.present() + if self.map_renderer: + self.map_renderer.render_map() + self.ctx.renderer.present() def handle_input(self) -> None: try: @@ -136,8 +132,13 @@ def handle_input(self) -> None: self.exit_status = 0 self.running = False - def run(self): - """This is our app main loop""" + def run(self) -> int: + """ + Main loop of the app. + + Returns: + int: Exit status (0 = success, 1 = error) + """ self.running = True self.exit_status = 1 @@ -149,9 +150,6 @@ def run(self): if __name__ == "__main__": - import glob - import os.path - pygame.init() pygame.font.init() window = Window("pytmx map viewer", size=(600, 600)) @@ -164,11 +162,12 @@ def run(self): # loop through a bunch of maps in the maps folder try: - for filename in glob.glob(os.path.join("apps", "data", "*.tmx")): + for filename in Path("apps/data").glob("*.tmx"): logger.info("Testing %s", filename) renderer.clear() if not SimpleTest(ctx, filename).run(): break - except: + except Exception as e: + logger.exception("Unhandled exception: %s", e) pygame.quit() raise diff --git a/pytmx/util_pygame_sdl2.py b/pytmx/util_pygame_sdl2.py index 48e389a..8380291 100644 --- a/pytmx/util_pygame_sdl2.py +++ b/pytmx/util_pygame_sdl2.py @@ -1,5 +1,5 @@ """ -Copyright (C) 2012-2024, Leif Theden +Copyright (C) 2012-2025, Leif Theden This file is part of pytmx. @@ -16,77 +16,104 @@ You should have received a copy of the GNU Lesser General Public License along with pytmx. If not, see . """ + import dataclasses import logging +from collections.abc import Callable +from dataclasses import dataclass from functools import partial from typing import Any, Optional from pygame.rect import Rect import pytmx +from pytmx.pytmx import ColorLike, PointLike, TileFlags logger = logging.getLogger(__name__) try: import pygame - from pygame._sdl2 import Image, Renderer, Texture, Window + from pygame._sdl2 import Renderer, Texture except ImportError: logger.error("cannot import pygame (is it installed?)") raise -@dataclasses.dataclass(order=True) +@dataclass(order=True) class PygameSDL2Tile: texture: Texture srcrect: Rect - size: tuple[int, int] + size: PointLike angle: float = 0.0 - center: None = None + center: Optional[PointLike] = None flipx: bool = False flipy: bool = False -def handle_flags(flags: Optional[pytmx.TileFlags]) -> tuple[float, bool, bool]: +def handle_flags(flags: Optional[TileFlags]) -> tuple[float, bool, bool]: """ Return angle and flip values for the SDL2 renderer - """ if flags is None: return 0.0, False, False if flags.flipped_diagonally: if flags.flipped_vertically: - return 270, False, False + return 270.0, False, False else: - return 90, False, False + return 90.0, False, False else: return 0.0, flags.flipped_horizontally, flags.flipped_vertically -def pygame_sd2_image_loader(renderer: Renderer, filename: str, colorkey, **kwargs): +def pygame_sd2_image_loader( + renderer: Renderer, + filename: str, + colorkey: Optional[ColorLike] = None, + **kwargs: Any, +) -> Callable[ + [Optional[Rect], Optional[TileFlags], Optional[PointLike]], PygameSDL2Tile +]: """ pytmx image loader for pygame - """ image = pygame.image.load(filename) - parent_rect = image.get_rect() - texture = Texture.from_surface(renderer, image) - def load_image(rect=None, flags=None) -> PygameSDL2Tile: + if colorkey: + if isinstance(colorkey, str): + if not colorkey.startswith("#") and len(colorkey) in (6, 8): + colorkey = pygame.Color(f"#{colorkey}") + else: + colorkey = pygame.Color(colorkey) + elif isinstance(colorkey, tuple) and 3 <= len(colorkey) <= 4: + colorkey = pygame.Color(colorkey) + else: + logger.error("Invalid colorkey") + raise ValueError("Invalid colorkey") + + parent_rect: Rect = image.get_rect() + texture: Texture = Texture.from_surface(renderer, image) + + def load_image( + rect: Optional[Rect] = None, + flags: Optional[TileFlags] = None, + center: Optional[PointLike] = None, + ) -> PygameSDL2Tile: if rect: - assert parent_rect.contains(rect) + assert parent_rect.contains(rect), "Tile rect must be within image bounds" else: rect = parent_rect angle, flipx, flipy = handle_flags(flags) rect = Rect(*rect) size = rect.size + return PygameSDL2Tile( texture=texture, srcrect=rect, size=size, angle=angle, - center=None, + center=center, flipx=flipx, flipy=flipy, ) @@ -99,7 +126,6 @@ def load_pygame_sdl2( ) -> pytmx.TiledMap: """ Load a TMX file, images, and return a TiledMap class - """ kwargs["image_loader"] = partial(pygame_sd2_image_loader, renderer) return pytmx.TiledMap(filename, *args, **kwargs)