Loading TMX Maps with pytmx
The Tiled map editor exports files in .tmx format. The pytmx library provides a convenient way to load these maps into pygame. Install it via pip and import the load_pygame function:
from pytmx.util_pygame import load_pygame
Load the map file in the level setup:
tmx_data = load_pygame('../data/map.tmx')
Understanding Map Layers
Tiled organizes map elements into layers. Each layer contains multiple tile objects. Two methods exist for retrieving layer data:
Method 1: Using .tiles()
Returns tuples of (x, y, surface) for each tile. The coordinates are in tile units, so multiply by TILE_SIZE to convert to pixel coordinates.
for x, y, surf in tmx_data.get_layer_by_name('HouseFloor').tiles():
Generic((x * TILE_SIZE, y * TILE_SIZE), surf, self.all_sprites)
Method 2: Using objects directly
Returns object instances with .x, .y, .image, and .name attributes. Coordinates are already in pixels.
for obj in tmx_data.get_layer_by_name('Trees'):
Tree((obj.x, obj.y), obj.image, self.all_sprites, obj.name)
The second method proves more flexible and easier to work with.
Sprite Class Hierarchy
Define a base Generic class for static map eelments:
class Generic(pygame.sprite.Sprite):
def __init__(self, pos, surf, groups, z=LAYERS['main']):
super().__init__(groups)
self.image = surf
self.rect = self.image.get_rect(topleft=pos)
self.z = z
For animated elements like water, extend this base class:
class Water(Generic):
def __init__(self, pos, frames, groups):
self.frames = frames
self.frame_index = 0
super().__init__(pos, self.frames[0], groups, z=LAYERS['water'])
def animate(self, dt):
self.frame_index += 6 * dt
if self.frame_index >= len(self.frames):
self.frame_index = 0
self.image = self.frames[int(self.frame_index)]
def update(self, dt):
self.animate(dt)
Simple decorative objects require minimal customization:
class WildFlower(Generic):
def __init__(self, pos, surf, groups):
super().__init__(pos, surf, groups)
class Tree(Generic):
def __init__(self, pos, surf, groups, name):
super().__init__(pos, surf, groups)
Layer Organization
Assign appropriate z-levels to create depth:
| Element | Layer | Description |
|---|---|---|
| Ground | ground |
Base terrain |
| Floor | house bottom |
Room flooring |
| Walls/Furniture/Fence | main |
Interactable elements |
| Water | water |
Animated water tiles |
| Player | main |
Player character |
LAYERS = {
'ground': 0,
'house bottom': 1,
'water': 2,
'main': 3,
}
Implementing the Pseudo-3D Effect
The key to pseudo-3D rendering lies in how sprites are drawn. When the player stands behind a tree, the tree must render on top. When in front, the player renders on top.
The solution: within each z-layer, sort sprites by their vertical position (rect.centery). Sprites with smaller y-coordinates render first, appaering behind those with larger y-coordinates.
class CameraGroup(pygame.sprite.Group):
def __init__(self):
super().__init__()
self.display_surface = pygame.display.get_surface()
self.offset = pygame.math.Vector2()
def custom_draw(self, player):
self.offset.x = player.rect.centerx - SCREEN_WIDTH / 2
self.offset.y = player.rect.centery - SCREEN_HEIGHT / 2
for layer_index in LAYERS.values():
for sprite in sorted(self.sprites(),
key=lambda s: s.rect.centery):
if sprite.z == layer_index:
offset_rect = sprite.rect.copy()
offset_rect.center -= self.offset
self.display_surface.blit(sprite.image, offset_rect)
The sorted() function with the lambda key ensures that within any given layer, sprites lower on the screen render after those higher up, creating the depth illusion.
Complete Level Setup
class Level:
def __init__(self):
self.display_surface = pygame.display.get_surface()
self.all_sprites = CameraGroup()
self.setup()
self.overlay = Overlay(self.player)
def setup(self):
tmx_data = load_pygame('../data/map.tmx')
# Static floor elements
for layer in ['HouseFloor', 'HouseFurnitureBottom']:
for x, y, surf in tmx_data.get_layer_by_name(layer).tiles():
Generic((x * TILE_SIZE, y * TILE_SIZE), surf,
self.all_sprites, LAYERS['house bottom'])
# Interactive elements
for layer in ['HouseWalls', 'HouseFurnitureTop', 'Fence']:
for x, y, surf in tmx_data.get_layer_by_name(layer).tiles():
Generic((x * TILE_SIZE, y * TILE_SIZE), surf,
self.all_sprites)
# Animated water
water_frames = import_folder('../graphics/water')
for x, y, surf in tmx_data.get_layer_by_name('Water').tiles():
Water((x * TILE_SIZE, y * TILE_SIZE), water_frames,
self.all_sprites)
# Trees with size information
for obj in tmx_data.get_layer_by_name('Trees'):
Tree((obj.x, obj.y), obj.image, self.all_sprites, obj.name)
# Decorative flowers
for obj in tmx_data.get_layer_by_name('Decoration'):
WildFlower((obj.x, obj.y), obj.image, self.all_sprites)
# Ground texture
Generic(
pos=(0, 0),
surf=pygame.image.load('../graphics/world/ground.png').convert_alpha(),
groups=self.all_sprites,
z=LAYERS['ground']
)
self.player = Player((640, 360), self.all_sprites)
def run(self, dt):
self.display_surface.fill('black')
self.all_sprites.custom_draw(self.player)
self.all_sprites.update(dt)
self.overlay.display()
This approach separates concerns clearly: Generic handles static elements, specialized subclasses handle unique behaviors, and the camera group's sorting mechanism creates convincing depth without actual 3D geometry.