Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 30 additions & 31 deletions apps/pygame_sdl2_demo.py
Original file line number Diff line number Diff line change
@@ -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 *

Expand All @@ -20,7 +22,7 @@
logger = logging.getLogger(__name__)


@dataclasses.dataclass
@dataclass
class GameContext:
window: Window
renderer: Renderer
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
)
Expand All @@ -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():
Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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))
Expand All @@ -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
60 changes: 43 additions & 17 deletions pytmx/util_pygame_sdl2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Copyright (C) 2012-2024, Leif Theden <leif.theden@gmail.com>
Copyright (C) 2012-2025, Leif Theden <leif.theden@gmail.com>

This file is part of pytmx.

Expand All @@ -16,77 +16,104 @@
You should have received a copy of the GNU Lesser General Public
License along with pytmx. If not, see <http://www.gnu.org/licenses/>.
"""

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,
)
Expand All @@ -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)