1

Im making a plat former game with pygame

So im creating sprites using

self.rect = self.image.get_rect()

then to get it to scale the hotbox im using

self.rect.inflate(-40,-20)

but this only seems to alter the hotbox from the right side and I think the bottom (Because the x,y starts in the top left of the sprite)

so now my hotbox for my enemy sprite is fine on the left side but still unproportionate on the left side

I hear theres a way to make the hitbox start in the centre of the sprite, How would I do this?

If not how should I go about fixing hotboxes

Thanks in advance,

edit: this is my code for my sprite

#ENEMY SPRITE class
class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load('img/blob.png')
        self.image = pygame.transform.scale(self.image, (65,35))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.move_direction = 1
        self.move_counter = 0

    def update(self): #update enemy (movement)
        self.rect.x += self.move_direction
        self.move_counter += 1
        if abs(self.move_counter) > 50:
            self.move_direction *= -1
            self.move_counter *= -1

Full code

#import modules
import pygame
from pygame.locals import *
from pygame import mixer
import pickle
from os import path


#initiliaze pygamee

pygame.mixer.pre_init(44100,-16,2,512) #volume control

mixer.init()



pygame.init()

#fps
clock = pygame.time.Clock()
font = pygame.font.SysFont('Bauhaus 93', 70)
font_score = pygame.font.SysFont('Bauhaus 93',30)
#screen creation/global variables
screen_width = 800
screen_height = 800
tile_size = 40
fps = 60

game_over = 0
main_menu = True
level = 1
max_levels = 7
score = 0



screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('Crashlandingv6')

#color
white = (255,255,255)
red = (255,15,15)
blue = (0,0,200)
#load images
bg_img = pygame.image.load('img/background.jpg')
bg_img = pygame.transform.scale(bg_img, (1000,1000))
earth_img = pygame.image.load('img/earth.png')
earth_img = pygame.transform.scale(earth_img, (100,100))
rect = bg_img.get_rect()
restart_img = pygame.image.load('img/restart_btn.png')
start_img = pygame.image.load('img/start_btn.png')
exit_img = pygame.image.load('img/exit_btn.png')


#Load sounds
pygame.mixer.music.load('img/music.wav')
pygame.mixer.music.play(-1,0.0,15000)
coin_fx = pygame.mixer.Sound('img/coin.wav')
coin_fx.set_volume(0.4)
jump_fx = pygame.mixer.Sound('img/jump.wav')
jump_fx.set_volume(0.4)
game_over_fx = pygame.mixer.Sound('img/gameover.wav')
game_over_fx.set_volume(0.5)
def draw_text(text,font,text_col,x,y):
    img = font.render(text,True, text_col)
    screen.blit(img,(x,y))

#function to reset level
def reset_level(level):
    player.reset(100, screen_height - 130)
    blob_group.empty()
    lava_group.empty()
    exit_group.empty()
    if path.exists(f'level{level}_data'):
        pickle_in = open(f'level{level}_data', 'rb')
        world_data = pickle.load(pickle_in)
    world = World(world_data)
    return world

#create button class

class Button():
    def __init__(self,x,y,image):
        self.image = image
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.clicked = False

    def draw(self):
        action = False

        pos = pygame.mouse.get_pos()


        #check for button collision (if button was clicked {action}
        if self.rect.collidepoint(pos):
            if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False:
                action = True
                self.clicked = True
        if pygame.mouse.get_pressed()[0] == 0:
            self.clicked = False
        #draw button to screen
        screen.blit(self.image,self.rect)

        return action

#class for player
class Player():
    def __init__(self, x, y):
        self.reset(x,y)

    def update(self,game_over):
        dx = 0 #delta x
        dy = 0 #delta y
        walk_cooldown = 4 #speed

        if game_over == 0: #if game is running gameover = 0 if game is over gameover = -1
            #get keypresses (controls)
            key = pygame.key.get_pressed()
            if key[pygame.K_SPACE] and self.jumped == False:
                jump_fx.play()
                self.vel_y = -15
                self.jumped = True

            if key[pygame.K_LEFT]:
                dx -= 5
                self.counter += 1
                self.direction = -1
            if key[pygame.K_RIGHT]:
                dx += 5
                self.counter += 1
                self.direction = 1
            if key[pygame.K_LEFT] == False and key[pygame.K_RIGHT] == False:
                self.counter = 0
                self.index = 0
                if self.direction == 1:
                    self.image = self.images_right[self.index]
                if self.direction == -1:
                    self.image = self.images_left[self.index]

            #TO DO < insert here !!
            # add idle player animation if all buttons are false set player to idle



            # players animation
            if self.counter > walk_cooldown:
                self.counter = 0
                self.index += 1
                if self.index >= len(self.images_right):
                    self.index = 0
                if self.direction == 1:
                    self.image = self.images_right[self.index]
                if self.direction == -1:
                    self.image = self.images_left[self.index]



            #add gravity
            self.vel_y += 1
            if self.vel_y > 10:
                self.vel_y = 10
            dy += self.vel_y

            #check for collision

            for tile in world.tile_list:
                #x direction collision
                if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height):
                    dx=0




                #y direction collision
                if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height):
                    #check if below ground (jumping)
                    if self.vel_y <0:
                        dy = tile[1].bottom - self.rect.top
                        self.vel_y = 0
                    #check if above ground(falling)
                    elif self.vel_y >= 0:
                        dy = tile[1].top - self.rect.bottom
                        self.jumped = False
                #check for collision with enemies
                if pygame.sprite.spritecollide(self, blob_group, False):
                    game_over = -1
                    game_over_fx.play()



                #check for collision with lava
                if pygame.sprite.spritecollide(self,lava_group,False):
                    game_over = -1


                # check for collision with lava
                if pygame.sprite.spritecollide(self, exit_group, False):
                    game_over = 1



            #if gameover (recall gameover = -1 gamerunning = 0)
        elif game_over == -1:
            self.image = self.dead_image
            draw_text('GAME OVER!', font, red, (screen_width //2) - 200, screen_height //2)
            if self.rect.y > 200:
                self.rect.y -= 5
        #update player coordinates
        self.rect.x += dx
        self.rect.y += dy



        #draw player onto screen
        screen.blit(self.image, self.rect)
        #for rect outlines uncomment #pygame.draw.rect(screen,(255,255,255), self.rect, 2)

        return game_over

    def reset(self,x,y): #Player class under reset button , when player  class is created info gets called from reset for efficiency purposes (instead of typing out twice)
        self.images_right = []
        self.images_left = []
        self.index = 0
        self.counter = 0
        for num in range(1, 7):
            img_right = pygame.image.load(f'img/guy{num}.png')
            img_right = pygame.transform.scale(img_right, (40, 80))
            img_left = pygame.transform.flip(img_right, True, False)  # flips right image on the x axis {true} and not y axis {false}
            self.images_right.append(img_right)
            self.images_left.append(img_left)
        self.dead_image = pygame.image.load('img/ghost.png')
        self.image = self.images_right[self.index]
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.width = self.image.get_width()
        self.height = self.image.get_height()
        self.vel_y = 0
        self.jumped = False
        self.direction = 0

#class for tiles
class World():
    def __init__(self,data):
        self.tile_list = []

        #load images
        dirt_img = pygame.image.load('img/dirt.png')
        moonrock_img = pygame.image.load('img/moonrock.png')



#game map
        row_count = 0
        for row in data:
            col_count = 0
            for tile in row:
                if tile == 1: #replace with dirt
                    img = pygame.transform.scale(dirt_img,(tile_size,tile_size))
                    img_rect = img.get_rect()
                    img_rect.x = col_count * tile_size
                    img_rect.y = row_count * tile_size
                    tile = (img,img_rect)
                    self.tile_list.append(tile)
                if tile == 2: #replace with moonrock
                    img = pygame.transform.scale(moonrock_img, (tile_size, tile_size))
                    img_rect = img.get_rect()
                    img_rect.x = col_count * tile_size
                    img_rect.y = row_count * tile_size
                    tile = (img, img_rect)
                    self.tile_list.append(tile)
                if tile == 3: #replace with alien
                    blob = Enemy(col_count * tile_size, row_count * tile_size + 10)
                    blob_group.add(blob)
                if tile == 6: #replace with acid
                    lava = Lava(col_count * tile_size, row_count * tile_size+(tile_size //2))
                    lava_group.add(lava)
                if tile == 7:
                    coin = Coin(col_count * tile_size + (tile_size //2), row_count * tile_size + (tile_size // 2))
                    coin_group.add(coin)
                if tile == 8:
                    exit = Exit(col_count * tile_size, row_count * tile_size - (tile_size//2))
                    exit_group.add(exit)
                col_count += 1
            row_count += 1


    def draw(self): #draws tiles to screen
        for tile in self.tile_list:
            screen.blit(tile[0],tile[1])
    #for rectangle outlines uncomment       #pygame.draw.rect(screen,(255,255,255), tile[1], 1)

def hitbox_from_image(surf):
    image_mask = pygame.mask.from_surface(surf)
    rect_list = image_mask.get_bounding_rects()
    return rect_list[0].unionall(rect_list)

#ENEMY SPRITE class
class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load('img/blob.png')
        self.image = pygame.transform.scale(self.image, (65,35))



        self.rect = hitbox_from_image(self.image)
        self.rect.x = x
        self.rect.y = y
        self.move_direction = 1
        self.move_counter = 0

    def update(self): #update enemy (movement)
        self.rect.x += self.move_direction
        self.move_counter += 1
        if abs(self.move_counter) > 50:
            self.move_direction *= -1
            self.move_counter *= -1

#LIQUID SPRITE (acid)
class Lava(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        img = pygame.image.load('img/lava2.jpg')
        self.image = pygame.transform.scale(img, (tile_size, tile_size//2))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

class Coin(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        img = pygame.image.load('img/coin.png')
        self.image = pygame.transform.scale(img, (tile_size//2, tile_size//2))
        self.rect = self.image.get_rect()
        self.rect.center = (x,y)




class Exit(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        img = pygame.image.load('img/exit.png')
        self.image = pygame.transform.scale(img, (tile_size, int(tile_size * 1.5)))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y






player = Player(100,screen_height - 130)
blob_group = pygame.sprite.Group()
lava_group = pygame.sprite.Group()
coin_group = pygame.sprite.Group()
exit_group = pygame.sprite.Group()


#score coin dumby coin
score_coin = Coin(tile_size//2, tile_size//2)
coin_group.add(score_coin)
#load in level data and create world

if path.exists(f'level{level}_data'):
    pickle_in = open(f'level{level}_data', 'rb')
    world_data = pickle.load(pickle_in)
world = World(world_data)

#create buttons
restart_button = Button(screen_width // 2 - 50, screen_height // 2 + 100, restart_img)
start_button = Button(screen_width// 2 - 350, screen_height // 2, start_img)
exit_button = Button(screen_width// 2 + 100, screen_height // 2, exit_img)
#main loop/ WHILE GAME IS RUNNING DO THIS
run = True
while run:
    clock.tick(fps) #run the fps timers
    screen.blit(bg_img,rect) #add bg img
    screen.blit(earth_img,(100,100))


    if main_menu == True:
        if exit_button.draw():
            run = False
        if start_button.draw():
            main_menu = False
    else:

        world.draw() #draw the world tiles
        if game_over == 0: # while alive / not dead
            blob_group.update()

            #update score and  checking for coin collection

            if pygame.sprite.spritecollide(player,coin_group,True):
                score += 1
                coin_fx.play()

            draw_text("X " + str(score), font_score ,white, tile_size - 10, 10)
        blob_group.draw(screen)
        lava_group.draw(screen)
        coin_group.draw(screen)
        exit_group.draw(screen)


        game_over = player.update(game_over)


        #if player is dead
        if game_over == -1:
            if restart_button.draw():
                world_data = []
                world = reset_level(level)
                game_over = 0
                score = 0

        #If level complete reset and next level
        if game_over == 1:
            level += 1
            if level <= max_levels:
                #reset level
                world_date = []
                world = reset_level(level)
                game_over = 0
            else:
                draw_text('WINNER WINNER!', font, blue, (screen_width //2) - 140, screen_height // 2)
                #restart game
                if restart_button.draw():
                    level = 1
                    # reset level
                    world_date = []
                    world = reset_level(level)
                    game_over = 0
                    score = 0

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


    pygame.display.update() #update display

pygame.quit() #quit game
Rabbid76
  • 202,892
  • 27
  • 131
  • 174

1 Answers1

1

See rect.inflate:

Returns a new rectangle with the size changed by the given offset. The rectangle remains centered around its current center.

However, while rect.inflate_ip changes the pygame.Rect object itself, rect.inflate does not change the object, but it returns a new object with a different size Note, the return value of rect.inflate_ip is None, but the return value of rect.inflate is a new pygame.Rect object.

Either call inflate_ip:

self.rect.inflate_ip(-40,-20)

or assign the return value of inflate to self.rect

self.rect = self.rect.inflate(-40,-20)

"how should I go about fixing hotboxes"

This depends on the area covered by the sprite in the bitmap. The covered area can be computed by using a pygame.mask.Mask object and get_bounding_rects.

Use pygame.mask.from_surface to create a pygame.mask.Mask from a pygame.Surface:

image_mask = pygame.mask.from_surface(self.image)

Get a list containing a bounding rectangles (sequence of pygame.Rect objects) for each connected component with get_bounding_rects:

rect_list = image_mask.get_bounding_rects()

Create the union rectangle of the sequence of rectangles with unionall:

self.rect = rect_list[0].unionall(rect_list)

See also How to get the correct dimensions for a pygame rectangle created from an image

Write a function that gets the job done:

def hitbox_from_image(surf):
    image_mask = pygame.mask.from_surface(surf)
    rect_list = image_mask.get_bounding_rects()
    return rect_list[0].unionall(rect_list)
self.rect = hitbox_from_image(self.image)
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • Is there an easier way to do this, is there a way I can make the rect start in the centre of the sprite and shorten all sides by like -10 pixels. I tried to do self.rect.inflate_ip(-10,-10) but I just got an error. Im not too familiar with pygame especially masks I only know the basics. the only reason im making a game is for school gamedev or pygame isn't really my interest –  Mar 29 '21 at 14:37
  • @JacksonCadeau The way I've described is not complicate at all. But that is exactly want `inflate does.` I've extended the answer: `self.rect = self.rect.inflate(-40,-20)` or `self.rect.inflate_ip(-40,-20)` – Rabbid76 Mar 29 '21 at 14:41
  • Thank you. im still kinda new to pygame so its confusing to me, im going to try to figure out what to do with the info you gave me. –  Mar 29 '21 at 14:49
  • @JacksonCadeau `inflate` doesn't change the rectangle itself, but returns a new rectangle. All you have to do is to replace `inflate` with `inflate_ip` – Rabbid76 Mar 29 '21 at 14:50
  • I replace self.rect = self.rect.inflate(-40,-20) into. self.rect.inflate_ip(-40,-20) but still it only scales the rect from the right side of my sprite. when I collide with it on the left side the collision is still colliding when my player 40 pixels too far away from the enemy sprite vs on the right it collides when it is a few pixel overlapping my sprite and player –  Mar 29 '21 at 14:54
  • @JacksonCadeau Yes that will work. But you have to finde the correct arguments for your sprite. – Rabbid76 Mar 29 '21 at 14:55
  • @JacksonCadeau The other option is to use the function I just added to the answer. – Rabbid76 Mar 29 '21 at 14:58
  • Where in my code do I add that, also thank you for this effort even though im completely lost your helping me anyway I appreciate you –  Mar 29 '21 at 15:02
  • @JacksonCadeau Instead of `self.rect = self.image.get_rect()`. (You don't need `self.rect.inflate(-40,-20)` anymore) – Rabbid76 Mar 29 '21 at 15:10
  • I added it to my code by again it only works from the right side –  Mar 29 '21 at 15:17
  • im gonna paste my whole code in the post, do you think u can take a look –  Mar 29 '21 at 15:24