Search code examples
pythonpygamecollisionmask

Mask collisions doing funky stuff when I make the camera follow the player


I'm making a racing game with almost realistic cars in Pygame. I made the cars successfully and I want to make the car move a bit slower when it's not touching the drive way. For that, I want to use Pygame mask collisions. Instead of using bounding boxes, they use the pixels and count them based on how many pixels of something are touching something. This all works correctly, but when I use the cam.follow_object(player) function, the game behaves very weird and there rise unexplainable bugs.

I included each individual picture and file, but I also included it on GitHub because I know it's annoying to download all images one by one.

Assets:

File app.py

import pygame
from pygame.locals import *
from random import choice, randint
import math, car, ui, pySave, camera, level

pygame.init()

# Basic Variables
screen_width = 1000
screen_height = 1000
fps = 80

screen = pygame.display.set_mode((screen_width, screen_height))
clock = pygame.time.Clock()

cam = camera.Camera(1, screen)
#cam.zoom_game(1.55)

stage = "home"

# Loading Images

images = {
    "start" : pygame.transform.scale(pygame.image.load("road_texture.png"), (200*cam.zoom, 75*cam.zoom)).convert_alpha(),
    "bcg" : pygame.transform.scale(pygame.image.load("map2.png"), (1000*cam.zoom, 1000*cam.zoom)).convert_alpha(),
    "car" : pygame.image.load("car.png").convert_alpha()
}

# Functionality Functions

def play_game():
    global stage
    stage = "game"

# Instantiating stuff

pos_save = pySave.Save_Manager("saved_info", "pos")

start_button = ui.Button((500, 500), images["start"], play_game, text="startgame")

test_car = car.CarSprite(images["car"], 400, 400, [2, 2.3, 2.7], 0.013, rotations=360, camera=cam)

speedometer = ui.TextWithBackground((100, 50), (100, 950), images["start"])

level = level.Level(images["bcg"], test_car, cam, pos = (0, 100))

# Groups and Lists

car_group = pygame.sprite.Group()

# Adding to Groups

car_group.add(test_car)

# Game Functions
def render():

    if stage == "home":

        screen.fill((255, 255, 255))
        start_button.update("Start Game")
        start_button.draw(screen)

    elif stage == "game":

        screen.fill((0, 76, 18))
        level.update(screen)#screen.blit(images["bcg"], (0-cam.scroll[0], 0-cam.scroll[1]))

        car_group.update()
        car_group.draw(screen)

        #cam.follow(test_car, )

        #speedometer.draw(screen)
        speedometer.update(screen, round(test_car.speed*60, 2), " km/h")
        #cam.zoom_game(2)


def collisions():
    pass

run = True
while run:

    clock.tick_busy_loop(80)

    render()
    collisions()

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


            pos_save.save("x", test_car.rect.x)
            pos_save.save("y", test_car.rect.y)

            pos_save.apply()

            print(f"Quit with {round(clock.get_fps(), 2)} FPS")

            quit()

    pygame.display.update()

File car.py:

import pygame
from pygame.locals import *
from random import choice, randint
import math, camera

class CarSprite(pygame.sprite.Sprite):
    def __init__(self, car_image, x, y, max_speed, accel, rot_speed=[1.8, 2.2, 3], rotations=360, camera=""):
        pygame.sprite.Sprite.__init__(self)

        self.rotated_images = {}
        self.min_angle = (360 / rotations)
        for i in range(rotations):
            rotated_image = pygame.transform.rotozoom(pygame.transform.scale(car_image, (12*camera.zoom, 25*camera.zoom)), 360 - 90 - (i*self.min_angle), 1)
            self.rotated_images[i*self.min_angle] = rotated_image

        self.min_angle = math.radians(self.min_angle)
        self.image       = self.rotated_images[0]
        self.rect        = self.image.get_rect()
        self.rect.center = (x, y)
        self.reversing = False
        self.heading   = 0
        self.speed     = 0
        self.velocity  = pygame.math.Vector2(0, 0)
        self.position  = pygame.math.Vector2(x, y)
        self.speed_hardening = 1
        self.acc = False
        self.steer_strenght_acc = rot_speed[0]
        self.steer_strength_normal = rot_speed[1]
        self.steer_strength_drift = rot_speed[2]
        self.steer_strength = rot_speed[1]
        self.drift_point = 0.00
        self.accel = accel
        self.max_speed = self.accel * 150
        self.cam = camera

    def turn(self, ori=1):
        if self.speed > 0.1 or self.speed < 0.0:
            self.heading += math.radians(self.steer_strenght * ori)
            image_index = int((self.heading + self.min_angle / 2) / self.min_angle) % len(self.rotated_images)
            image = self.rotated_images[image_index]
            if self.image is not image:
                x, y = self.rect.center
                self.image = image
                self.rect  = self.image.get_rect()
                self.rect.center = (x, y)

    def accelerate(self):
        self.speed += self.accel

    def brake(self):
        if self.speed > 0:
            self.speed -= self.accel * 3

        if abs(self.speed) < 0.1:
            self.speed = 0
        self.velocity.from_polar((self.speed, math.degrees(self.heading)))

    def move(self):
        keys = pygame.key.get_pressed()
        if keys[K_w]:
            self.accelerate()

        if keys[K_s]:
            self.brake()
        if keys[K_a]:
            self.turn(-1)
        if keys[K_d]:
            self.turn()

        if keys[pygame.K_s] or keys [pygame.K_w]:
            self.acc = True
        else:
            self.acc = False

    def update(self):


        self.move()
        self.speed_hardening = self.speed / 100
        self.speed = round(self.speed, 3)

        if self.acc:
            self.steer_strenght = self.steer_strenght_acc
        else:
            self.steer_strenght = self.steer_strength_normal

        if self.speed > self.max_speed and not pygame.key.get_pressed()[K_SPACE] and not self.drift_point > 0:
            self.speed += self.accel / 4 - self.speed_hardening / 2

        if self.speed > self.max_speed * 1.8:
            self.speed = self.max_speed * 1.8

        if self.speed < -self.max_speed / 4:
            self.speed = -self.max_speed / 4

        if not pygame.key.get_pressed()[K_SPACE]:
            self.velocity.from_polar((self.speed, math.degrees(self.heading)))
            self.speed += self.drift_point

            self.drift_point -= 0.0001
            if self.drift_point < 0:
                self.drift_point = 0
            self.speed -= self.drift_point

        else:
            self.steer_strenght = self.steer_strength_drift
            self.drift_point += 0.0001
            if self.drift_point > self.accel / 1.5:
                self.drift_point = self.accel / 1.5



        if not self.acc and not self.speed < 0.04:
            self.speed -= (self.accel / 2) + self.speed_hardening

            if self.speed < 0.05:
                self.speed = 0

        self.position += self.velocity
        self.rect.center = self.position

File level.py

import pygame
from pygame.locals import *
from random import choice, randint
import math, car, ui, pySave, camera

screen_width = 1000
screen_height = 1000

class Level:
    def __init__(self, image, car, camera, pos=(0, 0)):
       # Convert the images to a more suitable format for faster blitting
       self.image = image.convert()
       self.road = image
       self.cam = camera
       self.x, self.y = pos
       self.bcg_mask = pygame.mask.from_surface(self.road)
       self.car = car
       self.get_car_mask()

    def update(self, screen):
        # Calculate the overlap between the car mask and the background mask
        overlap = self.bcg_mask.overlap_mask(
            self.car_mask,
            (self.car.rect.x, self.car.rect.y)
        )

        self.x = 0 - self.cam.scroll[0]
        self.y = 0 - self.cam.scroll[1]

        # Fill the screen with the background color
        screen.blit(self.road.convert_alpha(), (self.x, self.y))
        screen.blit(overlap.to_surface(unsetcolor=(0, 0, 0, 0), setcolor=(255, 255, 255, 255)), (self.x, self.y))


        # Print the overlap count to the console
        print(overlap.count())

    def get_car_mask(self):
        # Convert the car image to a more suitable format for faster blitting
        carimg = self.car.image.convert()
        carimg.set_colorkey((0, 0, 0))

        self.carimg = carimg
        self.car_mask = pygame.mask.from_surface(self.carimg)

File camera.py

import pygame
from pygame.locals import *
class Camera:
    def __init__(self, speed, screen):
        self.scroll = [5, 5]
        self.speed = speed
        self.screen = screen
        self.zoom = 1

    def move_on_command(self):

        keys = pygame.key.get_pressed()

        if keys[K_UP]:
            self.scroll[1] -= self.speed
        if keys[K_DOWN]:
            self.scroll[1] += self.speed
        if keys[K_RIGHT]:
            self.scroll[0] += self.speed
        if keys[K_LEFT]:
            self.scroll[0] -= self.speed

    def follow(self, obj, speed=12):

        #self.scroll[0], self.scroll[1] = obj.rect.x, obj.rect.y

        if (obj.rect.x - self.scroll[0]) != self.screen.get_width()/2:
            self.scroll[0] += ((obj.rect.x - (self.scroll[0] + self.screen.get_width()/2)))
        if obj.rect.y  - self.scroll[1] != self.screen.get_height()/2:
            self.scroll[1] += ((obj.rect.y - (self.scroll[1] + self.screen.get_height()/2)))

    def zoom_game(self, zoom):
        self.zoom = zoom

File ui.py

import pygame
from pygame.locals import *
from random import choice, randint

class Button:
    def __init__(self, pos, image, action, click_times=1, dissapear=True, text="", textcolor = (255, 255, 255), fontsize=30):
        self.image = image
        self.rect = self.image.get_rect(center=pos)

        self.clicked = False
        self.clicked_times = click_times
        self.dissapear = dissapear
        self.dont_draw = False

        self.function = action

        self.text = text

        if not self.text == "":
            self.font = pygame.font.SysFont("Arial", fontsize, False, False)
            self.color = textcolor


    def update(self, var=""):
        pressed = pygame.mouse.get_pressed()[0]
        if not self.text == "":
            self.text = self.font.render(f"{var}", 1, self.color)
            self.text_rect = self.text.get_rect(center = self.rect.center)

        if pressed and not self.clicked and not self.clicked_times <= 0 and self.rect.collidepoint(pygame.mouse.get_pos()):
            self.function()
            self.clicked = True
            self.clicked_times -= 1

        if not pressed:
            self.clicked = False

        if self.clicked_times <= 0:
            if self.dissapear:
                self.dont_draw = True

    def draw(self, screen):

        if not self.dont_draw:
            screen.blit(self.image, (self.rect.x, self.rect.y))
            screen.blit(self.text, (self.text_rect.x, self.text_rect.y))

class TextWithBackground:
    def __init__(self, image_size, pos, image, fontsize=30, colour=(255, 255, 255)):
        self.image = pygame.transform.scale(image, image_size)
        self.rect = self.image.get_rect(center=pos)
        self.font = pygame.font.SysFont("Arial", fontsize, False, False)
        self.color = colour
        self.text = self.font.render("", 1, self.color)
        self.other_text = None
        self.other_rect = None

    def update(self, screen, variable, othertext=""):
        self.text = self.font.render(f"{variable}", 1, self.color)
        self.text_rect = self.text.get_rect(center=self.rect.center)

        if othertext:
            self.other_text = self.font.render(f"{othertext}", 1, self.color)
            self.other_rect = self.other_text.get_rect(topleft=(self.rect.center[0] - 5, self.rect.center[1] - 5))

        screen.blit(self.image, (self.rect.x, self.rect.y))
        screen.blit(self.text, (self.rect.x, self.rect.y))
        if self.other_text:
            screen.blit(self.other_text, (self.other_rect.x, self.other_rect.y))

Here you can find my images:

car.png

map2.png

road_texture.png


Solution

  • See PyGame collision with masks. You need to calculate the offset between the car and the map when you get the overlap area of the masks:

    class Level:
        # [...]
    
        def update(self, screen):
            self.x = 0 - self.cam.scroll[0]
            self.y = 0 - self.cam.scroll[1]
    
            # Calculate the overlap between the car mask and the background mask
            offset = (self.car.rect.x - self.x, self.car.rect.y - self.y)
            overlap = self.bcg_mask.overlap_mask(self.car_mask, offset)  
    
            # Fill the screen with the background color
            screen.blit(self.road.convert_alpha(), (self.x, self.y))
            screen.blit(overlap.to_surface(unsetcolor=(0,0,0,0), setcolor=(255,255,255,255)), (self.x, self.y))
    
            # Print the overlap count to the console
            print(overlap.count())