Sprite Is Not Rotating Towards The Player Properly How Do I Fix This?

Viewed 176

I am trying to make my sprites rotate towards the player, but the rotation is out of place. Here is a video of the behaviour.

I am not sure how to fix this,

Sprite class

  #-------------------------------- enemy shoots left and right

    shotsright = pygame.image.load("canss.png")
    class enemyshoot:
        def __init__(self,x,y,height,width,color):
            self.x = x
            self.y =y
            self.height = height
            self.width = width
            self.color = color
            self.rect = pygame.Rect(x,y,height,width)
            self.health = 10
            self.hitbox = (self.x + -20, self.y + 30, 31, 57)
           #-------------------------------------------------------
            # Make a Reference Copy of the bitmap for later rotation
            self.shootsright = pygame.image.load("canss.png")
            self.shootsright = pygame.transform.scale(self.shootsright,(self.shootsright.get_width()-150,self.shootsright.get_height()-150))            
            self.image    = self.shootsright
            self.rect     = self.image.get_rect()
            self.position = pygame.math.Vector2( (200, 180) )
            self.isLookingAtPlayer = False
        def draw(self):
            self.rect.topleft = (self.x,self.y)
            window.blit(self.image, self.rect)
            self.hits = (self.x + 20, self.y, 28,60)
            pygame.draw.rect(window, (255,0,0), (self.hitbox[0], self.hitbox[1] - 60, 100, 10)) # NEW
            pygame.draw.rect(window, (0,255,0), (self.hitbox[0], self.hitbox[1] - 60, 100 - (5 * (10 - self.health)), 10))
            self.hitbox = (self.x + 200, self.y + 200, 51, 65)
        def lookAt( self, coordinate ):
            # Rotate image to point in the new direction
            delta_vector  = coordinate - self.position
            radius, angle = delta_vector.as_polar()
            self.image    = pygame.transform.rotate(self.shootsright, -angle)
            # Re-set the bounding rectangle and position since 
            # the dimensions and centroid will have (probably) changed.
            current_pos      = self.rect.center
            self.rect        = self.image.get_rect()
            self.rect.center = current_pos
            


            
    black = (0,0,0)
    enemyshooting = []
    platformGroup = pygame.sprite.Group
    platformList = []
    level = ["                                                                                                                     p               p           p                         p                        p        ",
             "                                       ",
             "                             ",
             "                                      ",
             "                                  ",
             "                           ",
             "                                      ",
             "                                      ",
             "                                    ",
             "                                   ",
             "                    ",]
    for iy, row in enumerate(level):
        for ix, col in enumerate(row):
            if col == "p":
                new_platforms = enemyshoot(ix*10, iy*50, 10,10,(255,255,255))
                enemyshooting.append(new_platforms)

this is the rotating part


class enemyshoot:
     def __init__(self,x,y,height,width,color):
       # [................]
           #-------------------------------------------------------
            # Make a Reference Copy of the bitmap for later rotation
            self.shootsright = pygame.image.load("canss.png")
            self.shootsright = pygame.transform.scale(self.shootsright,(self.shootsright.get_width()-150,self.shootsright.get_height()-150))            
            self.image    = self.shootsright
            self.rect     = self.image.get_rect()
            self.position = pygame.math.Vector2( (200, 180) )      


        def lookAt( self, coordinate ):
            # Rotate image to point in the new direction
            delta_vector  = coordinate - pygame.math.Vector2(self.rect.center)
            radius, angle = delta_vector.as_polar()
            self.image    = pygame.transform.rotate(self.shootsright, -angle)
            # Re-set the bounding rectangle and position since 
            # the dimensions and centroid will have (probably) changed.
            current_pos      = self.rect.center
            self.rect        = self.image.get_rect()
            self.rect.center = current_pos



This is where I call the LookAt function to face the player:

      # so instead of this 
            for enemyshoot in enemyshooting:
                if not enemyshoot.isLookingAtPlayer:
                    enemyshoot.lookAt((playerman.x, playerman.y)) 

The rotation is out of place, and I can't figure out how to fix it. I am trying to make the mouth of the cannon rotate towards the player because that's where the bullets will append from.

2 Answers

self.position is set in the constructor, but it is not updated. Use the self.rect.center to compute the direction vector:

delta_vector = coordinate - pygame.math.Vector2(self.rect.center)

Use math.atan2 to compute the angle of rotation:
(See How do I make my player rotate towards mouse position?)

angle = (180 / math.pi) * math.atan2(-delta_vector.x, -delta_vector.y)

Setting the rectangle position in draw struggles with rotating the sprite in lookAt. Note the size of the rotated rectangle is enlarged. See How do I rotate an image around its center using Pygame?.

I recommend to set the look-at position in lookAt and to compute the rotated image in draw:

class enemyshoot:
    def __init__(self,x,y,height,width,color):
       # [...]

       self.look_at_pos = (x, y)

    def draw(self):
   
        self.rect = self.shootsright.get_rect(topleft = (self.x, self.y))

        dx = self.look_at_pos[0] - self.rect.centerx
        dy = self.look_at_pos[1] - self.rect.centery 
        angle = (180 / math.pi) * math.atan2(-dx, -dy)

        self.image = pygame.transform.rotate(self.shootsright, angle)
        self.rect  = self.image.get_rect(center = self.rect.center)

        window.blit(self.image, self.rect)
        self.hits = (self.x + 20, self.y, 28,60)
        pygame.draw.rect(window, (255,0,0), (self.hitbox[0], self.hitbox[1] - 60, 100, 10)) # NEW
        pygame.draw.rect(window, (0,255,0), (self.hitbox[0], self.hitbox[1] - 60, 100 - (5 * (10 - self.health)), 10))
        self.hitbox = (self.x + 200, self.y + 200, 51, 65)

    def lookAt(self, coordinate):
        self.look_at_pos = coordinate

I concur with everything @Rabbid76 says in the above answer.

I suspect part of your problem may be that the "human readable" part of the bitmap is not centered around the bitmap's centroid. Thus when rotated, it "sweeps" through an arc, rather than being rotated "around itself". (The preserving of the centre co-ordinate is an important step here to maintain a smooth rotation about the centroid of the object).

Consider the two bitmaps (the image on the right has a large 3/4 transparent section top-left):

centred image off-centred image

Both are rotated around their centroid, but since the visible part on the 2nd image is not centred, it rotates weirdly.

example video

So ensure your actual bitmap is centred within itself.

Reference Code:

import pygame
import random

# Window size
WINDOW_WIDTH    = 800
WINDOW_HEIGHT   = 400
WINDOW_SURFACE  = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE

DARK_BLUE = (   3,   5,  54 )

class RotationSprite( pygame.sprite.Sprite ):
    def __init__( self, image, x, y, ):
        pygame.sprite.Sprite.__init__(self)
        self.original = image
        self.image    = image
        self.rect     = self.image.get_rect()
        self.rect.center = ( x, y )
        # for maintaining trajectory
        self.position    = pygame.math.Vector2( ( x, y ) )
        self.velocity    = pygame.math.Vector2( ( 0, 0 ) )

    def lookAt( self, co_ordinate ):
        # Rotate image to point in the new direction
        delta_vector  = co_ordinate - self.position
        radius, angle = delta_vector.as_polar()
        self.image    = pygame.transform.rotozoom( self.original, -angle, 1 )
        # Re-set the bounding rectagle
        current_pos      = self.rect.center
        self.rect        = self.image.get_rect()
        self.rect.center = current_pos



### initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption( "Rotation Example" )

### Missiles!
rotation_sprites = pygame.sprite.Group()
rotation_image1 = pygame.image.load( 'rot_offcentre_1.png' )
rotation_image2 = pygame.image.load( 'rot_offcentre_2.png' )
rotation_sprites.add( RotationSprite( rotation_image1, WINDOW_WIDTH//3, WINDOW_HEIGHT//2 ) )
rotation_sprites.add( RotationSprite( rotation_image2, 2*( WINDOW_WIDTH//3 ), WINDOW_HEIGHT//2 ) )


### Main Loop
clock = pygame.time.Clock()
done = False
while not done:

    # Handle user-input
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            done = True
        elif ( event.type == pygame.MOUSEBUTTONUP ):
            # On mouse-click
            pass

    # Record mouse movements for positioning the paddle
    mouse_pos = pygame.mouse.get_pos()
    for m in rotation_sprites:
        m.lookAt( mouse_pos )
    rotation_sprites.update()

    # Update the window, but not more than 60 FPS
    window.fill( DARK_BLUE )
    rotation_sprites.draw( window )
    pygame.display.flip()

    # Clamp FPS
    clock.tick_busy_loop(60)

pygame.quit()
Related