旋转角色和精灵墙

Rotating character and sprite wall

我有一个代表我角色的精灵。这个精灵根据我的鼠标位置旋转每一帧,这反过来使得我的矩形根据鼠标的位置变大或变小。

基本上我想要的是让我的 sprite (Character) 不会进入 sprite 墙。现在,由于墙壁的矩形比实际图片看起来要大,而且我的矩形会根据我的鼠标位置不断增大和缩小,这让我不知道如何发表声明来阻止我的精灵以令人信服的方式进入墙壁.

我已经确定我的 ColideList 只是应该碰撞的块。我找到了 Detecting collision of two sprites that can rotate,但它在 Java 中,我不需要检查两个旋转精灵之间的碰撞,而是一个和一堵墙。

我的角色 class 看起来像这样:

class Character(pygame.sprite.Sprite):
    walking_frame = []
    Max_Hp = 100
    Current_HP = 100
    Alive = True
    X_Speed = 0
    Y_Speed = 0
    Loc_x = 370
    Loc_y = 430
    size = 15
    Current_Weapon = Weapon()
    Angle = 0
    reloading = False
    shot = False
    LastFrame = 0
    TimeBetweenFrames = 0.05
    frame = 0
    Walking = False
    Blocked = 0
    rel_path = "Sprite Images/All.png"
    image_file = os.path.join(script_dir, rel_path)
    sprite_sheet = SpriteSheet(image_file) #temp
    image = sprite_sheet.get_image(0, 0, 48, 48) #Temp
    image = pygame.transform.scale(image, (60, 60))
    orgimage = image
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.walking_frame.append(self.image)
        image = self.sprite_sheet.get_image(48, 0, 48, 48)
        self.walking_frame.append(image)
        image = self.sprite_sheet.get_image(96, 0, 48, 48)
        self.walking_frame.append(image)
        image = self.sprite_sheet.get_image(144, 0, 48, 48)
        self.walking_frame.append(image)
        image = self.sprite_sheet.get_image(0, 48, 48, 48)
        self.walking_frame.append(image)
        image = self.sprite_sheet.get_image(48, 48, 48, 48)
        self.walking_frame.append(image)
        image = self.sprite_sheet.get_image(96, 48, 48, 48)
        self.walking_frame.append(image)
        image = self.sprite_sheet.get_image(144, 48, 48, 48)
        self.walking_frame.append(image)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = [self.Loc_x,self.Loc_y]
        print "Shabat Shalom"
    def Shoot(self):
        if self.Alive:
            if(self.reloading == False):
                if(self.Current_Weapon.Clip_Ammo > 0):
                    bullet = Bullet(My_Man)
                    bullet_list.add(bullet)
                    self.Current_Weapon.Clip_Ammo -= 1
    def move(self):
        if self.Alive:
            self.Animation()

            self.Loc_x += self.X_Speed
            self.Loc_y += self.Y_Speed
            Wall_hit_List = pygame.sprite.spritecollide(My_Man, CollideList, False)
            self.Blocked = 0
            for wall in Wall_hit_List:
                if self.rect.right <= wall.rect.left and self.rect.right >= wall.rect.right:
                    self.Blocked = 1 #right
                    self.X_Speed= 0
                elif self.rect.left <= wall.rect.right and self.rect.left >= wall.rect.left:
                    self.Blocked = 3 #Left
                    self.X_Speed = 0
                elif self.rect.top <= wall.rect.bottom and self.rect.top >= wall.rect.top:
                    self.Blocked = 2 #Up
                    self.Y_Speed = 0
                elif self.rect.top >= wall.rect.bottom and self.rect.top <= wall.rect.top:
                    self.Blocked = 4 #Down
                    self.Y_Speed = 0
            self.image = pygame.transform.rotate(self.orgimage, self.Angle)
            self.rect = self.image.get_rect()
            self.rect.left, self.rect.top = [self.Loc_x, self.Loc_y]
    def Animation(self):
    #      #Character Walk Animation
        if self.X_Speed != 0 or self.Y_Speed != 0:
            if(self.Walking == False):
                self.LastFrame = time.clock()
                self.Walking = True
                if (self.frame < len(self.walking_frame)):
                    self.image = self.walking_frame[self.frame]
                    self.image = pygame.transform.scale(self.image, (60, 60))
                    self.orgimage = self.image
                    self.frame += 1
                else:
                    self.frame = 0
        else:
            if self.frame != 0:
                self.frame = 0
                self.image = self.walking_frame[self.frame]
                self.image = pygame.transform.scale(self.image, (60, 60))
                self.orgimage = self.image
        if self.Walking and time.clock() - self.LastFrame > self.TimeBetweenFrames:
            self.Walking = False
    def CalAngle(self,X,Y):
        angle = math.atan2(self.Loc_x - X, self.Loc_y - Y)
        self.Angle = math.degrees(angle) + 180

我的墙 class 看起来像这样:

class Wall(pygame.sprite.Sprite):
    def __init__(self, PosX, PosY, image_file, ImageX,ImageY):
        pygame.sprite.Sprite.__init__(self)
        self.sprite_sheet = SpriteSheet(image_file)
        self.image = self.sprite_sheet.get_image(ImageX, ImageY, 64, 64)
        self.image = pygame.transform.scale(self.image, (32, 32))
        self.image.set_colorkey(Black)
        self.rect = self.image.get_rect()
        self.rect.x = PosX
        self.rect.y = PosY

我的 BuildWall 函数如下所示:

def BuildWall(NumberOfBlocks,TypeBlock,Direction,X,Y,Collide):
    for i in range(NumberOfBlocks):
        if Direction == 1:
            wall = Wall(X + (i * 32), Y, spriteList, 0, TypeBlock)
            wall_list.add(wall)
        if Direction == 2:
            wall = Wall(X - (i * 32), Y, spriteList, 0, TypeBlock)
            wall_list.add(wall)
        if Direction == 3:
            wall = Wall(X, Y + (i * 32), spriteList, 0, TypeBlock)
            wall_list.add(wall)
        if Direction == 4:
            wall = Wall(X, Y - (i * 32), spriteList, 0, TypeBlock)
            wall_list.add(wall)
        if(Collide):
            CollideList.add(wall)

最后我的步行活动是这样的:

elif event.type == pygame.KEYDOWN:
        if event.key == pygame.K_ESCAPE: #Press escape also leaves game
            Game = False
        elif event.key == pygame.K_w and My_Man.Blocked != 2:
            My_Man.Y_Speed = -3
        elif event.key == pygame.K_s and My_Man.Blocked != 4:
            My_Man.Y_Speed = 3
        elif event.key == pygame.K_a and My_Man.Blocked != 3:
            My_Man.X_Speed = -3
        elif event.key == pygame.K_d and My_Man.Blocked != 1:
            My_Man.X_Speed = 3
        elif event.key == pygame.K_r and (My_Man.reloading == False):
            lastReloadTime = time.clock()
            My_Man.reloading = True
            if (My_Man.Current_Weapon.Name == "Pistol"):
                My_Man.Current_Weapon.Clip_Ammo = My_Man.Current_Weapon.Max_Clip_Ammo
            else:
                My_Man.Current_Weapon.Clip_Ammo, My_Man.Current_Weapon.Max_Ammo = Reload(My_Man.Current_Weapon.Max_Ammo,My_Man.Current_Weapon.Clip_Ammo,My_Man.Current_Weapon.Max_Clip_Ammo)
    elif event.type == pygame.KEYUP:
        if event.key == pygame.K_w:
            My_Man.Y_Speed = 0
        elif event.key == pygame.K_s:
            My_Man.Y_Speed = 0
        elif event.key == pygame.K_a:
            My_Man.X_Speed = 0
        elif event.key == pygame.K_d:
            My_Man.X_Speed = 0

这完全取决于您的 sprite 外观以及您想要的结果。我相信有 3 种不同类型的碰撞检测可以适用于您的场景。

防止您的矩形调整大小

由于图像在旋转时会变大,您可以通过移除多余的填充并保持图像的原始大小来进行补偿。

假设原图尺寸为32像素宽32像素高。旋转后,图像为 36 像素宽和 36 像素高。我们想去掉图像的中心(因为在它周围添加了填充)。

要取出新图像的中心,我们只需取出图像的次表面,其大小与之前位于图像中心的矩形相同。

def rotate(self, degrees):
    self.rotation = (self.rotation + degrees) % 360  # Keep track of the current rotation.
    self.image = pygame.transform.rotate(self.original_image, self.rotation))

    center_x = self.image.get_width() // 2
    center_y = self.image.get_height() // 2
    rect_surface = self.rect.copy()  # Create a new rectangle.
    rect_surface.center = (center_x, center_y)  # Move the new rectangle to the center of the new image.
    self.image = self.image.subsurface(rect_surface)  # Take out the center of the new image.

由于矩形的大小没有改变,我们不需要做任何事情来重新计算它(换句话说:self.rect = self.image.get_rect() 将不是必需的)。

矩形检测

从这里开始,您只需像往常一样使用 pygame.sprite.spritecollide(或者如果您有自己的函数)。

def collision_rect(self, walls):
    last = self.rect.copy()  # Keep track on where you are.
    self.rect.move_ip(*self.velocity)  # Move based on the objects velocity.
    current = self.rect  # Just for readability we 'rename' the objects rect attribute to 'current'.
    for wall in pygame.sprite.spritecollide(self, walls, dokill=False):
        wall = wall.rect  # Just for readability we 'rename' the wall's rect attribute to just 'wall'.
        if last.left >= wall.right > current.left:  # Collided left side.
            current.left = wall.right
        elif last.right <= wall.left < current.right:  # Collided right side.
            current.right = wall.left
        elif last.top >= wall.bottom > current.top:  # Collided from above.
            current.top = wall.bottom
        elif last.bottom <= wall.top < current.bottom:  # Collided from below.
            current.bottom = wall.top

循环碰撞

如果您正在铺设墙壁,这可能不会发挥最佳作用,因为您可以根据墙壁的大小和您的角色在瓷砖之间穿梭。它对许多其他事情都有好处,所以我会保留它。

如果您将属性 radius 添加到您的播放器和墙,您可以使用 pygame.sprite.spritecollide 并传递回调函数 pygame.sprite.collide_circle。您不需要 radius 属性,它是可选的。但如果你不这样做,pygame 将根据 sprites 的 rect 属性计算半径,除非半径不断变化,否则这是不必要的。

def collision_circular(self, walls):
    self.rect.move_ip(*self.velocity)
    current = self.rect
    for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_circle):
        distance = self.radius + wall.radius
        dx = current.centerx - wall.rect.centerx
        dy = current.centery - wall.rect.centery
        multiplier = ((distance ** 2) / (dx ** 2 + dy ** 2)) ** (1/2)
        current.centerx = wall.rect.centerx + (dx * multiplier)
        current.centery = wall.rect.centery + (dy * multiplier)

像素完美碰撞

这是最难实现的,而且性能很重,但可以给你最好的结果。我们仍然会使用 pygame.sprite.spritecollide,但这次我们将传递 pygame.sprite.collide_mask 作为回调函数。此方法要求您的精灵具有矩形属性和每像素 alpha 表面或具有颜色键的表面。

掩码属性是可选的,如果有 none 函数将临时创建一个。如果您使用掩码属性,则每次更改精灵图像时都需要更改更新它。

这种碰撞的难点不是检测它,而是正确响应并使其 move/stop 适当。我做了一个有问题的例子,演示了一种处理它的方法。

def collision_mask(self, walls):
    last = self.rect.copy()
    self.rect.move_ip(*self.velocity)
    current = self.rect
    for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_mask):
        if not self.rect.center == last.center:
            self.rect.center = last.center
            break
        wall = wall.rect
        x_distance = current.centerx - wall.centerx
        y_distance = current.centery - wall.centery
        if abs(x_distance) > abs(y_distance):
            current.centerx += (x_distance/abs(x_distance)) * (self.velocity[0] + 1)
        else:
            current.centery += (y_distance/abs(y_distance)) * (self.velocity[1] + 1)

完整代码

您可以尝试不同的例子,按 1 表示矩形碰撞,按 2 表示圆形碰撞,按 3 表示像素完美碰撞。有些地方有点小问题,机芯不是一流的,性能也不是很理想,但这只是一个简单的演示。

import pygame
pygame.init()

SIZE = WIDTH, HEIGHT = (256, 256)
clock = pygame.time.Clock()
screen = pygame.display.set_mode(SIZE)
mode = 1
modes = ["Rectangular collision", "Circular collision", "Pixel perfect collision"]


class Player(pygame.sprite.Sprite):

    def __init__(self, pos):
        super(Player, self).__init__()
        self.original_image = pygame.Surface((32, 32))
        self.original_image.set_colorkey((0, 0, 0))
        self.image = self.original_image.copy()
        pygame.draw.ellipse(self.original_image, (255, 0, 0), pygame.Rect((0, 8), (32, 16)))

        self.rect = self.image.get_rect(center=pos)
        self.rotation = 0
        self.velocity = [0, 0]
        self.radius = self.rect.width // 2
        self.mask = pygame.mask.from_surface(self.image)

    def rotate_clipped(self, degrees):
        self.rotation = (self.rotation + degrees) % 360  # Keep track of the current rotation
        self.image = pygame.transform.rotate(self.original_image, self.rotation)

        center_x = self.image.get_width() // 2
        center_y = self.image.get_height() // 2
        rect_surface = self.rect.copy()  # Create a new rectangle.
        rect_surface.center = (center_x, center_y)  # Move the new rectangle to the center of the new image.
        self.image = self.image.subsurface(rect_surface)  # Take out the center of the new image.

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

    def collision_rect(self, walls):
        last = self.rect.copy()  # Keep track on where you are.
        self.rect.move_ip(*self.velocity)  # Move based on the objects velocity.
        current = self.rect  # Just for readability we 'rename' the objects rect attribute to 'current'.
        for wall in pygame.sprite.spritecollide(self, walls, dokill=False):
            wall = wall.rect  # Just for readability we 'rename' the wall's rect attribute to just 'wall'.
            if last.left >= wall.right > current.left:  # Collided left side.
                current.left = wall.right
            elif last.right <= wall.left < current.right:  # Collided right side.
                current.right = wall.left
            elif last.top >= wall.bottom > current.top:  # Collided from above.
                current.top = wall.bottom
            elif last.bottom <= wall.top < current.bottom:  # Collided from below.
                current.bottom = wall.top

    def collision_circular(self, walls):
        self.rect.move_ip(*self.velocity)
        current = self.rect
        for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_circle):
            distance = self.radius + wall.radius
            dx = current.centerx - wall.rect.centerx
            dy = current.centery - wall.rect.centery
            multiplier = ((distance ** 2) / (dx ** 2 + dy ** 2)) ** (1/2)
            current.centerx = wall.rect.centerx + (dx * multiplier)
            current.centery = wall.rect.centery + (dy * multiplier)

    def collision_mask(self, walls):
        last = self.rect.copy()
        self.rect.move_ip(*self.velocity)
        current = self.rect
        for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_mask):
            if not self.rect.center == last.center:
                self.rect.center = last.center
                break
            wall = wall.rect
            x_distance = current.centerx - wall.centerx
            y_distance = current.centery - wall.centery
            if abs(x_distance) > abs(y_distance):
                current.centerx += (x_distance/abs(x_distance)) * (self.velocity[0] + 1)
            else:
                current.centery += (y_distance/abs(y_distance)) * (self.velocity[1] + 1)

    def update(self, walls):
        self.rotate_clipped(1)

        if mode == 1:
            self.collision_rect(walls)
        elif mode == 2:
            self.collision_circular(walls)
        else:
            self.collision_mask(walls)


class Wall(pygame.sprite.Sprite):

    def __init__(self, pos):
        super(Wall, self).__init__()
        size = (32, 32)
        self.image = pygame.Surface(size)
        self.image.fill((0, 0, 255))  # Make the Surface blue.
        self.image.set_colorkey((0, 0, 0))  # Will not affect the image but is needed for collision with mask.
        self.rect = pygame.Rect(pos, size)

        self.radius = self.rect.width // 2
        self.mask = pygame.mask.from_surface(self.image)


def show_rects(player, walls):
    for wall in walls:
        pygame.draw.rect(screen, (1, 1, 1), wall.rect, 1)
    pygame.draw.rect(screen, (1, 1, 1), player.rect, 1)


def show_circles(player, walls):
    for wall in walls:
        pygame.draw.circle(screen, (1, 1, 1), wall.rect.center, wall.radius, 1)
    pygame.draw.circle(screen, (1, 1, 1), player.rect.center, player.radius, 1)


def show_mask(player, walls):
    for wall in walls:
        pygame.draw.rect(screen, (1, 1, 1), wall.rect, 1)
    for pixel in player.mask.outline():
        pixel_x = player.rect.x + pixel[0]
        pixel_y = player.rect.y + pixel[1]
        screen.set_at((pixel_x, pixel_y), (1, 1, 1))

# Create walls around the border.
walls = pygame.sprite.Group()
walls.add(Wall(pos=(col, 0)) for col in range(0, WIDTH, 32))
walls.add(Wall(pos=(0, row)) for row in range(0, HEIGHT, 32))
walls.add(Wall(pos=(col, HEIGHT - 32)) for col in range(0, WIDTH, 32))
walls.add(Wall(pos=(WIDTH - 32, row)) for row in range(0, HEIGHT, 32))
walls.add(Wall(pos=(WIDTH//2, HEIGHT//2)))  # Obstacle in the middle of the screen

player = Player(pos=(64, 64))
speed = 2  # Speed of the player.
while True:
    screen.fill((255, 255, 255))
    clock.tick(60)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            quit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_a:
                player.velocity[0] = -speed
            elif event.key == pygame.K_d:
                player.velocity[0] = speed
            elif event.key == pygame.K_w:
                player.velocity[1] = -speed
            elif event.key == pygame.K_s:
                player.velocity[1] = speed
            elif pygame.K_1 <= event.key <= pygame.K_3:
                mode = event.key - 48
                print(modes[mode - 1])
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_a or event.key == pygame.K_d:
                player.velocity[0] = 0
            elif event.key == pygame.K_w or event.key == pygame.K_s:
                player.velocity[1] = 0

    player.update(walls)
    walls.draw(screen)
    screen.blit(player.image, player.rect)

    if mode == 1:
        show_rects(player, walls)  # Show rectangles for circular collision detection.
    elif mode == 2:
        show_circles(player, walls)  # Show circles for circular collision detection.
    else:
        show_mask(player, walls)  # Show mask for pixel perfect collision detection.

    pygame.display.update()

最后一个音符

在进一步编程之前,您确实需要重构代码。我试图阅读您的一些代码,但它真的很难理解。尝试关注 Python's naming conventions,这将使其他程序员更容易阅读和理解您的代码,从而使他们更容易帮助您解决问题。

只要遵循这些简单的指导方针,您的代码就可以提高可读性:

  • 变量名只能包含小写字母。超过 1 个单词的名称应该用下划线分隔。示例:variablevariable_with_words.
  • 函数和属性应遵循与变量相同的命名约定。
  • Class 名称的每个单词应以大写字母开头,其余部分应为小写字母。示例:ClassMyClass。被称为 CamelCase。
  • 类 中的方法用一行分隔,函数和 类 中的方法用两行分隔。

我不知道你用的是哪种IDE,但是Pycharm Community Edition对Python来说是一个很棒的IDE。当您打破 Python 惯例(当然还有更多)时,它会告诉您。

请务必注意,这些是约定而非规则。它们旨在使代码更具可读性,而不是严格遵守。如果您认为可以提高可读性,请打破它们。