Implementing Map Elements and Pseudo-3D Rendering in Pygame

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.

Tags: pygame game development tiled pytmx pseudo-3D

Posted on Thu, 29 Oct 2026 16:14:13 +0000 by nloding