How do I "Push" an object (crate) with another object (player) in Pygame?

Question:

I’m trying to make a pushable crate using Pygame.

I’m able to push the crate in all directions (up, down, left and right) which works fine. I can also push the crate up or down and move left and right at the same time. The issue is when I push the crate left or right and move up or down at the same time, the crate will ping to the top or bottom of my Player object.

I’ve created a GIF to show you what I mean:

Block pushing

I’m using a method/function in my Player class to handle the pushing:

def push(self, other):
    if self.this.colliderect(other):
        self.speed = 1
        if self.dx < 0:
            other.x = self.this.left - other.this.width
        elif self.dx > 0:
            other.x = self.this.right
        elif self.dy < 0:
            other.y = self.this.top - other.this.height
        elif self.dy > 0:
            other.y = self.this.bottom
    else:
        self.speed = 3

self.dx and self.dy are variables assigned in the update method, as follows:

def update(self, x, y):
    self.dx = x
    self.dy = y
    self.rect.x += x
    self.rect.y += y

and here is how I deal with movement within the game loop:

    # Deal with movement related key presses
    if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_w:
            up = True
        if event.key == pygame.K_s:
            down = True
        if event.key == pygame.K_a:
            left = True
        if event.key == pygame.K_d:
            right = True

    if event.type == pygame.KEYUP:
        if event.key == pygame.K_w:
            dy = 0
            up = False
        if event.key == pygame.K_s:
            dy = 0
            down = False
        if event.key == pygame.K_a:
            dx = 0
            left = False
        if event.key == pygame.K_d:
            dx = 0
            right = False

# Apply velocity to Player
if up:
    dy = -player.speed
if down: 
    dy = player.speed
if left:
    dx = -player.speed
if right:
    dx = player.speed

# Move the Player and check for wall/door collisions
if dx != 0:
    player.update(dx, 0)
    player.collision(level.wall_tiles)
    player.collision(level.door_tiles)
if dy != 0:
    player.update(0, dy)
    player.collision(level.wall_tiles)
    player.collision(level.door_tiles)

Logically, looking at my push() method, I can see why the crate would ping up while I’m pushing it left or right, because I am setting the crates y position respective of the player’s direction. However, I don’t understand why the crate doesn’t ping when I’m pushing it up or down and moving left or right…

I ultimately just want the player to be able to push the crate without it pinging about.

I’ve attempted multiple solutions, like checking if both self.dx and self.dy are active and dealing with the behaviour of the crate within the game loop, rather than a method attached to the Player object.

Nothing I try seems to get the pushing action to behave how I want it to.

If it helps to check out my entire game code so far, it’s here

Do you have any advise on how I can handle this interaction correctly?

Asked By: SatsumaSegment

||

Answers:

This looks caused by the global dx and dy variables (in main) getting get out of sync with the attributes player.dx and player.dy.

The reason is in this code, in main:

if dx != 0:
    player.update(dx, 0)
    player.collision(level.wall_tiles)
    player.collision(level.door_tiles)
if dy != 0:
    player.update(0, dy)
    player.collision(level.wall_tiles)
    player.collision(level.door_tiles)

paired with this code in objects:

def update(self, x, y):
    self.dx = x
    self.dy = y
    self.rect.x += x
    self.rect.y += y

If you look at either of these independently, you’d think they were doing mostly some reasonable stuff. The first block of code simplifies the wall and door collision code by handling the two axes of movement separately. That seems like it should be fine. The trouble is that player.update saves both of the values it gets passed, even when one is a hard-coded 0!

So, if you’re moving diagonally, only vertical pushes are ever possible, because the second call to player.update zeros out player.dx even though the global dx variable in main is non-zero.

I’ll leave it to you to figure out how best to fix this (you could change the wall-collision code, or the update logic). One general idea I will suggest is to try to get your code so that there’s a so called "single source of truth" about any given piece of data. In your current code, there are a lot of values that seem to have very similar, but maybe not quite identical data in them (e.g. dx and player.dx, or player.rect and player.this), and that can very easily lead to subtle bugs like this one. Sometimes a bit of redundancy is useful, but too much is definitely bad.

Answered By: Blckknght