Add collision detection to a plattformer in pygame

2019-04-13 18:46发布

问题:

I'm working on a small platformer game in which you place blocks to make a level, then play it.

I got gravity, jumping, and left and right movement.. but I am not sure how to make the player collide with walls when moving left or right.

The way I want it to work is like this-

if key[K_LEFT]:

if not block to the left:

move to the left

How would I go about doing this (relative to this source):

import pygame,random
from pygame.locals import *
import itertools
pygame.init()
screen=pygame.display.set_mode((640,480))
class Block(object):
    sprite = pygame.image.load("texture\\dirt.png").convert_alpha()
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)

class Player(object):
    sprite = pygame.image.load("texture\\playr.png").convert()
    sprite.set_colorkey((0,255,0))
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)

blocklist = []
player = []
colliding = False

while True:
    screen.fill((25,30,90))
    mse = pygame.mouse.get_pos()
    key=pygame.key.get_pressed()

    if key[K_LEFT]:
        p.rect.left-=1
    if key[K_RIGHT]:
        p.rect.left+=1
    if key[K_UP]:
        p.rect.top-=10

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

        if key[K_LSHIFT]:
            if event.type==MOUSEMOTION:
                if not any(block.rect.collidepoint(mse) for block in blocklist):
                    x=(int(mse[0]) / 32)*32
                    y=(int(mse[1]) / 32)*32
                    blocklist.append(Block(x+16,y+16))
        else:
            if event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:
                    to_remove = [b for b in blocklist if b.rect.collidepoint(mse)]
                    for b in to_remove:
                        blocklist.remove(b)

                    if not to_remove:
                        x=(int(mse[0]) / 32)*32
                        y=(int(mse[1]) / 32)*32
                        blocklist.append(Block(x+16,y+16))

                elif event.button == 3:
                    x=(int(mse[0]) / 32)*32
                    y=(int(mse[1]) / 32)*32
                    player=[]
                    player.append(Player(x+16,y+16))

    for b in blocklist:
        screen.blit(b.sprite, b.rect)
    for p in player:
        if any(p.rect.colliderect(block) for block in blocklist):
            #collide
            pass
        else:
            p.rect.top += 1
        screen.blit(p.sprite, p.rect)
    pygame.display.flip()

回答1:

A common approch is to seperate the horizontal and vertical collision handling into two seperate steps.

If you do this and also track the velocity of your player, it's easy to know on which side a collision happened.

First of all, let's give the player some attributes to keep track of his velocity:

class Player(object):
    ...
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)
        # indicates that we are standing on the ground
        # and thus are "allowed" to jump
        self.on_ground = True 
        self.xvel = 0
        self.yvel = 0
        self.jump_speed = 10
        self.move_speed = 8

Now we need a method to actually check for a collision. As already said, to make things easy, we use our xvel and yvel to know if we collided with our left or right side etc. This goes into the Player class:

def collide(self, xvel, yvel, blocks):
    # all blocks that we collide with
    for block in [blocks[i] for i in self.rect.collidelistall(blocks)]:

        # if xvel is > 0, we know our right side bumped 
        # into the left side of a block etc.
        if xvel > 0: self.rect.right = block.rect.left
        if xvel < 0: self.rect.left = block.rect.right

        # if yvel > 0, we are falling, so if a collision happpens 
        # we know we hit the ground (remember, we seperated checking for
        # horizontal and vertical collision, so if yvel != 0, xvel is 0)
        if yvel > 0:
            self.rect.bottom = block.rect.top
            self.on_ground = True
            self.yvel = 0
        # if yvel < 0 and a collision occurs, we bumped our head
        # on a block above us
        if yvel < 0: self.rect.top = block.rect.bottom

Next, we move our movement handling to the Player class. So let's create on object that keeps track of the input. Here, I use a namedtuple, because why not.

from collections import namedtuple
...
max_gravity = 100
Move = namedtuple('Move', ['up', 'left', 'right'])
while True:
    screen.fill((25,30,90))
    mse = pygame.mouse.get_pos()
    key = pygame.key.get_pressed()

    for event in pygame.event.get():
       ...

    move = Move(key[K_UP], key[K_LEFT], key[K_RIGHT])
    for p in player:
        p.update(move, blocklist)
        screen.blit(p.sprite, p.rect)

We pass the blocklist to the update method of the Player so we can check for collision. Using the move object, we now know where the player should move, so let's implement Player.update:

def update(self, move, blocks):

    # check if we can jump 
    if move.up and self.on_ground: 
        self.yvel -= self.jump_speed

    # simple left/right movement
    if move.left: self.xvel = -self.move_speed
    if move.right: self.xvel = self.move_speed

    # if in the air, fall down
    if not self.on_ground:
        self.yvel += 0.3
        # but not too fast
        if self.yvel > max_gravity: self.yvel = max_gravity

    # if no left/right movement, x speed is 0, of course
    if not (move.left or move.right):
        self.xvel = 0

    # move horizontal, and check for horizontal collisions
    self.rect.left += self.xvel
    self.collide(self.xvel, 0, blocks)

    # move vertically, and check for vertical collisions
    self.rect.top += self.yvel
    self.on_ground = False;
    self.collide(0, self.yvel, blocks)

The only thing left is to use a Clock to limit the framerate to let the game run at a constant speed. That's it.

Here's the complete code:

import pygame,random
from pygame.locals import *
from collections import namedtuple

pygame.init()
clock=pygame.time.Clock()
screen=pygame.display.set_mode((640,480))

max_gravity = 100

class Block(object):
    sprite = pygame.image.load("dirt.png").convert_alpha()
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)

class Player(object):
    sprite = pygame.image.load("dirt.png").convert()
    sprite.set_colorkey((0,255,0))
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)
        # indicates that we are standing on the ground
        # and thus are "allowed" to jump
        self.on_ground = True
        self.xvel = 0
        self.yvel = 0
        self.jump_speed = 10
        self.move_speed = 8

    def update(self, move, blocks):

        # check if we can jump 
        if move.up and self.on_ground: 
            self.yvel -= self.jump_speed

        # simple left/right movement
        if move.left: self.xvel = -self.move_speed
        if move.right: self.xvel = self.move_speed

        # if in the air, fall down
        if not self.on_ground:
            self.yvel += 0.3
            # but not too fast
            if self.yvel > max_gravity: self.yvel = max_gravity

        # if no left/right movement, x speed is 0, of course
        if not (move.left or move.right):
            self.xvel = 0

        # move horizontal, and check for horizontal collisions
        self.rect.left += self.xvel
        self.collide(self.xvel, 0, blocks)

        # move vertically, and check for vertical collisions
        self.rect.top += self.yvel
        self.on_ground = False;
        self.collide(0, self.yvel, blocks)

    def collide(self, xvel, yvel, blocks):
        # all blocks that we collide with
        for block in [blocks[i] for i in self.rect.collidelistall(blocks)]:

            # if xvel is > 0, we know our right side bumped 
            # into the left side of a block etc.
            if xvel > 0: self.rect.right = block.rect.left
            if xvel < 0: self.rect.left = block.rect.right

            # if yvel > 0, we are falling, so if a collision happpens 
            # we know we hit the ground (remember, we seperated checking for
            # horizontal and vertical collision, so if yvel != 0, xvel is 0)
            if yvel > 0:
                self.rect.bottom = block.rect.top
                self.on_ground = True
                self.yvel = 0
            # if yvel < 0 and a collision occurs, we bumped our head
            # on a block above us
            if yvel < 0: self.rect.top = block.rect.bottom

blocklist = []
player = []
colliding = False
Move = namedtuple('Move', ['up', 'left', 'right'])
while True:
    screen.fill((25,30,90))
    mse = pygame.mouse.get_pos()
    key = pygame.key.get_pressed()

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

        if key[K_LSHIFT]:
            if event.type==MOUSEMOTION:
                if not any(block.rect.collidepoint(mse) for block in blocklist):
                    x=(int(mse[0]) / 32)*32
                    y=(int(mse[1]) / 32)*32
                    blocklist.append(Block(x+16,y+16))
        else:
            if event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:
                    to_remove = [b for b in blocklist if b.rect.collidepoint(mse)]
                    for b in to_remove:
                        blocklist.remove(b)

                    if not to_remove:
                        x=(int(mse[0]) / 32)*32
                        y=(int(mse[1]) / 32)*32
                        blocklist.append(Block(x+16,y+16))

                elif event.button == 3:
                    x=(int(mse[0]) / 32)*32
                    y=(int(mse[1]) / 32)*32
                    player=[]
                    player.append(Player(x+16,y+16))

    move = Move(key[K_UP], key[K_LEFT], key[K_RIGHT])

    for b in blocklist:
        screen.blit(b.sprite, b.rect)
    for p in player:
        p.update(move, blocklist)
        screen.blit(p.sprite, p.rect)
    clock.tick(60)
    pygame.display.flip()

Note that I changed the image names so I just need a single image file for testing this. Also, I don't know why you keep the player in a list, but here's a nice animation of our game in action:



回答2:

Since you're using pygame, you can use pygame's rect.colliderect() to see if the player's sprite is colliding with a block. Then make a function that returns the side a certain rect is on relative to the other rect:

def rect_side(rect1, rect2): # Returns side of rect1 relative to rect2.
    if rect1.x > rect2.x:
        return "right"
    else:
        return "left"
    # If rect1.x == rect2.x the function will return "left".

Than if rect_side() returns "right" you restrict movement to the left and vice-versa.

PS: If you want the same but including vertical movement you can compare rect1.y to rect2.y and deal with outputs "up" and "down". You can make a tuple that represents horizontal and vertical directions.