Victoria
Victoria

Reputation: 51

Overlay tiles onto sprite in pygame

I am creating a tile-based 2d overworld for a game - heavily influenced by Pokemon - using pygame/python, Tiled for .tmx files, and the tmx library by Richard Jones. The code I'm using is mostly based on this wonderful demo of Pallet Town in python.

The game runs just fine, however, I am having problems with making tiles on the map (e.g. houses, trees) overlap the player sprite when it would make sense for the player sprite to disappear behind them. For example: in the image here, principles of depth perception would tell us that the house in the foreground should occlude the player in the background, but because the map is 2D there is no depth and therefore no occlusion. I would love to add depth, but seeing as I'm very new to pygame (and python in general), I'm at a loss at how to draw the relevant foreground objects over the sprite.

Luckily I'm not alone in this problem and plenty of documentation on possible solutions exist. For example:

However, this code isn't typically written for python and I'm not sure how to implement it in my situation. Sorting/drawing by z position (or by a 'depth' property) seems like the most sensible thing to do, but looking at the tmx library I can only find x and y values mentioned. Adding the player sprite to an empty object layer in Tiled is also a solution, but once again I'm unsure of how to do this and all my attempts have led to error messages. (Attempts not detailed here because I honestly don't know what I did and it didn't work anyway.)

My current code is as follows:

class Player(pygame.sprite.Sprite):
    def __init__(self, location, collStart, orientation, *groups):
        super(Player, self).__init__(*groups)
        self.image = pygame.image.load('sprites/player.png')
        self.imageDefault = self.image.copy()
        self.rect = pygame.Rect(location, (26,26))
        self.collider = pygame.Rect(collStart, (13,13))
        self.orient = orientation 
        self.holdTime = 0
        self.walking = False
        self.dx = 0
        self.step = 'rightFoot'
        # Set default orientation
        self.setSprite()
        self.speed = pygame.time.get_ticks() + 50  # slows down walking speed 
        by .5 sec (current time + 50 ms)


    def setSprite(self):
        # this function contains information about where to find which sprite 
        in the sprite sheet, probably not relevant here.

    def update(self, dt, game):
        key = pygame.key.get_pressed()
        if pygame.time.get_ticks() >= self.speed:
            self.speed = pygame.time.get_ticks() + 50
            # Setting orientation and sprite based on key input, removed the 
            #code here because it wasn't relevant
            #[....]   
            # Walking mode enabled if a button is held for 0.1 seconds
            if self.holdTime >= 100:
                self.walking = True
            lastRect = self.rect.copy()
            lastColl = self.collider.copy() # collider covers the bottom section of the sprite
            # Code for walking in the direction the player is facing, not relevant here
            #[....]      
            # Collision detection:
            # Reset to the previous rectangle if player collides
            # with anything in the foreground layer
            if len(game.tilemap.layers['triggers'].collide(self.collider,
                                                            'solid')) > 0:
                self.rect = lastRect
                self.collider = lastColl
            # Area entry detection, loads dialog screen from the dialog file:
            elif len(game.tilemap.layers['triggers'].collide(self.collider,
                                                            'entry')) > 0:
                entryCell = game.tilemap.layers['triggers'].find('entry')[0]
                game.fadeOut()
                run()
                pygame.quit()
                quit()

                return
            if self.dx == 16:
                # Makes the player appear to take steps w/ different feet, not relevant here
            #[....]
            # After traversing 32 pixels, the walking animation is done
            if self.dx == 32:
                self.walking = False
                self.setSprite()
                self.dx = 0

            game.tilemap.set_focus(self.rect.x, self.rect.y)

class Game(object):
    def __init__(self, screen):
        self.screen = screen

    def initArea(self, mapFile):
        """Load maps and initialize sprite layers for each new area"""
        self.tilemap = tmx.load(mapFile, screen.get_size())
        self.players = tmx.SpriteLayer()
        self.objects = tmx.SpriteLayer()
        # In case there is no sprite layer for the current map
        except KeyError:
            pass
        else:
            self.tilemap.layers.append(self.objects)
        # Initializing player sprite
        startCell = self.tilemap.layers['triggers'].find('playerStart')[0]
        self.player = Player((startCell.px, startCell.py), (startCell.px, 
        startCell.bottom-4),
                             startCell['playerStart'], self.players)
        self.tilemap.layers.append(self.players)
        self.tilemap.set_focus(self.player.rect.x, self.player.rect.y)  

    def main(self):
        clock = pygame.time.Clock()
        self.initArea('test tilemap.tmx')

        while 1:
            dt = clock.tick(30)

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return
                if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                    return

            self.tilemap.update(dt, self)
            screen.fill((0,0,0))
            self.tilemap.draw(self.screen)
            pygame.display.flip()

Once again, I'm using the tmx library found here. Maybe something needs to be changed there? Hopefully, someone can help me figure this out. It's definitely possible, as shown by this pokemon clone in python (no source code available, sadly). Also, first-time StackOverflow user so let me know if I'm committing any faux-passes :)

Upvotes: 3

Views: 1785

Answers (1)

Victoria
Victoria

Reputation: 51

Figured it out! As suggested by Kingsley in the comments, the solution was to change the draw order of the layers. Layers were being drawn in order of a list in the Layers class, with the player sprite having the highest index and thus being drawn on top of everything else. Moving the player between the background and foreground layer in the list made it appear behind the foreground objects. To do this I added the following code to the initArea function in the Game class:

  def initArea(self, mapFile):
    """Load maps and initialize sprite layers for each new area"""
    self.tilemap = tmx.load(mapFile, screen.get_size())
    self.players = tmx.SpriteLayer()
    self.objects = tmx.SpriteLayer()
    # Initializing player sprite
    startCell = self.tilemap.layers['triggers'].find('playerStart')[0]
    self.player = Player((startCell.px, startCell.py), (startCell.px, startCell.bottom-4),
                         startCell['playerStart'], self.players)
    foregroundItem = self.tilemap.layers.__getitem__("foreground") # finds the layer called foreground
    foregroundIndex = self.tilemap.layers.index(foregroundItem)  # finds the position of the foreground layer in the Layers list (Layers class specified in .tmx file)
    self.tilemap.layers.insert(foregroundIndex-1, self.players)  # move the Player layer one layer below the foreground layer
    self.tilemap.set_focus(self.player.rect.x, self.player.rect.y)

I'll experiment a bit more tonight, but for now this solution seems to work. Thanks for the help!

Upvotes: 2

Related Questions