1
\$\begingroup\$

So I coded a 2d sidescrolling platform, everything is fine except this:

The sprite stands on platform with her sword

As you can see, the sprite is standing at the platform when her feet is not in the platform anymore. I want to adjust the collision hitbox size so that when her feet is not on the platform, she falls to the ground.

Below is the code for sprite initiation:

    class Player(pygame.sprite.Sprite):

    #Initiating the player (23 / 5)
    #because there is only one character right now, it will have no case for character selection (23 / 5)
    def __init__(self):
         super().__init__()

         #width and height of character (23 / 5)
         width = 80
         height = 120

         #character sprite goes here (23 / 5)
         self.image = pygame.image.load("Malahayati.jpg").convert_alpha()
         self.image = pygame.transform.scale(self.image,(width,height))

         #hitbox (23 / 5)
         self.rect = self.image.get_rect()

         #character speed (23 / 5)
         self.vx = 0
         self.vy = 0

         #references to the list of sprite we bumped against (24 / 5)
         self.level = None

         #current state: 0 is idle, 1 is moving right, 2 is moving left, 3 is dashing
         self.status = 0

         #character status (24 / 5)
         self.rank = 1
         self.health = 220
         self.attack = 50
         self.defense = 20
         self.speed = 52.5
         self.exp = 0
         self.jumpspeed = 125
\$\endgroup\$

2 Answers 2

0
\$\begingroup\$

First of all, you have to give your sprites a second Rect attribute, the scaled hitbox which will be used for the collision detection. You have to do that because the self.rect.topleft coordinates are used as the blit position and if you shrink the Rect, it will be smaller but only fill the upper left area of the image. (The darker area represents the scaled Rect and the brighter area the image.)

 ________
|░░░░░   |
|░░░░░   |
|░░░░░   |
|        |
|________|

So create the hitbox either by scaling the original Rect with the inflate method or create a new one with the desired size. I also give my sprites a pos attribute (a pygame.math.Vector2) which represents the actual center position of the sprites. When the pos is updated in the update method, you have to assign it to the center of the rect and the hitbox as well in order to move them.

Then define a custom collision detection function which will be passed as the collided argument to pygame.sprite.spritecollide or groupcollide. In this function you can use the colliderect method of the hitbox Rect to check if it collides with the rect (or hitbox) of the other sprite.

In your case you also need to shift the hitbox because the weapon of the character is so large. You can do this by defining a vector (called offset here) which you have to add to the self.hitbox every update. When the character and its image are flipped, you also have to invert the self.offset vector.

Here's a minimal, complete example. The images are blue and gray (with the hitboxes centered at the blue area), the hitboxes red and the outer rects (self.rect for the blitting) are green. Press F to flip the player sprite.

import pygame as pg
from pygame.math import Vector2


pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))
pg.draw.rect(IMAGE, (160, 160, 160), (0, 0, 20, 110))


class Entity(pg.sprite.Sprite):

    def __init__(self, pos, *groups):
        super().__init__(*groups)
        self.image = IMAGE
        self.rect = self.image.get_rect(center=pos)
        # A deflated copy of the rect as the hitbox.
        self.hitbox = self.rect.inflate(-40, -20)
        self.vel = Vector2(0, 0)
        self.pos = Vector2(pos)  # Actual center position.
        self.offset = Vector2(10, 0)

    def update(self):
        self.pos += self.vel
        self.rect.center = self.pos
        # Add the offset to the pos to center the hitbox
        # at the blue part of the image.
        self.hitbox.center = self.pos + self.offset

    def flip(self):
        # Check which side the sprite is facing.
        if self.offset.x > 0:
            self.image = pg.transform.flip(IMAGE, True, False)
        else:
            self.image = IMAGE
        self.offset = -self.offset  # Invert the offset.
            

# This callback function is passed as the `collided`argument
# to pygame.sprite.spritecollide or groupcollide.
def collided(sprite, other):
    """Check if the hitboxes of the two sprites collide."""
    return sprite.hitbox.colliderect(other.hitbox)


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    all_sprites = pg.sprite.Group()
    player = Entity((300, 200), all_sprites)
    player.flip()
    enemies = pg.sprite.Group(
        Entity((100, 250), all_sprites),
        Entity((400, 300), all_sprites),
        )

    done = False

    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            elif event.type == pg.MOUSEMOTION:
                player.pos = event.pos
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_f:
                    player.flip()

        all_sprites.update()
        # Pass the custom collided callback function to spritecollide.
        collided_sprites = pg.sprite.spritecollide(player, enemies, False, collided)
        for sp in collided_sprites:
            print('Collision', sp)

        screen.fill((30, 30, 30))

        all_sprites.draw(screen)
        for sprite in all_sprites:
            # Draw rects and hitboxes.
            pg.draw.rect(screen, (0, 230, 0), sprite.rect, 2)
            pg.draw.rect(screen, (250, 30, 0), sprite.hitbox, 2)

        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    main()
    pg.quit()

Alternatively, you could give your sprite two offset vector attributes and add the current offset to the rect.topleft position when you blit the sprite. However, that means the sprite group's draw method can't be used anymore and you have to blit the sprites in a for loop. On the other hand, you wouldn't need the hitbox and the collided callback function anymore.

import pygame as pg
from pygame.math import Vector2


pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))
pg.draw.rect(IMAGE, (160, 160, 160), (0, 0, 20, 110))
IMAGE_FLIPPED = pg.transform.flip(IMAGE, True, False)


class Entity(pg.sprite.Sprite):

    def __init__(self, pos, *groups):
        super().__init__(*groups)
        self.image = IMAGE
        self.rect = self.image.get_rect(center=pos).inflate(-40, -20)
        self.vel = Vector2(0, 0)
        self.pos = Vector2(pos)  # Actual center position.
        self.offset_right = Vector2(-30, -10)
        self.offset_left = Vector2(-10, -10)
        self.offset = self.offset_right

    def update(self):
        self.pos += self.vel
        self.rect.center = self.pos

    def flip(self):
        # Check which side the sprite is facing.
        if self.offset.x <= -30:
            # Swap the image and the offset vector.
            self.image = IMAGE_FLIPPED
            self.offset = self.offset_left  
        else:
            self.image = IMAGE
            self.offset = self.offset_right


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    all_sprites = pg.sprite.Group()
    player = Entity((300, 200), all_sprites)
    player.flip()
    enemies = pg.sprite.Group(
        Entity((100, 250), all_sprites),
        Entity((400, 300), all_sprites),
        )
    # Define blit as a local variable to increase the performance.
    blit = screen.blit

    done = False

    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            elif event.type == pg.MOUSEMOTION:
                player.pos = event.pos
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_f:
                    player.flip()

        all_sprites.update()
        collided_sprites = pg.sprite.spritecollide(player, enemies, False)
        for sp in collided_sprites:
            print('Collision with id', id(sp))

        screen.fill((30, 30, 30))
        
        for sprite in all_sprites:
            # Add the current offset to the blit position.
            blit(sprite.image, sprite.rect.topleft+sprite.offset)
            pg.draw.rect(screen, (250, 30, 0), sprite.rect, 2)

        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    main()
    pg.quit()
\$\endgroup\$
3
  • \$\begingroup\$ In short, I have to create another box that will be used as collision hitbox , that followed the character spriteholder box? \$\endgroup\$ May 27, 2018 at 16:29
  • \$\begingroup\$ Yes, that's my suggestion. \$\endgroup\$
    – skrx
    May 27, 2018 at 17:38
  • \$\begingroup\$ I am trying to learn your code now, I will give an update later on I need to break it down so I can use this method in my code \$\endgroup\$ May 27, 2018 at 17:52
0
\$\begingroup\$

The hitbox is set up in this line:

self.rect = self.image.get_rect()

Documentation gives the following constructor: Rect(left, top, width, height) https://www.pygame.org/docs/ref/rect.html

So you need to change the above line to this (replacing characterWidth with the new narrower width, and offset to the width of the whitespace to the left of the character's feet):

self.rect = pygame.Rect(self.image.get_rect().left + offset, self.image.get_rect().top, characterWidth, self.image.get_rect().height)

It looks like the whitespace is almost (but not exactly) half the width of the sprite, so here's a working example you can play around with until it looks right (change the two 40s, but ensure the two values always add up to 80):

self.rect = pygame.Rect(self.image.get_rect().left + 40, self.image.get_rect().top, 40, self.image.get_rect().height)
\$\endgroup\$
1
  • \$\begingroup\$ It worked, but on the opposite side \$\endgroup\$ May 27, 2018 at 16:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .