I'm fairly new to Pygame, and can't seem to find a solid answer on this. I have a shape, specifically an ellipse, that I want to rotate both left and right. The key bind would be a and d, as the arrow keys are already binded to move left and right on an x,y axis.
I know that it involves pygame.transform.rotate, however I can't seem to implement this right.
def main():
#Setup basic variables and methods for pygame
pygame.init()
windowWidth = 800
windowHeight = 700
fps = 45
clock = pygame.time.Clock()
gameWindow = pygame.display.set_mode((windowWidth, windowHeight))
surface = pygame.Surface((50,50))
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
shipX = windowWidth/2
shipY = windowWidth/2
shipSpeed = 4
while(True):
pygame.draw.ellipse(gameWindow, WHITE, (shipX, shipY, 20, 30))
#Monitor the FPS of the game
clock.tick(fps)
for event in pygame.event.get():
# ________________________________________
if event.type == pygame.QUIT:
gameExit()
rotate = 0
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP] and shipY > shipSpeed: shipY -= shipSpeed
if pressed[pygame.K_DOWN] and shipY < windowHeight - shipSpeed - 20: shipY += shipSpeed
if pressed[pygame.K_LEFT] and shipX > shipSpeed:shipX -= shipSpeed
if pressed[pygame.K_RIGHT] and shipX < windowWidth - shipSpeed - 20: shipX += shipSpeed
if pressed[ord('a')]: rotate = pygame.transform.rotate(surface, -20)
if pressed[ord('d')]: rotate = pygame.transform.rotate(surface, 20)
gameWindow.fill(BLACK)
# 'flip' display - always after drawing...
pygame.display.flip()
The expected result is that the shape will change it's angle, and then move accordingly.
Again, I'm very new to pygame, so any detailed help would be appreciated.
Your problem is that you draw the ellipse directly on the screen, but you should draw your ellipse on another Surface.
Then you can rotate that new Surface with pygame.transform.rotate.
Here's a simple example:
import pygame
import random
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
screen_rect = screen.get_rect()
clock = pygame.time.Clock()
surface = pygame.Surface((100, 200))
surface.set_colorkey((2, 3, 4))
surface.fill((2, 3, 4))
rect = surface.get_rect(center=(100, 100))
pygame.draw.ellipse(surface, pygame.Color('white'), (0, 0, 100, 200))
angle = 0
dt = 0
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: rect.move_ip(0, -5)
if pressed[pygame.K_DOWN]: rect.move_ip(0, 5)
if pressed[pygame.K_LEFT]: rect.move_ip(-5, 0)
if pressed[pygame.K_RIGHT]: rect.move_ip(5, 0)
if pressed[pygame.K_a]: angle += 1
if pressed[pygame.K_d]: angle -= 1
rotated = pygame.transform.rotate(surface, angle)
rect = rotated.get_rect(center=rect.center)
rect.clamp_ip(screen_rect)
screen.fill(pygame.Color('dodgerblue'))
screen.blit(rotated, rect.topleft)
pygame.display.update()
dt = clock.tick(60)
if __name__ == '__main__':
main()
Note that I use a Rect to store the position of the object because it's easy then to rotate the Surface around its center (by setting its center attribute), and to ensure the Surface does not go outside the screen (by using clamp_ip).
Also, it's important to always rotate the source Surface, and not the already rotated Surface. Otherwise, you'll get distortions.
Note that we have three things here: an image, a position, and some behaviour logic. Whenever you see these things together, consider putting them together into a class. Pygame already offers a nice class for this, called Sprite.
Here's the same example, but Sprite-based:
import pygame
import random
class Thingy(pygame.sprite.Sprite):
def __init__(self, area):
super().__init__()
# image is what get's painted on the screen
self.image = pygame.Surface((100, 200))
self.image.set_colorkey((2, 3, 4))
self.image.fill((2, 3, 4))
pygame.draw.ellipse(self.image, pygame.Color('white'), (0, 0, 100, 200))
# we keep a reference to the original image
# since we use that for rotation to prevent distortions
self.original = self.image.copy()
# rect is used to determine the position of a sprite on the screen
# the Rect class also offers a lot of useful functions
self.rect = self.image.get_rect(center=(100, 100))
self.angle = 0
self.area = area
def update(self, events, dt):
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: self.rect.move_ip(0, -5)
if pressed[pygame.K_DOWN]: self.rect.move_ip(0, 5)
if pressed[pygame.K_LEFT]: self.rect.move_ip(-5, 0)
if pressed[pygame.K_RIGHT]: self.rect.move_ip(5, 0)
if pressed[pygame.K_a]: self.angle += 1
if pressed[pygame.K_d]: self.angle -= 1
# let's rotate the image, but ensure that we keep the center position
# so it doesn't "jump around"
self.image = pygame.transform.rotate(self.original, self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
self.rect.clamp_ip(self.area)
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
screen_rect = screen.get_rect()
clock = pygame.time.Clock()
sprites = pygame.sprite.Group(Thingy(screen_rect))
dt = 0
while True:
# nice clean main loop
# all game logic goes into the sprites
# handle "global" events
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
# update all sprites
sprites.update(events, dt)
# draw everything
screen.fill(pygame.Color('dodgerblue'))
sprites.draw(screen)
pygame.display.update()
dt = clock.tick(60)
if __name__ == '__main__':
main()
You should make a class for your object:
class myRect(pygame.Surface):
def __init__(self, parent, xpos, ypos, width, height):
super(myRect, self).__init__(width, height)
self.xpos = xpos
self.ypos = ypos
self.parent = parent
def update(self, parent):
parent.blit(self, (self.xpos, self.ypos))
def rotate(self, angle):
#(your rotation code goes here)
Related
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...
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()
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)
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()
From the pymunk examples I've seen that there's a difference between the pymunk coordinates and pygame coordinates. Also, that pymunk is meant just for the 2D physics, while pygame is for rendering objects/sprites on the screen.
So when searching for how to build an environment where the camera follows the player, people (including me) end up getting confused. I've seen the examples here, here, here and here (even surprised that nobody answered this), but given the number of questions related to the same topic being asked repeatedly, I honestly feel the answers do not adequately explain the concept and request that the simplest possible example be shown to the community, where all the code is explained with comments.
I've worked in 3D environments like OGRE and OSG where the camera was a proper concept that could be defined with a view frustum, but I'm surprised the 2D world does not have a pre-defined function for it. So:
If not in the official tutorials of pymunk or pygame, at least could a simple example be provided (with a pymunk body as the player and few pymunk bodies in the world) as an answer here, where a player moves around in a 2D pymunk+pygame world and the camera follows the player?
OK, I'll try to make this simple (I assume basic pygame knowledge).
First, let's start with something basic. A little sprite that you can move around the world:
import pygame
import random
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((32, 32))
self.image.fill(pygame.Color('dodgerblue'))
self.rect = self.image.get_rect()
self.pos = pygame.Vector2((100, 200))
def update(self, events, dt):
pressed = pygame.key.get_pressed()
move = pygame.Vector2((0, 0))
if pressed[pygame.K_w]: move += (0, -1)
if pressed[pygame.K_a]: move += (-1, 0)
if pressed[pygame.K_s]: move += (0, 1)
if pressed[pygame.K_d]: move += (1, 0)
if move.length() > 0: move.normalize_ip()
self.pos += move*(dt/5)
self.rect.center = self.pos
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
dt = 0
player = Player()
sprites = pygame.sprite.Group(player)
background = screen.copy()
background.fill((30, 30, 30))
for _ in range(1000):
x, y = random.randint(0, 1000), random.randint(0, 1000)
pygame.draw.rect(background, pygame.Color('green'), (x, y, 2, 2))
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
sprites.update(events, dt)
screen.blit(background, (0, 0))
sprites.draw(screen)
pygame.display.update()
dt = clock.tick(60)
if __name__ == '__main__':
main()
Nothing crazy so far.
So, what is a "camera"? It's just a x and an y value we use to move the entire "world" (e.g. everything that is not UI). It's an abstraction between the coordinates of our game objects and the screen.
In our example above, when a game object (the player, or the background) wants to be drawn at position (x, y), we draw them at the screen at this very position.
Now, if we want to move around a "camera", we simply create another x, y-pair, and add this to the game object's coordinates to determine the actual position on the screen. We start to distinguish between world coordinates (what the game logic thinks where the position of an object is) and the screen coordinates (the actual position of an object on the screen).
Here's our example with a "camera" ("camera" in quotes) because it's really just two values:
import pygame
import random
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((32, 32))
self.image.fill(pygame.Color('dodgerblue'))
self.rect = self.image.get_rect()
self.pos = pygame.Vector2((100, 200))
def update(self, events, dt):
pressed = pygame.key.get_pressed()
move = pygame.Vector2((0, 0))
if pressed[pygame.K_w]: move += (0, -1)
if pressed[pygame.K_a]: move += (-1, 0)
if pressed[pygame.K_s]: move += (0, 1)
if pressed[pygame.K_d]: move += (1, 0)
if move.length() > 0: move.normalize_ip()
self.pos += move*(dt/5)
self.rect.center = self.pos
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
dt = 0
player = Player()
sprites = pygame.sprite.Group(player)
# the "world" is now bigger than the screen
# so we actually have anything to move the camera to
background = pygame.Surface((1500, 1500))
background.fill((30, 30, 30))
# a camera is just two values: x and y
# we use a vector here because it's easier to handle than a tuple
camera = pygame.Vector2((0, 0))
for _ in range(3000):
x, y = random.randint(0, 1000), random.randint(0, 1000)
pygame.draw.rect(background, pygame.Color('green'), (x, y, 2, 2))
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
# copy/paste because I'm lazy
# just move the camera around
pressed = pygame.key.get_pressed()
camera_move = pygame.Vector2()
if pressed[pygame.K_UP]: camera_move += (0, 1)
if pressed[pygame.K_LEFT]: camera_move += (1, 0)
if pressed[pygame.K_DOWN]: camera_move += (0, -1)
if pressed[pygame.K_RIGHT]: camera_move += (-1, 0)
if camera_move.length() > 0: camera_move.normalize_ip()
camera += camera_move*(dt/5)
sprites.update(events, dt)
# before drawing, we shift everything by the camera's x and y values
screen.blit(background, camera)
for s in sprites:
screen.blit(s.image, s.rect.move(*camera))
pygame.display.update()
dt = clock.tick(60)
if __name__ == '__main__':
main()
Now you can move the camera with the arrow keys.
That's it. We just move everything a little bit before blitting it to the screen.
For a more complete example (supporting sprites, stopping at the edge of the world, smooth movement), see this question.
And for using pymunk: it just works. It's not affected by drawing stuff to another position, since it works with the world coordinates, not the screen coordinates. The only pitfall is that pymunk's y-axis is flipped compared to pygame's y-axis, but you probably know this already.
Here's an example:
import pygame
import random
import pymunk
class Player(pygame.sprite.Sprite):
def __init__(self, space):
super().__init__()
self.space = space
self.image = pygame.Surface((32, 32))
self.image.fill(pygame.Color('dodgerblue'))
self.rect = self.image.get_rect()
self.pos = pygame.Vector2((100, 200))
self.body = pymunk.Body(1,1666)
self.body.position = self.pos
self.poly = pymunk.Poly.create_box(self.body)
self.space.add(self.body, self.poly)
def update(self, events, dt):
pressed = pygame.key.get_pressed()
move = pygame.Vector2((0, 0))
if pressed[pygame.K_w]: move += (0, 1)
if pressed[pygame.K_a]: move += (-1, 0)
if pressed[pygame.K_s]: move += (0, -1)
if pressed[pygame.K_d]: move += (1, 0)
if move.length() > 0: move.normalize_ip()
self.body.apply_impulse_at_local_point(move*5)
# if you used pymunk before, you'll probably already know
# that you'll have to invert the y-axis to convert between
# the pymunk and the pygame coordinates.
self.pos = pygame.Vector2(self.body.position[0], -self.body.position[1]+500)
self.rect.center = self.pos
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
dt = 0
space = pymunk.Space()
space.gravity = 0,-100
player = Player(space)
sprites = pygame.sprite.Group(player)
# the "world" is now bigger than the screen
# so we actually have anything to move the camera to
background = pygame.Surface((1500, 1500))
background.fill((30, 30, 30))
# a camera is just two values: x and y
# we use a vector here because it's easier to handle than a tuple
camera = pygame.Vector2((0, 0))
for _ in range(3000):
x, y = random.randint(0, 1000), random.randint(0, 1000)
pygame.draw.rect(background, pygame.Color('green'), (x, y, 2, 2))
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
# copy/paste because I'm lazy
# just move the camera around
pressed = pygame.key.get_pressed()
camera_move = pygame.Vector2()
if pressed[pygame.K_UP]: camera_move += (0, 1)
if pressed[pygame.K_LEFT]: camera_move += (1, 0)
if pressed[pygame.K_DOWN]: camera_move += (0, -1)
if pressed[pygame.K_RIGHT]: camera_move += (-1, 0)
if camera_move.length() > 0: camera_move.normalize_ip()
camera += camera_move*(dt/5)
sprites.update(events, dt)
# before drawing, we shift everything by the camera's x and y values
screen.blit(background, camera)
for s in sprites:
screen.blit(s.image, s.rect.move(*camera))
pygame.display.update()
dt = clock.tick(60)
space.step(dt/1000)
if __name__ == '__main__':
main()
Note that when you use pymunk.Space.debug_draw, you won't be able to translate the world coordinates to screen coordinates, so it would be best to simply draw the pymunk stuff to another Surface, and translate that very Surface.
Here's pymunk's pygame_util_demo.py with a moving camera:
import sys
import pygame
from pygame.locals import *
import pymunk
from pymunk.vec2d import Vec2d
import pymunk.pygame_util
import shapes_for_draw_demos
def main():
pygame.init()
screen = pygame.display.set_mode((1000,700))
pymunk_layer = pygame.Surface((1000,700))
pymunk_layer.set_colorkey((12,12,12))
pymunk_layer.fill((12,12,12))
camera = pygame.Vector2((0, 0))
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 16)
space = pymunk.Space()
captions = shapes_for_draw_demos.fill_space(space)
# Info
color = pygame.color.THECOLORS["black"]
options = pymunk.pygame_util.DrawOptions(pymunk_layer)
while True:
for event in pygame.event.get():
if event.type == QUIT or \
event.type == KEYDOWN and (event.key in [K_ESCAPE, K_q]):
return
elif event.type == KEYDOWN and event.key == K_p:
pygame.image.save(screen, "pygame_util_demo.png")
# copy/paste because I'm lazy
pressed = pygame.key.get_pressed()
camera_move = pygame.Vector2()
if pressed[pygame.K_UP]: camera_move += (0, 1)
if pressed[pygame.K_LEFT]: camera_move += (1, 0)
if pressed[pygame.K_DOWN]: camera_move += (0, -1)
if pressed[pygame.K_RIGHT]: camera_move += (-1, 0)
if camera_move.length() > 0: camera_move.normalize_ip()
camera += camera_move*5
screen.fill(pygame.color.THECOLORS["white"])
pymunk_layer.fill((12,12,12))
space.debug_draw(options)
screen.blit(pymunk_layer, camera)
screen.blit(font.render("Demo example of pygame_util.DrawOptions()", 1, color), (205, 680))
for caption in captions:
x, y = caption[0]
y = 700 - y
screen.blit(font.render(caption[1], 1, color), camera + (x,y))
pygame.display.flip()
clock.tick(30)
if __name__ == '__main__':
sys.exit(main())
This code is mostly just the generic start up of a pygame window but I'm trying to make it so the object moves (the planet object I've made) in the orbit around the sun object I've made for the coordinates I've given it. I know the x and y values are updating but I don't understand why the object doesn't move.
#import the library
import pygame
import math
#classes
class button:
def _init_ (self,screen, colour, x, y, width,height, letter):
self.screen = screen
self.colour = colour
self.x = x
self.y = y
self.width = width
self.height = height
self.letter = letter
self.radius = radius
def draw(self):
pygame.draw.rect(self.screen, self.colour,(self.x,self.y, self.width, self.height))
if self.letter!= '+' and self.letter!= '-':
font = pygame.font.SysFont('agencyfb',15,True,False)
else:
font = pygame.font.SysFont('agencyfb',25,True,False)
text = font.render(self.letter, True, black)
text_rect = text.get_rect(center=(self.x+self.width/2,self.y+self.height/2))
screen.blit(text, text_rect)
class orbit:
def __init__(self,screen,colour,x,y,radius,width):
self.screen = screen
self.colour = colour
self.x = x
self.y = y
self.width = width
self.radius = radius
def draw_circle(self):
pygame.draw.circle(self.screen,self.colour,(self.x,self.y),self.radius,self.width)
#define colours
##Sun = pygame.draw.circle(screen,Sun,[1000,450],100,0)
Black = (0,0,0)
White = (255,255,255)
Green = (0,255,0)
Red = (255,0,0)
Blue = (0,0,255)
Sun = (255,69,0)
Sun = []
Planet = []
#initialise the engine
pygame.init()
#Opening a window
size = (1920,1080)
screen = pygame.display.set_mode(size)
#set window title
pygame.display.set_caption("Orbit Simulator")
#loop unti the user clicks the close button
done = False
#
x=1000
y=450
Sun.append(orbit(screen,Red,1000,450,100,0))
Planet.append(orbit(screen,White,x,y,50,0))
#
#used to manage how fast the screen updates
clock = pygame.time.Clock()
#------ Main program Loop ------
while not done:
#--- Main event loop
for event in pygame.event.get(): #user did something
if event.type == pygame.QUIT: #if user clicked close
done = True #flag that we are done and exit the loop
#------ Game logic should go here ------
#------ Drawing code should go here -------
#first, clear the screen to white. Don't put other drawing commands above this or they will be erased with this command.
screen.fill(Black)
for i in Sun:
i.draw_circle()
for i in Planet:
r=150
angle=0
count = 0
while angle <= 360:
angle_radians = math.radians(angle)
x = int(math.cos(angle_radians)*r)
y = int(math.sin(angle_radians)*r)
angle +=1
count +=1
print(count)
x+=1000
y+=450
pygame.draw.circle(screen,White,[x,y],10,0)
print("Position [",x,",",y,"]")
#update the screen
pygame.display.flip()
#------ Limit to 60 frames per second ------
clock.tick(60)
#------ When the loop ends, quit ------
pygame.quit()
You can make an object rotate around another by using trigonometry or vectors. With vectors you just have to rotate a vector which defines the offset from the rotation center each frame and add it to the position vector (self.pos which is the rotation center) to get the desired self.rect.center coordinates of the orbiting object.
import pygame as pg
from pygame.math import Vector2
class Planet(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((40, 40), pg.SRCALPHA)
pg.draw.circle(self.image, pg.Color('dodgerblue'), (20, 20), 20)
self.rect = self.image.get_rect(center=pos)
self.pos = Vector2(pos)
self.offset = Vector2(200, 0)
self.angle = 0
def update(self):
self.angle -= 2
# Add the rotated offset vector to the pos vector to get the rect.center.
self.rect.center = self.pos + self.offset.rotate(self.angle)
def main():
pg.init()
screen = pg.display.set_mode((640, 480))
screen_rect = screen.get_rect()
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
planet = Planet(screen_rect.center, all_sprites)
yellow = pg.Color('yellow')
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
return
all_sprites.update()
screen.fill((30, 30, 30))
pg.draw.circle(screen, yellow, screen_rect.center, 60)
all_sprites.draw(screen)
pg.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()
pg.quit()
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...
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()
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)
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()
Having trouble with my PyGame experimental game - I'm learning how to work with sprites.
I have been trying to code 'collision' detection between sprites (ball and paddle) and have managed to get the collision detection working but my ball sprite seems to reset its position instead of carrying on. Could anyone take a look and see where my error is?
Here is my code:
import pygame
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
#variables, constants, functions
x = 1
y = 1
x_vel = 10
y_vel = 10
bat_x = 1
bat_y = 1
bat_x_vel = 0
bat_y_vel = 0
score = 0
class Ball(pygame.sprite.Sprite):
"""
This class represents the ball.
It derives from the "Sprite" class in Pygame.
"""
def __init__(self, width, height):
""" Constructor. Pass in the color of the block,
and its x and y position. """
# Call the parent class (Sprite) constructor
super().__init__()
# Set the background color and set it to be transparent
self.image = pygame.Surface([width, height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
# Draw the ellipse
pygame.draw.ellipse(self.image, (255,0,0), [0,0,width,height], 10)
# Fetch the rectangle object that has the dimensions of the image
# image.
# Update the position of this object by setting the values
# of rect.x and rect.y
self.rect = self.image.get_rect()
# Instance variables that control the edges of where we bounce
self.left_boundary = 0
self.right_boundary = 0
self.top_boundary = 0
self.bottom_boundary = 0
# Instance variables for our current speed and direction
self.vel_x = 5
self.vel_y = 5
def update(self):
""" Called each frame. """
self.rect.x += self.vel_x
self.rect.y += self.vel_y
if self.rect.right >= self.right_boundary or self.rect.left <= self.left_boundary:
self.vel_x *= -1
if self.rect.bottom >= self.bottom_boundary or self.rect.top <= self.top_boundary:
self.vel_y *= -1
class Paddle(pygame.sprite.Sprite):
"""
This class represents the ball.
It derives from the "Sprite" class in Pygame.
"""
def __init__(self, width, height):
""" Constructor. Pass in the color of the block,
and its x and y position. """
# Call the parent class (Sprite) constructor
super().__init__()
# Set the background color and set it to be transparent
self.image = pygame.Surface([width, height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
# Draw the rectangle
pygame.draw.rect(self.image, (0, 255, 0), [0, 0, width, height], 0)
# Fetch the rectangle object that has the dimensions of the image
# image.
# Update the position of this object by setting the values
# of rect.x and rect.y
self.rect = self.image.get_rect()
# Instance variables for our current speed and direction
self.x_vel = 0
self.y_vel = 0
def update(self):
# Get the current mouse position. This returns the position
# as a list of two numbers.
self.rect.x = self.rect.x + self.x_vel
self.rect.y = self.rect.y + self.y_vel
#initialise ball and paddle
paddle = Paddle(20, 100)
ball = Ball(100,100)
# This is a list of every sprite.
# All blocks and the player block as well.
all_sprites_list = pygame.sprite.Group()
all_sprites_list.add(ball)
all_sprites_list.add(paddle)
ball_sprites_list = pygame.sprite.Group()
ball_sprites_list.add(ball)
# Initialize Pygame
pygame.init()
# Set the height and width of the screen
screen_width = 700
screen_height = 400
screen = pygame.display.set_mode((screen_width, screen_height))
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Loop until the user clicks the close button.
done = False
# -------- Main Program Loop -----------
while not done:
# --- Events code goes here (mouse clicks, key hits etc)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
paddle.y_vel = -3
if event.key == pygame.K_DOWN:
paddle.y_vel = 3
if event.type == pygame.KEYUP:
if event.key == pygame.K_UP:
paddle.y_vel = 0
if event.key == pygame.K_DOWN:
paddle.y_vel = 0
# --- Game logic should go here
# Calls update() method on every sprite in the list
all_sprites_list.update()
# collision check
ball_hit_list = pygame.sprite.spritecollide(paddle, ball_sprites_list, False)
# Check the list of collisions.
for ball in ball_hit_list:
score +=1
print(score)
# --- Clear the screen
screen.fill((255,255,255))
# --- Draw all the objects
all_sprites_list.draw(screen)
# render text
myfont = pygame.font.SysFont("monospace", 15)
label = myfont.render(str(score), 1, (0,0,0))
screen.blit(label, (100, 100))
# --- Update the screen with what we've drawn.
pygame.display.flip()
# --- Limit to 60 frames per second
clock.tick(60)
pygame.quit()
Sorry,
Have found the error.
Didn't set the boundaries of the window properly.
# Instance variables that control the edges of where we bounce
self.left_boundary = 0
self.right_boundary = 700
self.top_boundary = 0
self.bottom_boundary = 400