Patric Hartmann
Patric Hartmann

Reputation: 748

PyGame: Panning tiled map causes gaps

I'm using PyGame 1.9.2 on Python 3.4.3 (Win8.1).

My game window consists of a tiled map using 48x48px tiles. The game's framework moves the tiles according to player input using the following method of the game's framwork class:

def moveTiles(self):
    xmove = self.parent.dt * self.panning[0] * self.panning_speed
    ymove = self.parent.dt * self.panning[1] * self.panning_speed
    for tile in self.game_map:
        tile.x += xmove
        tile.y += ymove
        tile.rect.x = int(tile.x)
        tile.rect.y = int(tile.y)
        tile.pos = (tile.rect.x, tile.rect.y)

While "self.panning_speed" is pretty self-explanatory, self.panning is a list containing two values for x and y panning (e.g. [0, 0] = no panning, [-1, 1] = panning down-left, etc.).

The tile.x/tile.y values are then "int'ed" to be used as x-/y-values of the rect and the pos (the latter used for blitting).

I add the exact same amount to every single tile's x/y value, then I use "int" on them which, to my knowledge, always floors the float down to the next lower int. So technically seen the relative change to every single tile's x/y value is the exactly same as to any other tile's x/y value.

Yet: I still get gaps between rows or columns from time to time! Those gaps appear irregularly but are very visible and when you stop panning while a gap is visible that gap will stay until you pan again.

See this image for an example: See this image for an example (black line in the red square is the gap; gold and pink are the tiles)

Where in my code can these gaps occur?

Upvotes: 4

Views: 553

Answers (3)

Micheal O'Dwyer
Micheal O'Dwyer

Reputation: 1261

I think that the main problem is that your game assets do not have padding.

Padding is extremely important to have in your assets especially when you are using cameras and float values.

Sometimes when using a camera the float value does not provide the accuracy that is needed and can cause texture bleeding or the lines that you see from time to time in your game.

The solution is to add padding to your assets.

The way I do this is as follows:

  1. Download and install GIMP

  2. Install a plugin that can add padding from here.

  3. Load every asset and add padding using the plugin in GIMP.

There is no need to change the name of any of the assets. Just add the padding and overwrite.

I hope this helped you and if you have any further questions please feel free to post a comment below!

Upvotes: 0

skrx
skrx

Reputation: 20488

Here's a solution that should work better. Just add the velocity to the panning vector when the player is moving and never move the tiles. The panning is then added to the rect.topleft position of the tiles when they're blitted to get the correct position. You have to convert the panning to integers first (create another vector, called offset in the example) before you add it to the tile positions or you'll still get gaps.

import itertools
import pygame as pg
from pygame.math import Vector2


TILE_SIZE = 44
TILE_IMG1 = pg.Surface((TILE_SIZE, TILE_SIZE))
TILE_IMG1.fill(pg.Color('dodgerblue3'))
TILE_IMG2 = pg.Surface((TILE_SIZE, TILE_SIZE))
TILE_IMG2.fill(pg.Color('aquamarine3'))


class Tile(pg.sprite.Sprite):

    def __init__(self, pos, image):
        super().__init__()
        self.image = image
        self.rect = self.image.get_rect(topleft=pos)
        self.pos = Vector2(pos)


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    dt = 0

    image_cycle = itertools.cycle((TILE_IMG1, TILE_IMG2))
    game_map = pg.sprite.Group()
    # Create tile instances and add them to the game map group.
    for y in range(16):
        for x in range(20):
            image = next(image_cycle)
            game_map.add(Tile((x*TILE_SIZE, y*TILE_SIZE), image))
        next(image_cycle)

    panning = Vector2(0, 0)
    velocity = Vector2(0, 0)

    done = False

    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_a:
                    velocity.x = 100
                elif event.key == pg.K_d:
                    velocity.x = -100
            elif event.type == pg.KEYUP:
                if event.key == pg.K_d and velocity.x < 0:
                    velocity.x = 0
                elif event.key == pg.K_a and velocity.x > 0:
                    velocity.x = 0

        # Add the velocity to the panning when we're moving.
        if velocity.x != 0 or velocity.y != 0:
            panning += velocity * dt

        game_map.update()

        screen.fill((30, 30, 30))
        # Assigning screen.blit to a local variable improves the performance.
        screen_blit = screen.blit
        # Convert the panning to ints.
        offset = Vector2([int(i) for i in panning])
        # Blit the tiles.
        for tile in game_map:
            # Add the offset to the tile's rect.topleft coordinates.
            screen_blit(tile.image, tile.rect.topleft+offset)

        pg.display.flip()
        dt = clock.tick(30) / 1000


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()

Another alternative would be to blit the tiles onto a big background surface at program start. Then you won't get gaps and that also improves the performance.

Upvotes: 1

furas
furas

Reputation: 143231

I think problem is that float can't keep every value and we have situation that 0.1 + 0.2 != 0.3 And it can makes difference between expected result and obtained result.

You can use only integer values to change position for all tiles ie.

tile.rect.x = int(xmove)
tile.rect.y = int(ymove)

This way you can loose only small value (smaller then one pixel) but if you cumulate many small values then you can get few pixels.

So you can try to cumulate differences between float and integer values.

I can't check if this works correctly but I would do something like this

def moveTiles(self):

    # add moves to already cumulated differences betweeen floats and integers
    self.xcululated += self.parent.dt * self.panning[0] * self.panning_speed
    self.ycululated += self.parent.dt * self.panning[1] * self.panning_speed

    # get only integer values
    xmove = int(self.xcululated)
    ymove = int(self.ycululated)

    # remove integers from floats
    self.xcululated += xmove
    self.ycululated += ymove

    for tile in self.game_map:
        # use the same integer values for all tiles
        tile.rect.x += xmove
        tile.rect.y += tmove

Upvotes: 1

Related Questions