1
\$\begingroup\$

I am making a technically top down game with a shifted perspective. It shows more of the characters side, not just above their head. I think most "top down" games have a similar art style.

Due to this, only the bottom of the character sprite should collide and interact with game objects. Should I use two separate objects for the hitbox and the sprite? It seems inefficient to do this. Is there some other solution?

For reference, I am using python3 and the pygame library. I am using the sprite class from pygame. My player class inherits from the sprite class and is comprised of a surface object for the sprite image, and a rectangle object for the coordinates and length/width. Using a separate object for the hitbox and sprite would require to classes for this.

\$\endgroup\$
1
  • \$\begingroup\$ Here's my answer to a similar question on Stack Overflow. \$\endgroup\$
    – skrx
    Commented Apr 25, 2018 at 20:31

2 Answers 2

3
\$\begingroup\$

Instead of making your player object inherit from the sprite class you could use member variables for the sprite and rect. Your player class can then stand alone, and have separate sprite and rect objects. In this way, when you update the player position, you have to set the sprite and collision rect position, but they can be changed independently.

Eg, as a starting point

class Player:
    rect = None
    sprite = None
\$\endgroup\$
0
\$\begingroup\$

There are two, relatively clean, solutions to this problem:

Either give your sprite a separate hitbox attribute (the scaled rect) and pass a customized callback function to pygame.sprite.spritecollide in which you check if the hitboxes instead of the rects collide, or alternatively scale the actual self.rect, then blit the sprites in a for loop and add a predefined offset vector to the rect.topleft coordinates to adjust the blit position.

Here's the hitbox solution:

import pygame as pg
from pygame.math import Vector2


pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))


class Entity(pg.sprite.Sprite):

    def __init__(self, pos, *groups):
        super().__init__(*groups)
        self.image = IMAGE
        # The rect is still used as the blit position.
        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.

    def update(self):
        # Move the sprite by adding the velocity vector to the position.
        self.pos += self.vel
        self.rect.center = self.pos  # Update the rect.
        # Move the hitbox as well. You can also adjust the position
        # of the hitbox here by adding another vector to the pos.
        self.hitbox.center = self.pos


# 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."""
    # Check if the hitboxes collide (instead of the rects).
    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)
    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

        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 with sprite', id(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()

And the offset vector solution:

import pygame as pg
from pygame.math import Vector2


pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))


class Entity(pg.sprite.Sprite):

    def __init__(self, pos, *groups):
        super().__init__(*groups)
        self.image = IMAGE
        # Scale the rect with the inflate method.
        self.rect = self.image.get_rect(center=pos).inflate(-40, -20)
        self.vel = Vector2(0, 0)
        self.pos = Vector2(pos)  # Actual center position.
        # Add this vector to the rect.topleft coords during the
        # blitting to adjust the position.
        self.offset = Vector2(-20, -10)

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


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    all_sprites = pg.sprite.Group()
    player = Entity((300, 200), all_sprites)
    enemies = pg.sprite.Group(
        Entity((100, 250), all_sprites),
        Entity((400, 300), all_sprites),
        )
    # Assign screen.blit to 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

        all_sprites.update()
        collided_sprites = pg.sprite.spritecollide(player, enemies, False)
        for sp in collided_sprites:
            print('Collision with sprite', 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\$

You must log in to answer this question.

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