3

I'm having issues getting this collision to work 100%. If I only press one key at a time, the collision seems to work fine, but if I press a key and continue pressing, while colliding, then press another key, the collision seems to account for both keys at the same time. From researching, it seems like I need to do separate axis calculations, but I'm not sure exactly how to do that, using the algorithm I have for collision. I want this to be procedural style, if possible. If anyone could amend my code, with a working procedural solution, I'd greatly appreciate it. Thanks.

import pygame as pg
import sys
from math import fabs

pg.init()

width = 600
height = 600

gameDisplay = pg.display.set_mode((width, height))
pg.display.set_caption('Block')


white = (255, 255, 255)
red = (255, 0, 0)

clock = pg.time.Clock()
closed = False
FPS = 60
Player_Speed = 200
x, y = 270, 0
vx = 0
vy = 0
collision = False

def Collision(hero, enemy):
    global vx, vy, x, y, collision
    deltay = fabs(block.centery - ENEMY.centery)
    deltax = fabs(block.centerx - ENEMY.centerx)
    if deltay < ENEMY.height and deltax < ENEMY.width:
        collision = True
        if vx > 0:
            vx = 0
            x = ENEMY[0] - block[2] 
        if vx < 0:
            vx = 0
            x = ENEMY[0] + 30
        if vy > 0:
            vy = 0
            y = ENEMY[1] - block[3] 
        if vy < 0:
            vy = 0
            y = ENEMY[1] + 30
    else:
        collision = False


def xy_Text(x, y):
    font = pg.font.SysFont("Courier", 16, True)
    text = font.render("X: " + str(round(x)), True, (0,150,0))
    text1 = font.render("Y: " + str(round(y)), True, (0,150,0))
    gameDisplay.blit(text, (0,0))
    gameDisplay.blit(text1, (0,14))

while not closed:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            closed = True

        dt = clock.tick(FPS)/1000
        vx, vy = 0, 0

        keys = pg.key.get_pressed()

        if keys[pg.K_ESCAPE]:
            closed = True
        if keys[pg.K_LEFT] or keys[pg.K_a]:
            vx = -Player_Speed
        if keys[pg.K_RIGHT] or keys[pg.K_d]:
            vx = Player_Speed
        if keys[pg.K_UP] or keys[pg.K_w]:
            vy = -Player_Speed
        if keys[pg.K_DOWN] or keys[pg.K_s]:
            vy = Player_Speed
        if vx != 0 and vy != 0:
            vx *= 0.7071
            vy *= 0.7071


    gameDisplay.fill(white)
    ENEMY = pg.draw.rect(gameDisplay, red, (270, 270, 30, 30))
    block = pg.draw.rect(gameDisplay, (0, 150, 0), (x, y, 30, 30))
    xy_Text(x, y)
    x += vx * dt
    y += vy * dt
    Collision(block, ENEMY)
    pg.display.update()
    clock.tick(FPS)

pg.quit()
sys.exit()
Tophiero
  • 33
  • 4
  • Collision handling is hard. I would advise you to - instead of trying to reinvent the wheel - use an existing library like Box2d or Pymunk – Omni Dec 14 '17 at 21:37
  • I know I can use an existing library, even pygame has collision handling. I'm "reinventing the wheel" for educational purposes. I want to understand how everything works. Thanks for the advice, though. – Tophiero Dec 14 '17 at 21:42
  • That's a great motive. For instance, in a force-based approach you could handle collisions by creating auxiliary forces to move colliding objects apart. The number of pressed keys would propably be irrelevant for such a model. The fact that this plays a major role in your question on the other hand implies - from my point of view - that you need more research into how such simulations work. – Omni Dec 14 '17 at 22:00
  • [Program Arcade Games With Python And Pygame - Lab 16: Pygame Platformer Examples](http://programarcadegames.com/index.php?&chapter=example_code_platformer) In one example you can see: 1. move only vertically, 2. check all collisions, 3. move only horizontally, 4. check all collisions again. – furas Dec 15 '17 at 00:58
  • I assume you want to handle collisions with walls, but I'm not sure since you called the one rect `ENEMY`. – skrx Dec 15 '17 at 06:23

2 Answers2

1

After many changes collision works

I moved

x += vx * dt
y += vy * dt

into Collision.

I changed organization in code and I always use Rect() to keep position and size of player and enemy

I also add second enemy to test how to check collision with many elements.

import pygame as pg
import sys
from math import fabs

# - functions --- (lower_case_names)

def check_collision(player, enemy1, enemy2):
    global player_vx, player_vy

    # --- X ---

    player.x += player_vx * dt

    # enemy 1

    deltay = fabs(player.centery - enemy1.centery)
    deltax = fabs(player.centerx - enemy1.centerx)

    if deltay < enemy1.height and deltax < enemy1.width:
        if player_vx > 0:
            player_vx = 0
            player.x = enemy1.x - player.w
        elif player_vx < 0:
            player_vx = 0
            player.x = enemy1.x + player.w

    # enemy 2

    deltay = fabs(player.centery - enemy2.centery)
    deltax = fabs(player.centerx - enemy2.centerx)

    if deltay < enemy2.height and deltax < enemy2.width:
        if player_vx > 0:
            player_vx = 0
            player.x = enemy2.x - player.w
        elif player_vx < 0:
            player_vx = 0
            player.x = enemy2.x + player.w

    # --- Y ---

    player.y += player_vy * dt

    # enemy 1

    deltay = fabs(player.centery - enemy1.centery)
    deltax = fabs(player.centerx - enemy1.centerx)

    if deltay < enemy1.height and deltax < enemy1.width:
        if player_vy > 0:
            player_vy = 0
            player.y = enemy1.y - player.h
        elif player_vy < 0:
            player_vy = 0
            player.y = enemy1.y + player.w

    # enemy 2

    deltay = fabs(player.centery - enemy2.centery)
    deltax = fabs(player.centerx - enemy2.centerx)

    if deltay < enemy2.height and deltax < enemy2.width:
        if player_vy > 0:
            player_vy = 0
            player.y = enemy2.y - player.h
        elif player_vy < 0:
            player_vy = 0
            player.y = enemy2.y + player.w


def xy_text(screen, x, y):
    font = pg.font.SysFont("Courier", 16, True)

    text = font.render("X: " + str(round(x)), True, (0,150,0))
    screen.blit(text, (0,0))

    text = font.render("Y: " + str(round(y)), True, (0,150,0))
    screen.blit(text, (0,14))

# --- constants --- (UPPER_CASE_NAMES)

WIDTH = 600
HEIGHT = 600

WHITE = (255, 255, 255)
RED = (255, 0, 0)

FPS = 60

# --- main --- (lower_case_names)

player_speed = 200
player_vx = 0
player_vy = 0

player = pg.Rect(270, 0, 30, 30)
enemy1 = pg.Rect(270, 270, 30, 30)
enemy2 = pg.Rect(240, 300, 30, 30)

# - init -
pg.init()

game_display = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption('Block')

# - mainloop -

clock = pg.time.Clock()

closed = False
while not closed:

    dt = clock.tick(FPS)/1000

    # - events -

    for event in pg.event.get():
        if event.type == pg.QUIT:
            closed = True

    keys = pg.key.get_pressed()

    player_vx = 0
    player_vy = 0

    if keys[pg.K_ESCAPE]:
        closed = True
    if keys[pg.K_LEFT] or keys[pg.K_a]:
        player_vx = -player_speed
    if keys[pg.K_RIGHT] or keys[pg.K_d]:
        player_vx = player_speed
    if keys[pg.K_UP] or keys[pg.K_w]:
        player_vy = -player_speed
    if keys[pg.K_DOWN] or keys[pg.K_s]:
        player_vy = player_speed
    if player_vx != 0 and player_vy != 0:
        player_vx *= 0.7071
        player_vy *= 0.7071

    # - updates -

    check_collision(player, enemy1, enemy2)

    # - draws -

    game_display.fill(WHITE)
    pg.draw.rect(game_display, RED, enemy1)
    pg.draw.rect(game_display, RED, enemy2)
    pg.draw.rect(game_display, (0, 150, 0), player)
    xy_text(game_display, player.x, player.y)
    pg.display.update()
    clock.tick(FPS)

# - end -

pg.quit()
sys.exit()
furas
  • 134,197
  • 12
  • 106
  • 148
  • Thank you for the amendments and advice. I liked the way you separated everything with comments. I'll start doing that. The only problem with this code, that I found, was that is slows movement and isn't as smooth. The collision does work, though. – Tophiero Dec 21 '17 at 04:40
  • Is there any way to get this code to work with the smooth movement? – Tophiero Dec 21 '17 at 05:05
  • problem is two `tips(FPS)` - very near one each other - so `dt` gives time between both `tips(FPS)` but not time used to draw elements. Now code works faster. – furas Dec 21 '17 at 21:21
  • if you will have much more objects then checking collision can be problem and there is method "Space partitioning" ("Spatial Partition") so you check collison only with neerest objects - [youtube: 2D grid spatial partitioning basics (speed up collision broad-phase)](https://www.youtube.com/watch?v=7HY_SqqaoL4), or [Game Programming Patterns: Spatial Partition](http://gameprogrammingpatterns.com/spatial-partition.html) – furas Dec 21 '17 at 21:25
1

If you want to handle collisions with walls, move along the x or y axis first, set the player back if s/he collided and then do the same for the other axis. If you use pygame.Rects, you can use their left, right, top and bottom attributes to easily set the player back to the corresponding side of the block.

So check if the player collides with a block and if s/he moved to the right (vx > 0), then set hero.right to block.left and do the same for the other directions. You also have to return the new x and y coords if the player rect was updated.

I suggest to put the blocks (rects) into a blocks list which you can pass to the wall collision functions and then just use a for loop.

from math import fabs
import pygame as pg


pg.init()

width = 600
height = 600

gameDisplay = pg.display.set_mode((width, height))

white = (255, 255, 255)
red = (255, 0, 0)

clock = pg.time.Clock()
closed = False
FPS = 60
Player_Speed = 200
x, y = 270, 0
vx = 0
vy = 0
# Define the font in the global scope.
FONT = pg.font.SysFont("Courier", 16, True)


# Better don't use global variables.
def handle_horizontal_collisions(hero, blocks, x, vx):
    """Sets the player back to the left or right side."""
    for block in blocks:
        if hero.colliderect(block):
            if vx > 0:
                hero.right = block.left
            elif vx < 0:
                hero.left = block.right
            return hero.x  # Need to update the actual `x` position.
    return x


def handle_vertical_collisions(hero, blocks, y, vy):
    """Sets the player back to the top or bottom side."""
    for block in blocks:
        if hero.colliderect(block):
            if vy > 0:
                hero.bottom = block.top
            elif vy < 0:
                hero.top = block.bottom
            return hero.y  # Need to update the actual `y` position.
    return y


def xy_Text(x, y):
    text = FONT.render("X: " + str(round(x)), True, (0,150,0))
    text1 = FONT.render("Y: " + str(round(y)), True, (0,150,0))
    gameDisplay.blit(text, (0,0))
    gameDisplay.blit(text1, (0,14))


# Use pygame.Rects for the player and blocks.
player = pg.Rect(x, y, 30, 30)
# Put the blocks into a list.
blocks = []
for rect in (pg.Rect(200, 200, 60, 30), pg.Rect(230, 230, 60, 30)):
    blocks.append(rect)

dt = 0

while not closed:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            closed = True
        # clock.tick should not be called in the event loop.

    vx, vy = 0, 0
    # key.get_pressed should also not be in the event loop.
    keys = pg.key.get_pressed()

    if keys[pg.K_ESCAPE]:
        closed = True
    if keys[pg.K_LEFT] or keys[pg.K_a]:
        vx = -Player_Speed
    if keys[pg.K_RIGHT] or keys[pg.K_d]:
        vx = Player_Speed
    if keys[pg.K_UP] or keys[pg.K_w]:
        vy = -Player_Speed
    if keys[pg.K_DOWN] or keys[pg.K_s]:
        vy = Player_Speed
    if vx != 0 and vy != 0:
        vx *= 0.7071
        vy *= 0.7071

    # Game logic.
    x += vx * dt
    player.x = x
    x = handle_horizontal_collisions(player, blocks, x, vx)

    y += vy * dt
    player.y = y
    y = handle_vertical_collisions(player, blocks, y, vy)

    # Rendering.
    gameDisplay.fill(white)
    for block in blocks:
        pg.draw.rect(gameDisplay, red, block)
    pg.draw.rect(gameDisplay, (0, 150, 0), player)

    xy_Text(x, y)
    pg.display.update()
    dt = clock.tick(FPS)/1000

pg.quit()
skrx
  • 19,980
  • 5
  • 34
  • 48
  • This was the best answer. It does exactly what I ask for, and some, without sacrificing smooth movement. Like the guy above, you, too, separate different sections of code with comments. I will start doing this as well; it makes code look much neater and organized. Thank you for your time, efforts, and advice. – Tophiero Dec 21 '17 at 04:47