1

I am currently making a top down racing game and need a way to detect when a vehicle has completed a full lap. I have chosen to do this by adding images around the circuit, acting as checkpoints, which match with the track surface. When driven over, they output true, all must have output true in order for a lap to count. However, I cannot find a way to detect a collision between my vehicles and an image.

I have tried adding rects to the vehicles and checking if an output can be produced when the two vehicles collide but I just get this error:

AttributeError: 'pygame.Surface' object has no attribute 'rect'

Is there any way I can do this? My code can be seen below.

import pygame
from pygame.locals import *
import math
import time

pygame.init()
F1image = pygame.image.load("F1image.png")
sportsimage = pygame.image.load("sportsimage.png")
bikeimage = pygame.image.load("bikeimage.png")
muscleimage = pygame.image.load("muscleimage.png")
truckimage = pygame.image.load("truckimage.png")
screen = pygame.display.set_mode((1280,720))
xpos = 280
xpos_2 = 280
ypos = 50
ypos_2 = 85
keys = [False, False, False, False]
keys_2 = [False, False, False, False]
direction = 0
direction_2 = 0
forward = 0
forward_2 = 0

class Background(pygame.sprite.Sprite):
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location

BackGround = Background('track.png', [0,0])

class Vehicle:
    'Base class for all vehicles (Cars and Motorbikes) in the game'
    vehicleCount = 0

    def __init__(self, max_speed, acceleration, turning_radius, image):
        pygame.sprite.Sprite.__init__(self)
        self.max_speed = max_speed
        self.acceleration = acceleration
        self.turning_radius = turning_radius
        self.image = image
        self.rect = self.image.get_rect()
        Vehicle.vehicleCount  = Vehicle.vehicleCount + 1


    def displayAmount():
        print ("Total number of Vehicle enteries: ", Vehicle.vehicleCount)

    def displayVehicle(self):
        print ("max speed: ", self.max_speed, "acceleration: ", self.acceleration, "turning radius: ", self.turning_radius)

    def checkCollision(self, sprite1, sprite2):
        col = pygame.sprite.collide_rect(sprite1, sprite2)
        if col == True:
            print ("True")

F1 = Vehicle(5.0, 0.1, 2.84, F1image)
sportscar = Vehicle(4.5, 0.2, 2.01, sportsimage)
bike = Vehicle(4.0, 0.15, 2.64, bikeimage)
musclecar = Vehicle(3.5, 0.25, 1.76, muscleimage)
truck = Vehicle(3.0, 0.3, 1.20, truckimage)

print (F1.max_speed)

player1choice = input("Input player 1 choice").lower()
player2choice = input("Input player 2 choice").lower()

if player1choice == ("f1"):
    choice1 = F1
elif player1choice == ("sports"):
    choice1 = sportscar
elif player1choice == ("muscle"):
    choice1 = musclecar
elif player1choice == ("truck"):
    choice1 = truck
else:
    choice1 = bike

if player2choice == ("f1"):
    choice2 = F1
elif player2choice == ("sports"):
    choice2 = sportscar
elif player2choice == ("muscle"):
    choice2 = musclecar
elif player2choice == ("truck"):
    choice2 = truck
else:
    choice2 = bike

running = True
while running:
    pygame.display.set_caption("Speed Wars")
    WHITE = (255, 255, 255)
    screen.fill(WHITE)
    screen.blit(BackGround.image, BackGround.rect)

    #Vehicle 1
    if keys[0] == True:
        direction += (choice1).turning_radius
    if keys[1] == True:
        direction -= (choice1).turning_radius
    if keys[2] == True and forward <= (choice1).max_speed:
        forward += (choice1).acceleration
    if keys[3] == True and forward >= 0:
        forward -= (choice1).acceleration

    #Vehicle 2
    if keys_2[0] == True:
        direction_2 += (choice2).turning_radius
    if keys_2[1] == True:
        direction_2 -= (choice2).turning_radius
    if keys_2[2] == True and forward_2 <= (choice2).max_speed:
        forward_2 += (choice2).acceleration
    if keys_2[3] == True and forward_2 >= 0:
        forward_2 -= (choice2).acceleration

    movex = math.cos(direction / 57.29) * forward
    movey = math.sin(direction / 57.29) * forward
    xpos += movex
    ypos -= movey

    movex_2 = math.cos(direction_2 / 57.29) * forward_2
    movey_2 = math.sin(direction_2 / 57.29) * forward_2
    xpos_2 += movex_2
    ypos_2 -= movey_2

    rotation = pygame.transform.rotate((choice1).image, direction)
    rotation_2 = pygame.transform.rotate((choice2).image, direction_2)
    screen.blit(rotation, (xpos, ypos))
    screen.blit(rotation_2, (xpos_2, ypos_2))
    pygame.display.flip()
    time.sleep(0.01)

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

        if event.type == pygame.KEYDOWN:
            if event.key == K_LEFT:
                keys[0] = True
            elif event.key == K_RIGHT:
                keys[1] = True
            elif event.key == K_UP:
                keys[2] = True
            elif event.key == K_DOWN:
                keys[3] = True

            if event.key == K_a:
                keys_2[0] = True
            elif event.key == K_d:
                keys_2[1] = True
            elif event.key == K_w:
                keys_2[2] = True
            elif event.key == K_s:
                keys_2[3] = True

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT:
                keys[0] = False
            elif event.key == pygame.K_RIGHT:
                keys[1] = False
            elif event.key == pygame.K_UP:
                keys[2] = False
            elif event.key == pygame.K_DOWN:
                keys[3] = False

            if event.key == pygame.K_a:
                keys_2[0] = False
            elif event.key == pygame.K_d:
                keys_2[1] = False
            elif event.key == pygame.K_w:
                keys_2[2] = False
            elif event.key == pygame.K_s:
                keys_2[3] = False

        #Collision detection
        (choice1).checkCollision((choice2).image, (choice1).image)
RossC
  • 101
  • 2
  • 8
  • What line of code is causing the error? Somewhere in your code you are trying to access a rect from a `Surface` instead of from an object or by using the `get_rect()` method. – Micheal O'Dwyer Jan 21 '18 at 17:27
  • It sounds like you could use [pixel-perfect collision detection](https://stackoverflow.com/a/48025629/6220679) which you can achieve with the help of masks. – skrx Jan 21 '18 at 19:45

2 Answers2

2

The issue is that your code passes in two images into the checkCollision method in your Vehicle class. Then you are passing these two images into the collide_rect function which expects two Sprites.

As a result, you get an error telling you that the two objects passed in(Surfaces in this case), do not contain rects.

To fix this problem:

  • Use the superclass Sprite for your Vehicle class.

  • Simply pass in the other sprite into the checkCollision method.

As a result, your checkCollision function should look something like this:

def checkCollision(self, sprite2):
    col = pygame.sprite.collide_rect(self, sprite2)
    if col == True:
        print ("True")

And the call to it should look something like this:

choice1.checkCollision(choice2)

Also, your Vehicle class header should look like this:

class Vehicle(pygame.sprite.Sprite)

Some other issues in your code that should be fixed:

  • You are receiving input from the keyboard. This is very weird in a game. Instead, you should look at handling this through keyboard input.

  • You use brackets around choice1 and choice2. This is not needed.

  • There is code in your main game loop that doesn't need to be ran every frame such as pygame.display.set_caption(). This again is not needed and code like this should go before the main game loop.

  • The order of you main game loop is different to how it is normally done. First, you should do event handling. Second, do your logic and lastly, do your rendering.

  • Also, you are making 5 objects and loading many images where only two will be used. Instead, create and load the objects that will be used in the game after the user has decided which car they will play as.

  • Never use time.sleep() inside a pygame script. This function is evil when used with pygame and causes many errors and bugs. If you would like to use a framerate cap then use a Clock.

I highly recommend you follow these items.

I hope this answer helped you and if you have any further questions please feel free to post a comment below!

Micheal O'Dwyer
  • 1,237
  • 1
  • 16
  • 26
  • You have to pass two *sprites* to `checkCollision` instead of two rects. It would also make sense to change it, so that the method takes only the other sprite as an argument and then pass `self` to `collide_rect`: `col = pygame.sprite.collide_rect(self, other_sprite)`. `collide_rect` could also be replaced by `pygame.Rect.colliderect`. BTW, the `Vehicle` class should inherit from `pygame.sprite.Sprite`. – skrx Jan 21 '18 at 19:12
  • Thanks for catching that! For some reason, I thought he was using the `rect.colliderect` method instead of the `pygame.sprite.collide_rect` method. – Micheal O'Dwyer Jan 21 '18 at 19:28
  • @MichealO'Dwyer , thanks for replying, this solution does appear to work somewhat. When the program is run, it appears to output 'True' constantly, even while the two vehicles are not touching / intersecting each other. Any ideas? – RossC Jan 22 '18 at 11:37
  • @skrx See my above reply to MichealO'Dwyer , any ideas what's causing this? – RossC Jan 22 '18 at 12:08
  • I think the problem is that the pygame uses rectangles for collision detection which is at times inaccurate. @skrx posted an imformative answer below on how to implement pixel-perfect collision detection. – Micheal O'Dwyer Jan 22 '18 at 16:33
  • If you want, you can see the problem for yourself by adding the lines `pygame.draw.rect(screen, (255, 0, 0), self.rect, 4)` and `pygame.draw.rect(screen, (255, 0, 0), sprite2.rect, 4)` to your `checkCollision` method. This will simply draw the rectangles being passed in to the `collide_rect` function. – Micheal O'Dwyer Jan 22 '18 at 16:36
  • @RossC I think the guys in the other thread have already explained that you have to update the `rect`s of the vehicles. You should also create separate vehicle objects/instances for the players, because if both players choose the same car, they'll share the same rect. I mean first let the players choose their cars (the strings) and then create the instances afterwards instead of assigning the same instance to both players. – skrx Jan 22 '18 at 21:42
1

To implement checkpoints in a game I would use a solution similar to this: Define a list, group, etc. which contains your checkpoints and set the start point and the active point to the first checkpoint in the list. You can use an itertools.cycle iterator to easily cycle through the points. When the player touches a checkpoint, you set the active_checkpoint to the next point in the iterator and check if it was the start point, if yes, increment your laps counter.

If you want pixel-perfect collision detection, you can give the sprites a self.mask attribute and use pygame.sprite.collide_mask.

Here's a simplified example. I just swap out the images of the sprites here to show which one is active.

import itertools

import pygame as pg


CHECKPOINT_IMG = pg.Surface((120, 20), pg.SRCALPHA)
CHECKPOINT_IMG.fill((120, 60, 0))
CHECKPOINT2_IMG = pg.Surface((120, 20), pg.SRCALPHA)
CHECKPOINT2_IMG.fill((220, 110, 0))


class Player(pg.sprite.Sprite):

    def __init__(self, pos, checkpoints):
        super().__init__()
        self.image = pg.Surface((60, 60), pg.SRCALPHA)
        pg.draw.polygon(self.image, (0, 100, 240), [(30, 0), (60, 60), (0, 60)])
        self.rect = self.image.get_rect(center=pos)
        self.mask = pg.mask.from_surface(self.image)
        self.checkpoints = itertools.cycle(checkpoints)
        self.active_checkpoint = next(self.checkpoints)
        self.start_point = self.active_checkpoint
        self.active_checkpoint.image = self.active_checkpoint.image_active
        self.laps = -1  # I start at -1 because the start is the first checkpoint.

    def handle_event(self, event):
        if event.type == pg.MOUSEMOTION:
            self.rect.center = event.pos
            if pg.sprite.collide_mask(self, self.active_checkpoint):
                if self.active_checkpoint == self.start_point:  # Completed a round.
                    self.laps += 1
                    pg.display.set_caption('Laps: {}'.format(self.laps))
                # I change the images of the previous and next checkpoint
                # to show which one is active.
                self.active_checkpoint.image = self.active_checkpoint.image_inactive
                # Switch to the next checkpoint.
                self.active_checkpoint = next(self.checkpoints)
                self.active_checkpoint.image = self.active_checkpoint.image_active


class Checkpoint(pg.sprite.Sprite):

    def __init__(self, pos, angle=0):
        super().__init__()
        self.image_inactive = pg.transform.rotate(CHECKPOINT_IMG, angle)
        self.image_active = pg.transform.rotate(CHECKPOINT2_IMG, angle)
        self.image = self.image_inactive
        self.rect = self.image.get_rect(center=pos)
        self.mask = pg.mask.from_surface(self.image)


class Game:
    def __init__(self):
        self.screen = pg.display.set_mode((640, 480))

        self.done = False
        self.clock = pg.time.Clock()
        self.checkpoints = (
            Checkpoint((100, 200), 0),
            Checkpoint((300, 100), 60),
            Checkpoint((500, 300), 10),
            Checkpoint((200, 300), 30),
            )

        self.player = Player((20, 20), self.checkpoints)
        self.all_sprites = pg.sprite.Group(self.player)
        self.all_sprites.add(self.checkpoints)

    def run(self):
        while not self.done:
            self.event_loop()
            self.update()
            self.draw()
            pg.display.flip()
            self.clock.tick(60)

    def event_loop(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.done = True
            self.player.handle_event(event)

    def update(self):
        pass

    def draw(self):
        self.screen.fill((30, 30, 30))
        self.all_sprites.draw(self.screen)


if __name__ == '__main__':
    pg.init()
    game = Game()
    game.run()
    pg.quit()
skrx
  • 19,980
  • 5
  • 34
  • 48