Night
Night

Reputation: 21

How can I get tile collisions working in Pygame with a tile map of sprites?

I need help figuring out tile collisions for a platformer I'm currently making in Pygame. I have movement, with gravity working, as well as a tile map, but I don't really understand how to get collisions with the sprites working. I did make a list that appends all of the tiles that aren't 0 (so 1 or 2) into a list called tile_collisions (see line 113) but I don't really know what to do with that. Also, you can ignore the commented-out code, that was just a failed attempt. I'm trying to make this as simple as possible, without classes, because those aren't allowed for this assignment.

https://github.com/Night-28/Treble-Quest.git ^ The "main.py" file won't run without all of the pngs so if you do want to run it, you might have to download all of them, sorry!

import pygame as py
from tile_map import map

# Pygame setup
py.init()

clock = py.time.Clock()

# COLOURS
bg_colour = (110, 121, 228)
sky_colour = (190, 220, 255)
start_colour = (225, 225, 225)

screen_width = 1280
screen_height = 720
screen = py.display.set_mode((screen_width, screen_height))

p_sprite = py.image.load("plant_drone.png")
p_rect = p_sprite.get_rect()
p_rect.centery = screen_height - 32

grass_block = py.image.load("grass_block.png")
dirt_block = py.image.load("dirt_block.png")

# def collisions(rect, tiles):
#   collider_list = []
#   for tile in tiles:
#       if rect.colliderect(tile):
#           collider_list.append(tile)
#   return collider_list
#
# def move(rect, movement, tiles):
#   collision_types = {"top": False, "bottom": False, "right": False, "left": False}
#
#   rect.x += movement[0]
#   collider_list = collisions(rect, tile)
#   for tile in collider_list:
#       if movement[0] > 0:
#           rect.right = tile.left
#           collision_types["right"] = True
#       elif movement[0] < 0:
#           rect.left = tile.right
#           collision_types["left"] = True
#
#   rect.y += movement[1]
#   collider_list = collisions(rect, tile)
#   for tile in collider_list:
#       if movement[1] > 0:
#           rect.bottom = tile.top
#           collision_types["bottom"] = True
#       elif movement[1] < 0:
#           rect.top = tile.bottom
#           collision_types["top"] = True
#
#   return rect, collision_types

# MAIN MENU
def menu():
    py.display.set_caption("Game Menu")
    while True:
        start()
        py.display.update()
        clock.tick(60)

# START OPTION (BLINKING TEXT)
def start():
    start_font = py.font.Font('Halogen.otf', 50)

    bg = py.image.load("GM Treble Quest V2.png")
    bg_rect = py.Rect((0, 0), bg.get_size())
    screen.blit(bg, bg_rect)

    n = 0
    while True:
        start = start_font.render(("Press Enter To Play"), True, start_colour)

        if n % 2 == 0:
            screen.blit(start, (450, 625))
            clock.tick(50000)

        else:
            screen.blit(bg, bg_rect)
        n += 0.5

        py.display.update()
        clock.tick(3)

        for event in py.event.get():
            if event.type == py.QUIT:
                exit()

            elif event.type == py.KEYDOWN:
                if event.key == py.K_RETURN:
                    play()

# GAME
def play():
    py.display.set_caption("Treble Quest")
    player_y, player_x = p_rect.bottom, 32

    velocity_x, velocity_y = 5, 0
    ground = 480
    gravity_factor = 0.35
    acl_factor = -12

    while True:
        clock.tick(100)
        vertical_acl = gravity_factor
        screen.fill(sky_colour)
        screen.blit(p_sprite, p_rect)

# TILE MAP
        tile_collisions = []
        y = 0
        for row in map:
            x = 0
            for tile in row:
                if tile == 1:
                    screen.blit(dirt_block, (x * 32, y * 32))
                if tile == 2:
                    screen.blit(grass_block, (x * 32, y * 32))
                if tile != 0:
                    tile_collisions.append(py.Rect(x * 32, y * 32, 32, 32))
                x += 1
            y += 1

        screen.blit(p_sprite, p_rect)

#   player_movement = [0, 0]
#       if moving_right == True:
#           player_movement[0] += 2
#       if moving_left == True:
#           player_movement[0] -= 2
#       player_movement[1] += player

# MOVEMENT
        for event in py.event.get():
            if event.type == py.QUIT:
                exit()

            if event.type == py.KEYDOWN:
                if velocity_y == 0 and event.key == py.K_w:
                    vertical_acl = acl_factor

        velocity_y += vertical_acl
        player_y += velocity_y

        if player_y > ground:
            player_y = ground
            velocity_y = 0
            vertical_acl = 0

        p_rect.bottom = round(player_y)

        keys = py.key.get_pressed()

        player_x += (keys[py.K_d] - keys[py.K_a]) * velocity_x
        p_rect.centerx = player_x

        py.display.update()

menu()

Upvotes: 1

Views: 378

Answers (1)

Kingsley
Kingsley

Reputation: 14924

Your code is almost there.

A simple way to do collisions is to iterate through the bunch of rectangles created when you process the map tiles. Check each rectangle to see if it would intersect the player's position.

When your player needs to move, calculate where the move intends to go (don't actually move the player yet). Then check if the target location will overlap with any of the blocker-tiles. If there is no overlap, then move the player. Otherwise the player stops.

In the example code below, I've implemented a illustrative version of this. While this method is simple, and works (mostly), it suffers from a few issues:

  • If the velocity is high enough, the player's "next position" may jump right through a blocker tile. This is because the code only tests the destination, not the in-between locations.
  • Also when a potential collision happens, the player just stops. But at high speeds, the player rectangle may stop a few pixels before the collision tile, rather than against the tile, which would be more what a player would expect.

Really you should test to cover these failures. The second issue is easy to fix by calculating how far the player can move in the desired direction, and only move this lesser amount.

screenshot

Note that I didn't really understand your movement algorithm. There seems to be a velocity, but no stopping. Given I was just illustrating collisions, I hacked it into some kind of working state that suited what I needed to show.

import pygame as py
#from tile_map import map

map = [ [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0 ],
        [ 0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0 ],
        [ 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 ] ]

# Pygame setup
py.init()

clock = py.time.Clock()

# COLOURS
bg_colour = (110, 121, 228)
sky_colour = (190, 220, 255)
start_colour = (225, 225, 225)

screen_width = 1280
screen_height = 720
screen = py.display.set_mode((screen_width, screen_height))


def fakeBitmap( width, height, back_colour, text=None, text_colour=(100,100,100) ):
    """ Quick function to replace missing bitmaps """
    bitmap = py.Surface( ( width, height ) )
    bitmap.fill( back_colour )
    if ( text != None and len( text.strip() ) > 0 ):
        font = py.font.SysFont( None, 10 )
        text_bitmap = font.render( text, True, text_colour, back_colour )
        bitmap.blit( text_bitmap, text_bitmap.get_rect(center = bitmap.get_rect().center ) )
    return bitmap


#p_sprite = py.image.load("plant_drone.png")
p_sprite = fakeBitmap( 32, 32, ( 128,128,0), "PLAYER" )
p_rect = p_sprite.get_rect()
p_rect.centery = screen_height - 32

#grass_block = py.image.load("grass_block.png")
#dirt_block = py.image.load("dirt_block.png")
grass_block = fakeBitmap( 32, 32, ( 0,200,0 ), "grass_block.png" )
dirt_block  = fakeBitmap( 32, 32, ( 102,49, 0 ), "dirt_block.png")


# MAIN MENU
def menu():
    py.display.set_caption("Game Menu")
    while True:
        start()
        py.display.update()
        clock.tick(60)


# START OPTION (BLINKING TEXT)
def start():
    #start_font = py.font.Font('Halogen.otf', 50)
    start_font = py.font.SysFont( None, 16 )

    #bg = py.image.load("GM Treble Quest V2.png")
    bg = fakeBitmap( 1280, 1024, ( 0,0,128 ), "GM Treble Quest V2.png" )
    bg_rect = py.Rect((0, 0), bg.get_size())
    screen.blit(bg, bg_rect)

    n = 0
    while True:
        start = start_font.render(("Press Enter To Play"), True, start_colour)

        if n % 2 == 0:
            screen.blit(start, (450, 625))
            clock.tick(50000)

        else:
            screen.blit(bg, bg_rect)
        n += 0.5

        py.display.update()
        clock.tick(3)

        for event in py.event.get():
            if event.type == py.QUIT:
                exit()

            elif event.type == py.KEYDOWN:
                if event.key == py.K_RETURN:
                    play()



def playerCanMoveTo( player_next, blockers ):
    """ Is a player allowed to move to player_next, or will that mean
        colliding with any of the rectangles defined in blockers """

    can_move = True

    # Simple check, is the destination blocked
    for block_rect in blockers:
        print( "is Player [%d,%d %d %d] inside Block [%d,%d %d %d]" % ( player_next.x, player_next.y, player_next.width, player_next.height, block_rect.x, block_rect.y, block_rect.width, block_rect.height ) )
        if ( block_rect.colliderect( player_next ) ):
            can_move = False
            break  # don't need to check further

    return can_move



# GAME
def play():
    py.display.set_caption("Treble Quest")
    player_y, player_x = p_rect.bottom, 32

    velocity_x, velocity_y = 5, 0
    ground = 480
    gravity_factor = 0.35
    acl_factor = -12

    # Only need to do this once, moved away from main loop
    tile_collisions = []
    y = 0
    for row in map:
        x = 0
        for tile in row:
            if tile != 0:
                tile_collisions.append(py.Rect(x * 32, y * 32, 32, 32))
            x += 1
        y += 1


    while True:
        clock.tick(100)
        vertical_acl = gravity_factor
        screen.fill(sky_colour)
        screen.blit(p_sprite, p_rect)

        # TILE MAP
        y = 0
        for row in map:
            x = 0
            for tile in row:
                if tile == 1:
                    screen.blit(dirt_block, (x * 32, y * 32))
                if tile == 2:
                    screen.blit(grass_block, (x * 32, y * 32))

                x += 1
            y += 1

        screen.blit(p_sprite, p_rect)


        # MOVEMENT
        for event in py.event.get():
            if event.type == py.QUIT:
                exit()

            elif event.type == py.KEYDOWN:
                if velocity_y == 0 and event.key == py.K_w:
                    vertical_acl = acl_factor

        velocity_y += vertical_acl
        player_y += velocity_y

        if player_y > ground:
            player_y = ground
            velocity_y = 0
            vertical_acl = 0

        p_rect.bottom = round(player_y)

        keys = py.key.get_pressed()

        # Accelerate left/right [A] <-> [D]
        if ( keys[py.K_a] ):
            velocity_x -= 1
            velocity_x = max( -5, velocity_x )
        elif ( keys[py.K_d] ):
            velocity_x += 1
            velocity_x = min( 5, velocity_x )


        # Is the player allowed to move?
        target_rect = p_rect.copy()
        target_rect.centerx += velocity_x
        # Check where the player would move to, is allowed
        if ( playerCanMoveTo( target_rect, tile_collisions ) ):
            player_x += velocity_x
            p_rect.centerx = player_x
            print( "moving %d" % ( velocity_x ) )
        else:
            velocity_x = 0
            print( "blocked" )

        py.display.update()

menu()

Upvotes: 1

Related Questions