4

I'm having trouble with moving sprites. I can move them in the x and y axis with no problem at all. What I can't figure out is how can I move them according to a certain angle. What I mean is, I'm trying to create a function which includes the object I'm trying to move, its speed, and its direction(which should be measured in degrees). Something like:

MovingObject(obj,speed,direction) #this is the function I'm trying to define

It's more like a "spawning function" rather than just movement... Take for example that I would like to create an object of the class "Bullet" and want it to follow certain direction (different from the x and y axis, of course) Actually I have no clear idea of how to do such thing and I would like some advice in order to achieve so. Thanks for reading this!

EDIT: @Joran Beasley I tried to do what you told me...but I guess I did it wrong...

import pygame, math, time
screen=pygame.display.set_mode((320,240))
clock=pygame.time.Clock()
pygame.init()
def calculate_new_xy(old_xy,speed,angle_in_radians):
    new_x = old_xy.x + (speed*math.cos(angle_in_radians))
    new_y = old_xy.y + (speed*math.sin(angle_in_radians))
    return new_x, new_y
class Bullet(pygame.sprite.Sprite):
    def __init__(self,x,y,direction,speed):        
            pygame.sprite.Sprite.__init__(self)
            self.image=pygame.Surface((16, 16))
            self.image.fill((255,0,0))
            self.rect=self.image.get_rect()
            self.rect.center=(x,y)
            self.direction=math.radians(direction)
            self.speed=speed
    def update(self):
            self.rect.center=calculate_new_xy(self.rect,self.speed,self.direction)
spr=pygame.sprite.Group()
bullet=Bullet(160,120,45,1); spr.add(bullet)
play=True
while play:
    clock.tick(60)
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            play=False
    screen.fill((0,0,0))
    spr.update()
    spr.draw(screen)
    pygame.display.flip()
pygame.quit()

The object moves but... not in the specified direction...

DarkBlood202
  • 55
  • 1
  • 1
  • 6
  • change it to `calculate_new_xy(self.rect.center,...)` and then access x as [0] and y as [1] and set your speed to 2 ... it should work – Joran Beasley Oct 12 '17 at 00:04

3 Answers3

6

you just need a little basic trig

def calculat_new_xy(old_xy,speed,angle_in_radians):
    new_x = old_xy.X + (speed*math.cos(angle_in_radians))
    new_y = old_xy.Y + (speed*math.sin(angle_in_radians))
    return new_x, new_y

--- edit ---

Here is your code from above edited to work

import pygame, math, time
screen=pygame.display.set_mode((320,240))
clock=pygame.time.Clock()
pygame.init()
def calculate_new_xy(old_xy,speed,angle_in_radians):
    new_x = old_xy[0] + (speed*math.cos(angle_in_radians))
    new_y = old_xy[1] + (speed*math.sin(angle_in_radians))
    return new_x, new_y
class Bullet(pygame.sprite.Sprite):
    def __init__(self,x,y,direction,speed):
            pygame.sprite.Sprite.__init__(self)
            self.image=pygame.Surface((16, 16))
            self.image.fill((255,0,0))
            self.rect=self.image.get_rect()
            self.rect.center=(x,y)
            self.direction=math.radians(direction)
            self.speed=speed
    def update(self):
            self.rect.center=calculate_new_xy(self.rect.center,self.speed,self.direction)
spr=pygame.sprite.Group()
bullet=Bullet(160,120,45,2); spr.add(bullet)
play=True
while play:
    clock.tick(60)
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            play=False
    screen.fill((0,0,0))
    spr.update()
    spr.draw(screen)
    pygame.display.flip()
pygame.quit()
Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
  • Thanks! I don't remember learning this in school... Do you know what this update equation is called? Or maybe the basic key words associated with it? For some reason Im having a hard time finding where this update equation comes from.. – Brett Young Oct 20 '22 at 15:01
  • `trigonometry` i guess is what you want to search ... `cos(angle)` gives an x component, `sin(angle)` gives a y component ... – Joran Beasley Oct 20 '22 at 16:13
2

If you are using Pygame, I suggest to use pygame.math.Vector2 for this task. Set a direction vector from its Polar coordinates (speed, angle_in_degrees) with from_polar(). Add this vector to the current position of the bullet. This is more comprehensible than using sin and cos:

def calculate_new_xy(old_xy, speed, angle_in_degrees):
    move_vec = pygame.math.Vector2()
    move_vec.from_polar((speed, angle_in_degrees))
    return old_xy + move_vec

Be aware, that a pygame.Rect can only store integer coordinates. This is because a pygame.Rect is supposed to represent an area on the screen:

The coordinates for Rect objects are all integers. [...]

If you want the bullet to go straight in a certain direction at an angle that is not divisible by 45 °, you must store object positions with floating point accuracy. You have to store the location of the object in separate variables respectively attributes and to synchronize the pygame.Rect object. round the coordinates and assign it to the location (e.g. .topleft) of the rectangle:

class Bullet(pygame.sprite.Sprite):
    def __init__(self, x, y, direction, speed):
            # [...]

            self.rect = self.image.get_rect(center = (x, y))
            self.pos = (x, y)

    def update(self, screen):
            self.pos = calculate_new_xy(self.pos, self.speed, -self.direction)
            self.rect.center = round(self.pos[0]), round(self.pos[1])

See also Move towards target


Minimal example

import pygame

def calculate_new_xy(old_xy, speed, angle_in_degrees):
    move_vec = pygame.math.Vector2()
    move_vec.from_polar((speed, angle_in_degrees))
    return old_xy + move_vec

class Bullet(pygame.sprite.Sprite):
    def __init__(self, x, y, direction, speed):
            pygame.sprite.Sprite.__init__(self)
            self.image = pygame.Surface((16, 8), pygame.SRCALPHA)
            self.image.fill((255, 0, 0))
            self.image = pygame.transform.rotate(self.image, direction)
            self.rect = self.image.get_rect(center = (x, y))
            self.pos = (x, y)
            self.direction = direction
            self.speed = speed
    def update(self, screen):
            self.pos = calculate_new_xy(self.pos, self.speed, -self.direction)
            self.rect.center = round(self.pos[0]), round(self.pos[1])
            if not screen.get_rect().colliderect(self.rect):
                self.kill()

pygame.init()
screen = pygame.display.set_mode((320,240))
clock = pygame.time.Clock()
spr = pygame.sprite.Group()
play = True
frame_count = 0
while play:
    clock.tick(60)
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            play = False
    
    spr.update(screen)
    if (frame_count % 10) == 0:
        spr.add(Bullet(*screen.get_rect().center, frame_count, 2))
    frame_count += 1

    screen.fill((0,0,0))
    spr.draw(screen)
    pygame.draw.circle(screen, (64, 128, 255), screen.get_rect().center, 10)
    pygame.display.flip()

pygame.quit()
exit()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
1

I recommend to use vectors. To get the velocity, rotate the start direction vector Vector2(1, 0) by the angle and multiply it by the desired speed. Then just add this velocity vector to the position vector in the update method and also update the rect position to move the sprite. (Press 'a' or 'd' to rotate, mouse 1 or space to shoot.)

import pygame as pg
from pygame.math import Vector2


pg.init()
screen = pg.display.set_mode((640, 480))
screen_rect = screen.get_rect()

FONT = pg.font.Font(None, 24)
BULLET_IMAGE = pg.Surface((20, 11), pg.SRCALPHA)
pg.draw.polygon(BULLET_IMAGE, pg.Color('aquamarine1'),
                [(0, 0), (20, 5), (0, 11)])


class Bullet(pg.sprite.Sprite):

    def __init__(self, pos, angle):
        super().__init__()
        self.image = pg.transform.rotate(BULLET_IMAGE, -angle)
        self.rect = self.image.get_rect(center=pos)
        # To apply an offset to the start position,
        # create another vector and rotate it as well.
        offset = Vector2(40, 0).rotate(angle)
        # Then add the offset vector to the position vector.
        self.pos = Vector2(pos) + offset  # Center of the sprite.
        # Rotate the direction vector (1, 0) by the angle.
        # Multiply by desired speed.
        self.velocity = Vector2(1, 0).rotate(angle) * 9

    def update(self):
        self.pos += self.velocity  # Add velocity to pos to move the sprite.
        self.rect.center = self.pos  # Update rect coords.

        if not screen_rect.contains(self.rect):
            self.kill()


def main():
    clock = pg.time.Clock()
    cannon_img = pg.Surface((40, 20), pg.SRCALPHA)
    cannon_img.fill(pg.Color('aquamarine3'))
    cannon = cannon_img.get_rect(center=(320, 240))
    angle = 0
    bullet_group = pg.sprite.Group()  # Add bullets to this group.

    while True:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                return
            elif event.type == pg.MOUSEBUTTONDOWN:
                # Left button fires a bullet from center with
                # current angle. Add the bullet to the bullet_group.
                if event.button == 1:
                    bullet_group.add(Bullet(cannon.center, angle))

        keys = pg.key.get_pressed()
        if keys[pg.K_a]:
            angle -= 3
        elif keys[pg.K_d]:
            angle += 3
        if keys[pg.K_SPACE]:
            bullet_group.add(Bullet(cannon.center, angle))

        # Rotate the cannon image.
        rotated_cannon_img = pg.transform.rotate(cannon_img, -angle)
        cannon = rotated_cannon_img.get_rect(center=cannon.center)

        bullet_group.update()

        # Draw
        screen.fill((30, 40, 50))
        screen.blit(rotated_cannon_img, cannon)
        bullet_group.draw(screen)
        txt = FONT.render('angle {:.1f}'.format(angle), True, (150, 150, 170))
        screen.blit(txt, (10, 10))
        pg.display.update()

        clock.tick(30)

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

Regarding the code in your added example, the easiest solution is to calculate the speed_x and speed_y ("velocity" would be more fitting) in the __init__ method and then just update the self.rect.x and y attributes in the update method.

import math
import pygame


pygame.init()

screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()

BULLET_IMAGE = pygame.Surface((20, 11), pygame.SRCALPHA)
pygame.draw.polygon(BULLET_IMAGE, pygame.Color('aquamarine1'),
                [(0, 0), (20, 5), (0, 11)])


class Bullet(pygame.sprite.Sprite):

    def __init__(self, x, y, angle, speed):
        pygame.sprite.Sprite.__init__(self)
        # Rotate the bullet image (negative angle because y-axis is flipped).
        self.image = pygame.transform.rotate(BULLET_IMAGE, -angle)
        self.rect = self.image.get_rect(center=(x, y))
        angle = math.radians(angle)
        self.speed_x = speed * math.cos(angle)
        self.speed_y = speed * math.sin(angle)

    def update(self):
        self.rect.x += self.speed_x
        self.rect.y += self.speed_y

spr = pygame.sprite.Group()
bullet = Bullet(10, 10, 60, 3)
bullet2 = Bullet(10, 10, 30, 3)
spr.add(bullet, bullet2)

play = True
while play:
    clock.tick(60)
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            play = False
    screen.fill((30,30,40))
    spr.update()
    spr.draw(screen)
    pygame.display.flip()

pygame.quit()

There's a problem, because pygame.Rects can only have ints as the x and y attributes, so the movement won't be 100% correct. To solve this problem, you need to store the coords/position of the sprite in separate variables, add the speed to them and afterwards update the rect:

    # In `__init__`.
    self.pos_x = x
    self.pos_y = y

def update(self):
    self.pos_x += self.speed_x
    self.pos_y += self.speed_y
    self.rect.center = (self.pos_x, self.pos_y)
skrx
  • 19,980
  • 5
  • 34
  • 48
  • I really appreciate your answer... But I'm afraid to say that...it's a little confusing... Also... I don't know if that's what i wanted to get... Probably I should have said something like: I would like to define a function which creates a "moving object" that moves in a certain direction... Maybe... if you explain it to me... in simpler...words...? (I'm sorry, this is a little hard for me to understand...) – DarkBlood202 Oct 11 '17 at 22:28
  • I guess...I don't – DarkBlood202 Oct 11 '17 at 22:33
  • Okay, I can't and shouldn't explain vector math here, so I recommend to go to school. :P You can learn it for example on [khanacademy.org](https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces), but better learn trigonometry first. – skrx Oct 11 '17 at 23:01
  • Pygame's vectors use trigonometry under the hood for the rotation, so you don't have to deal with it directly. The movement is pretty simple: Imagine you had separate `pos_x`, `pos_y`, `velocity_x` and `velocity_y` attributes, then you would just move the sprite by adding `pos_x += velocity_x` and `pos_y += velocity_y`. With vectors you can just do this in one line by adding the two vectors: `pos += velocity`. – skrx Oct 11 '17 at 23:02
  • Actually... I had trigonometry at highschool... But I don't know exactly how to apply it here... I understood that brief explanation, by the way... – DarkBlood202 Oct 11 '17 at 23:07
  • Check out the added trigonometry solution. – skrx Oct 12 '17 at 00:00
  • I have implemented your solution to solve the inaccuracy problem caused by the trig. solution however the sprite has a vibrating effect when it isn't moving straight along an axis, eg, if the angle = 110 deg. Any ideas what this could be being caused by? – Mat Whiteside Jun 04 '18 at 21:50
  • @MatWhiteside that happens because the images have to be blitted at integer coordinates, since computer monitors don't have fractional pixels. The actual position which consists of floats is truncated to ints when you assign it to the `rect.center`. I don't know if there's a way to avoid or reduce this vibration (let me know if you find a solution). If the bullets move fast enough, the user probably won't notice it. – skrx Jun 05 '18 at 14:05