Search code examples
pythonpygamegame-physicscollision

Multiple collisions not testing


I have a box2d pygame platforming example. You can move the player around and spawn platforms. I cannot perform the jump action on the player unless the player is colliding with all of the platforms in the level. This is not intended behavior, and what is desired is to be able to jump when you are touching one platform and not all the others. I am using a for loop to loop through all the collisions for the player, and setting can_jump as needed.

main.py

import pygame
from draw import Draw
from Box2D import (b2World,b2Vec2)

from box import Box

from player import Player

def Run():
    PPM = 20
    TARGET_FPS = 60
    TIME_STEP = 1.0 / TARGET_FPS

    SCREEN_WIDTH,SCREEN_HEIGHT = 640,480
    CAPTION = ""

    BGCOLOR = ((255,255,255))

    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption(CAPTION)
    clock = pygame.time.Clock()

    world = b2World(gravity=(0, 30), doSleep=True)

    closed = False

    global player, hold_left, hold_right, can_jump, speed, deceleration
    player = Player(world,200,200,PPM)
    speed = 20
    deceleration = 0.95
    hold_left = False
    hold_right = False
    can_jump = False

    while not closed:

        def left():
            player.body.ApplyForce(b2Vec2(-1*speed*speed,0),point=player.body.worldCenter,wake=True)
        def right():
            player.body.ApplyForce(b2Vec2(speed*speed,0),point=player.body.worldCenter,wake=True)
        if hold_left:
            left()
        elif hold_right:
            right()

        if len(player.body.contacts) == 0:
            can_jump = False
        else:
            for contact in player.body.contacts:
                contact = contact.contact
                print(contact)
                if contact.manifold.localPoint == b2Vec2(0,1):
                    can_jump=True
                elif contact.manifold.localPoint != b2Vec2(0,1):
                    can_jump=False

        for event in pygame.event.get():
            if event.type == pygame.QUIT: 
                closed = True
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_d:
                    hold_right = True
                elif event.key == pygame.K_a:
                    hold_left = True
                elif event.key == pygame.K_SPACE:
                    if can_jump:
                        player.body.linearVelocity.y = 0
                        player.body.ApplyLinearImpulse(b2Vec2(0,-150),point=player.body.worldCenter,wake=True)
            if event.type == pygame.KEYUP:
                if event.key == pygame.K_d:
                    hold_right = False
                elif event.key == pygame.K_a:
                    hold_left = False

        click = pygame.mouse.get_pressed()  
        if click[0] == 1:
            x,y = pygame.mouse.get_pos()
            Box(world, x, y, PPM)

        player.body.linearVelocity.x *= deceleration

        screen.fill(BGCOLOR)

        Draw(screen,world.bodies,PPM)

        world.Step(TIME_STEP, 10, 10)
        pygame.display.flip()
        clock.tick(TARGET_FPS)

    pygame.quit()

if __name__ == '__main__':
    Run()

player.py

from Box2D import (b2FixtureDef, b2PolygonShape)

class Player:
    def __init__(self, world, x, y, PPM):
        self.x = x / PPM
        self.y = y / PPM
        self.w = 1
        self.h = 1
        self.gh = 0.1

        self.world = world
        self.body = self.world.CreateDynamicBody(
            position=(self.x, self.y),
            fixtures=b2FixtureDef(
                shape=b2PolygonShape(box=(self.w, self.h)), density=2.0, friction = 0.1))
        self.body.fixedRotation = True

box.py

from Box2D import (b2FixtureDef, b2PolygonShape)

class Box:
    def __init__(self, world, x, y, PPM):
        self.x = x / PPM
        self.y = y / PPM
        self.w = 10
        self.h = 1

        self.world = world
        self.body = self.world.CreateStaticBody(
            position=(self.x, self.y),
            fixtures=b2FixtureDef(
                shape=b2PolygonShape(box=(self.w, self.h)), density=2.0, friction = 0.1))

draw.py

import pygame
from Box2D import b2PolygonShape

def Poly(screen,body,fixture,PPM):
    shape = fixture.shape
    vertices = [(body.transform * v) * PPM for v in shape.vertices]
    pygame.draw.polygon(screen, (0, 255, 187), vertices)
    pygame.draw.polygon(screen, (0,0,0), vertices,2)

def Draw(screen,bodies,PPM):
    for body in bodies:
        for fixture in body.fixtures:
            if isinstance(fixture.shape, b2PolygonShape):
                Poly(screen,body,fixture,PPM)

Solution

  • You almost certainly have a logic error in this section of code, which appears to control your boolean can_jump variable:

    else:
        for contact in player.body.contacts:
            contact = contact.contact
            print(contact)
            if contact.manifold.localPoint == b2Vec2(0,1):
                can_jump=True
            elif contact.manifold.localPoint != b2Vec2(0,1):
                can_jump=False
    

    One error, one cleanup, and a couple tips...

    First the error:

    you are looping through the contacts. When you fall out of this loop, what has determined the T/F value of can_jump?

    Ans: Only the last contact in the data container will affect the value of can_jump. Does that make sense?

    Tip:

    you are testing for a T/F condition here, the proper way to do it is do the test once and let that determine the result. Right now you are testing "if true" then testing "if false." This is redundant and error-prone. So something like this is better:

    if contact.blah.blah == b2vec():
        can_jump = True
    else:
        can_jump = False
    

    Correction:

    Now, that doesn't solve your problem. (This is getting as long as your code...lol). What you probably want to do is check for "all" or "any" of the contacts to determine the condition, depending on the logic of your game. You could either do this with a loop, as you are doing now, and, if you hit a "fail" condition, set the variable, and break out of the loop or such. Or you could be slick and look at Python's any() and all() built in commands.

    For example... (you would replace the logic test here with your own, but this shows the structure)

    contacts = {2, 5, 7}
    
    can_jump = any(contact > 3 for contact in contacts)
    print(can_jump)  # True
    
    can_jump = all(contact > 3 for contact in contacts)
    print(can_jump)  # False
    

    So, you could either re-write your loop logic correctly or you could replace it all with a 1-liner, which should work even if contacts is empty, which will generate a False as well!