Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
672 views
in Technique[技术] by (71.8m points)

python - How do I center a surface (subsurface) around a rectangle? (scaled sprite hitbox / collision rect)

Currently I have working code which will cycle through a spritesheet, adding each cell/image (9 in total) as a subsurface into a list. As the game updates, I am setting the Player image as the current cell in which the code has indexed by. Meanwhile, I also have a set rectangle which acts as the sprites 'hitbox'/collision rect.

However, setting the subsurface as the new image, I found that the sprite scales from the top-left corner of the collision rect. As the sprite is significantly larger than the collision rect, the collision rect is placed far away from the actual char model/sprite.

I am trying to center the subsurface/sprite image AROUND the collision rect, as opposed from scaling from the top-left corner.

Here is my code:

import pygame as pg
from settings import *
vec = pg.math.Vector2

class Civilian(pg.sprite.Sprite):
    def __init__(self, game, x, y):
        self.groups = game.all_sprites, game.player1group
        pg.sprite.Sprite.__init__(self, self.groups)
        self.game = game
        self.image = pg.Surface((TILESIZE, TILESIZE))
        self.rect = self.image.get_rect()
        self.vel = vec(0, 0)
        self.pos = vec(x , y)
        self.move = 0

    def animate(self, direction):
        if direction == 'right':
            self.spritesheet = pg.image.load('walk right.png') # Loading the right directional movement spritesheet into the variable
        if direction == 'left':
            self.spritesheet = pg.image.load('walk left.png')
        if direction == 'up':
            self.spritesheet = pg.image.load('walk up.png')
        if direction == 'down':
            self.spritesheet = pg.image.load('walk down.png')
        self.frames = [] # List which will contain each cell of the spritesheet
        # Adding the cells to the list #
        self.frames.append(self.spritesheet.subsurface(pg.Rect(0, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(61, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(122, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(183, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(244, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(305, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(366, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(427, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(488, 0, 61, 67)).convert_alpha())
        # Number of frames/cells
        self.frames_number = len(self.frames)
        # Current animation frame
        self.current_frame = 0
        # Frame rectangle
        self.frame_rect = self.frames[0].get_rect()


    def get_keys(self):
        self.vel= vec(0, 0)
        keys = pg.key.get_pressed()

        if keys[pg.K_a]:    # Const. subtracts player speed from velocity (E.g. Moves sprite to the left)
            self.vel.x= -PLAYER_SPEED
            self.move += 1
            self.moving = 'left' # Uses different spritesheet depending on direction
        elif keys[pg.K_d]:    # Const. adds player speed value to velocity (E.g. Moves sprite to the right)
            self.vel.x= PLAYER_SPEED
            self.move += 1
            self.moving = 'right'
        elif keys[pg.K_w]:    # Const. subtracts player speed value from y velocity (Moves player upwards; opposite)
            self.vel.y= -PLAYER_SPEED
            self.move += 1
            self.moving = 'up'
        elif keys[pg.K_s]: # Const. adds player speed value to y velocity (Moves player downwards; opposite)
            self.vel.y= PLAYER_SPEED
            self.move += 1
            self.moving = 'down'
        if self.vel.x != 0 and self.vel.y != 0:   # Offsetting increased vecocity when moving diagonally (Has both x and y velocity)
            self.vel *= 0.7071

    def collide_with_player2(self, dir, ifColliding):
        if dir == 'x':
            collides = pg.sprite.spritecollide(self, self.game.player2group, False)
            if collides:
                if self.vel.x > 0:
                    self.pos.x = collides[0].rect.left - self.rect.width
                if self.vel.x < 0:
                    self.pos.x = collides[0].rect.right
                self.vel.x = 0
                self.rect.x = self.pos.x
                print("collide x")
                self.ifColliding = True

        if dir == 'y':
            collides = pg.sprite.spritecollide(self, self.game.player2group, False)
            if collides:
                if self.vel.y > 0:
                    self.pos.y = collides[0].rect.top - self.rect.height
                if self.vel.y < 0:
                    self.pos.y = collides[0].rect.bottom
                self.vel.y = 0
                self.rect.y = self.pos.y
                print("collide y")
                self.ifColliding = True



    def collide_with_walls(self, dir):
        if dir == 'x':
            collides = pg.sprite.spritecollide(self, self.game.walls, False)
            if collides:
                if self.vel.x > 0:
                    self.pos.x = collides[0].rect.left - self.rect.width
                if self.vel.x < 0:
                    self.pos.x = collides[0].rect.right
                self.vel.x = 0
                self.rect.x = self.pos.x
        if dir == 'y':
            collides = pg.sprite.spritecollide(self, self.game.walls, False)
            if collides:
                if self.vel.y > 0:
                    self.pos.y = collides[0].rect.top - self.rect.height
                if self.vel.y < 0:
                    self.pos.y = collides[0].rect.bottom
                self.vel.y = 0
                self.rect.y = self.pos.y





    def update(self):
        # frame updates
        self.moving = 'idle'
        self.animate('down') # Sets the down spritesheet as default
        self.get_keys()
        if self.moving == 'up':
            self.animate(self.moving) # Uses the up-movement spritesheet if char moving upwards
        if self.moving == 'down':
            self.animate(self.moving) # Same as above, different direction
        if self.moving == 'left':
            self.animate(self.moving)
        if self.moving == 'right':
            self.animate(self.moving)
        # frame updates
        self.ifColliding = False

        self.pos += self.vel * self.game.dt
        self.rect.x = self.pos.x
        self.collide_with_walls('x'), self.collide_with_player2('x', self.ifColliding)
        self.rect.y = self.pos.y
        self.collide_with_walls('y'), self.collide_with_player2('y', self.ifColliding)
        if self.ifColliding == True:
            Thief.health -= COL_DAMAGE
            print(Thief.health)
        self.current_frame = (self.current_frame + self.move) % self.frames_number
        if self.moving == 'idle':
            self.current_frame = 0
        self.image = self.frames[self.current_frame] # Image of sprite changes as program cycles through the sheet

In short, Id like to center the self.image surface on the self.rect (Collision rect).

[EDIT]

I have attempted to change references to self.rect within the colliion functions (collide_with_player2, collide_with_walls) to self.col_rect in hopes this would work, but found this not to be the case.

As stated I have created the new rectangle i'd like to use for collision so that self.rect is used for the image blitting, and self.col_rect is used for collision. Although inefficient, I would still like to allow for this as a temporary fix to the problem. I am new to pygame, so I was hoping someone could help me in changing the rectangle used in collision from self.rect, to self.col_rect instead. Again, any feedback would be greatly appreciated!

Updated code:

import pygame as pg
from settings import *
vec = pg.math.Vector2

class Civilian(pg.sprite.Sprite):
    def __init__(self, game, x, y):
        self.groups = game.all_sprites, game.player1group, game.bothplayers
        pg.sprite.Sprite.__init__(self, self.groups)
        self.game = game
        self.image = pg.Surface((61, 67))
        self.rect = self.image.get_rect()
        self.col_rect = self.rect.inflate(-40, -40)
        self.vel = vec(0, 0)
        self.pos = vec(x , y)
        self.move = 0

    def animate(self, direction):
        if direction == 'right':
            self.spritesheet = pg.image.load('walk right civ.png') # Loading the right directional movement spritesheet into the variable
        if direction == 'left':
            self.spritesheet = pg.image.load('walk left civ.png')
        if direction == 'up':
            self.spritesheet = pg.image.load('walk up civ.png')
        if direction == 'down':
            self.spritesheet = pg.image.load('walk down civ.png')
        self.frames = [] # List which will contain each cell of the spritesheet
        # Adding the cells to the list #
        self.frames.append(self.spritesheet.subsurface(pg.Rect(0, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(61, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(122, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(183, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(244, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(305, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(366, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(427, 0, 61, 67)).convert_alpha())
        self.frames.append(self.spritesheet.subsurface(pg.Rect(488, 0, 61, 67)).convert_alpha())
        # Number of frames/cells
        self.frames_number = len(self.frames)
        # Current animation frame
        self.current_frame = 0
        # Frame rectangle
        self.frame_rect = self.frames[0].get_rect()


    def get_keys(self):
        self.vel= vec(0, 0)
        keys = pg.key.get_pressed()

        if keys[pg.K_a]:    # Const. subtracts player speed from velocity (E.g. Moves sprite to the left)
            self.vel.x= -PLAYER_SPEED
            self.move += 1
            self.moving = 'left' # Uses different spritesheet depending on direction
        elif keys[pg.K_d]:    # Const. adds player speed value to velocity (E.g. Moves sprite to the right)
            self.vel.x= PLAYER_SPEED
            self.move += 1
            self.moving = 'right'
        elif keys[pg.K_w]:    # Const. subtracts player speed value from y velocity (Moves player upwards; opposite)
            self.vel.y= -PLAYER_SPEED
            self.move += 1
            self.moving = 'up'
        elif keys[pg.K_s]: # Const. adds player speed value to y velocity (Moves player downwards; opposite)
            self.vel.y= PLAYER_SPEED
            self.move += 1
            self.moving = 'down'
        if self.vel.x != 0 and self.vel.y != 0:   # Offsetting increased vecocity when moving diagonally (Has both x and y velocity)
            self.vel *= 0.7071

    def collide_with_player2(self, dir, ifColliding):
        if dir == 'x':
            collides = pg.sprite.spritecollide(self, self.game.player2group, False)
            if collides:
                if self.vel.x > 0:
                    self.pos.x = collides[0].rect.left - self.rect.width
                if self.vel.x < 0:
                    s

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

If you want a scaled collision rect/hitbox, you need to give your sprites a second rect (I call it hitbox here). You have to do that because pygame blits the images/surfaces at the topleft coords of the self.rect. So the first rect self.rect serves as the blit position and the self.hitbox is used for the collision detection.

You also need to define a custom callback function for the collision detection that you have to pass to pygame.sprite.spritecollide as the fourth argument.

def collided(sprite, other):
    """Check if the `hitbox` rects of the two sprites collide."""
    return sprite.hitbox.colliderect(other.hitbox)

collided_sprites = pg.sprite.spritecollide(player, enemies, False, collided)

Here's a complete example (the self.rects are the green rectangles and the self.hitboxes are the reds):

import pygame as pg
from pygame.math import Vector2


class Entity(pg.sprite.Sprite):

    def __init__(self, pos, *groups):
        super().__init__(*groups)
        self.image = pg.Surface((70, 50))
        self.image.fill((0, 80, 180))
        self.rect = self.image.get_rect(center=pos)
        # A inflated copy of the rect as the hitbox.
        self.hitbox = self.rect.inflate(-42, -22)
        self.vel = Vector2(0, 0)
        self.pos = Vector2(pos)

    def update(self):
        self.pos += self.vel
        self.rect.center = self.pos
        self.hitbox.center = self.pos  # Also update the hitbox coords.


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)
    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', 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__':
    pg.init()
    main()
    pg.quit()

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...