I am trying to build a game in which combining images (pygame sprites) is an essential tool.
I have set up my code such that I can move sprites across x,y with the mouse and rotate them. The sprites are blitted to the display surface and so this motion can be seen on screen.
Once the user has arranged two sprites as they wish within a square zone, I need them to be able to save this whole zone as a new sprite.
I cannot see a way currently on pygame to capture a region of the display and store this as a sprite. Is this possible? What functions should I use for this purpose?
You could check which sprites collide with the square area and pass them to a Combined sprite class, combine the rects with the union_ip method and create a new surface with the necessary size to blit the surfaces of the single sprites onto it. (Press C to combine the sprites.)
import pygame as pg
BLUE = pg.Color('dodgerblue1')
SIENNA = pg.Color('sienna1')
GREEN = pg.Color('green')
class Entity(pg.sprite.Sprite):
def __init__(self, pos, color):
super().__init__()
self.image = pg.Surface((42, 68))
self.image.fill(color)
self.rect = self.image.get_rect(topleft=pos)
def move(self, velocity):
self.rect.move_ip(velocity)
class Combined(pg.sprite.Sprite):
def __init__(self, sprites):
super().__init__()
# Combine the rects of the separate sprites.
self.rect = sprites[0].rect.copy()
for sprite in sprites[1:]:
self.rect.union_ip(sprite.rect)
# Create a new transparent image with the combined size.
self.image = pg.Surface(self.rect.size, pg.SRCALPHA)
# Now blit all sprites onto the new surface.
for sprite in sprites:
self.image.blit(sprite.image, (sprite.rect.x-self.rect.left,
sprite.rect.y-self.rect.top))
def move(self, velocity):
self.rect.move_ip(velocity)
def main():
pg.init()
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
entity = Entity((50, 80), BLUE)
entity2 = Entity((50, 180), SIENNA)
all_sprites = pg.sprite.Group(entity, entity2)
area = pg.Rect(200, 50, 200, 200)
selected = None
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
return
elif event.type == pg.MOUSEBUTTONDOWN:
for sprite in all_sprites:
if sprite.rect.collidepoint(event.pos):
selected = sprite
elif event.type == pg.MOUSEBUTTONUP:
selected = None
elif event.type == pg.MOUSEMOTION:
if selected:
selected.move(event.rel)
elif event.type == pg.KEYDOWN:
if event.key == pg.K_c:
# A 'list comprehension' to find the colliding sprites.
colliding_sprites = [sprite for sprite in all_sprites
if sprite.rect.colliderect(area)]
combined = Combined(colliding_sprites)
all_sprites.add(combined)
# Kill the colliding sprites if they should be removed.
# for sprite in colliding_sprites:
# sprite.kill()
all_sprites.update()
screen.fill((30, 30, 30))
pg.draw.rect(screen, SIENNA, area, 2)
all_sprites.draw(screen)
for sprite in all_sprites: # Outlines.
pg.draw.rect(screen, GREEN, sprite.rect, 1)
pg.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()
pg.quit()
Alternatively, you could try to add the combined sprites to another sprite group or a list and blit and move them together.
Related
from numpy import size
import pygame
import sys
# --- constants --- # PEP8: `UPPER_CASE_NAMES` for constants
WHITE = (255, 255, 255) # PE8: space after `,`
SIZE = (700, 500)
FPS = 120 # there is no need to use `500` because Python can't run so fast,
# and monitors runs with 60Hz (eventually 120Hz) which can display 60 FPS (120 FPS)
# --- classes --- # PEP8: `CamelCaseNames` for classes
class MySprite(pygame.sprite.Sprite):
def __init__(self, x, y, picture, colorkey):
super().__init__()
# you need one of them
# load image
self.image = pygame.image.load(picture)
# OR
# create surface
# self.image = pygame.Surface((10, 10))
# self.image.fill((255, 0, 0))
# ----
self.rect = self.image.get_rect()
self.rect.center = (x, y)
self.image.set_colorkey(colorkey)
def update(self):
if event.type == pygame.MOUSEBUTTONDOWN:
mouse = pygame.mouse.get_pos()
print(mouse[:])
# --- main ---
pygame.init()
window_game = pygame.display.set_mode(SIZE)
backGround = pygame.image.load('bg.jpg').convert_alpha(window_game)
backGround = pygame.transform.smoothscale(backGround,SIZE)
backGround.set_colorkey(WHITE)
#placeSP_group = pygame.sprite.Group()
placeSP_group = pygame.sprite.OrderedUpdates() # Group which keep order
sprite1 = [MySprite(0, 0, 'crossHair.png', WHITE),MySprite(0, 0, 'crossHair_2.png', WHITE)]
placeSP_group.add([sprite1[0],sprite1[1]])
pygame.mouse.set_visible(False)
clock = pygame.time.Clock() # PEP8: `lower_case_names` for variables
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
#running = False
pygame.quit()
exit()
# create new Sprite
global x,y
x, y = pygame.mouse.get_pos()
new_sprite = sprite1[:]
# add new Sprite at the end of OrderedUpdates()
placeSP_group.add([new_sprite])
# remove Sprite at the beginning of OrderedUpdates()
placeSP_group.sprites()[0].kill()
placeSP_group.update()
# ---
pygame.display.flip()
window_game.fill('white')
window_game.blit(backGround,(0,0)).size
placeSP_group.draw(window_game)
clock.tick(FPS)
when i assignee new_sprite to all the assigned sprites in placeSP
it dosen't show any thing can you help me with that i am not sure why is that happening but can you fix it ... this an edited question. And i didn't got any answer .... but i have the concept in my head can and i also don't wan't to modify my code that much can you help me with that...
Create a sprite class with a list of images. Add an attribute that counts the frames. Get image from image list in the update method based on number of frames:
class MySprite(pygame.sprite.Sprite):
def __init__(self, x, y, image_list):
super().__init__()
self.frame = 0
self.image_list = image_list
self.image = self.image_list[0]
self.rect = self.image.get_rect()
self.rect.center = (x, y)
def update(self, event_list):
animation_interval = 20
self.frame += 1
if self.frame // animation_interval >= len(self.image_list):
self.frame = 0
self.image = self.image_list[self.frame // animation_interval]
for event in event_list:
if event.type == pygame.MOUSEBUTTONDOWN:
mouse = event.pos
print(mouse[:])
However, instead of constantly creating and killing the sprite every frame, you need to create the sprite once before the application loop. Change the position of the sprite and update the sprite in the application loop:
file_list = ['crossHair.png', 'crossHair_2.png', 'crossHair_3.png']
image_list = []
for name in file_list:
image = pygame.image.load(name)
image.set_colorkey(WHITE)
image_list.append(image)
# create sprite
sprite1 = MySprite(0, 0, image_list)
placeSP_group = pygame.sprite.OrderedUpdates()
placeSP_group.add([sprite1])
clock = pygame.time.Clock()
running = True
while running:
# handle events
event_list = pygame.event.get()
for event in event_list:
if event.type == pygame.QUIT:
running = False
# update sprite
x, y = pygame.mouse.get_pos()
placeSP_group.sprites()[0].rect.center = (x, y)
placeSP_group.update(event_list)
# draw scene
window_game.fill('white')
window_game.blit(backGround,(0,0)).size
placeSP_group.draw(window_game)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
exit()
See also:
how to create an illusion of animations in pygame
Animated sprite from few images
How do I create animated sprites using Sprite Sheets in Pygame?
In my game I am trying to make it so when you walk into a object, it displays an image.
I'm pretty sure that pygame.display.update() is being called every frame because otherwise the game would be perfectly still.
However when I draw my new rect upon collision it doesn't appear, unless I put another pygame.display.update(rect) with it after it being drawn. This means that update is being called twice at one time, in the main game loop and after drawing the rect. This causes the rect (which has been drawn now) to flicker because of the multiple update calls.
I cannot figure it out why it doesn't get drawn without the second update call.
Main game loop call:
def events(self):
#game loop events
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.playing = False
self.running = False
def update(self):
self.all_sprites.update()
def main(self):
while self.playing:
self.events()
self.update()
self.draw()
self.running = False
def draw(self):
self.screen.fill(black)
self.all_sprites.draw(self.screen)
self.clock.tick(FPS)
pygame.display.update()
#create game instance
g= Game()
g.new()
while g.running:
#main gameloop
g.main()
pygame.quit()
sys.exit()
Here is when I call to draw the rect after collision with my object:
def update(self):
self.hitbox.center = numpy.add(self.rect.center,(8,22))
self.interactionhitbox.center = numpy.add(self.rect.center, (8,16))
if(self.showPopup):
# Initialwzng Color
color = (255,0,0)
# Drawing Rectangle
rect = pygame.Rect((0,0,60,60))
pygame.display.update(rect) # WITHOUT THIS LINE IT DOES NOT GET DRAWN, WITH IT IT FLICKERS
pygame.draw.rect(self.game.screen, color, rect)
So basically with the second pygame.display.update(rect) call it appears but flickers, and without it it doesn't show up at all
Any help is appreciated sorry if this is a bad question or not formatted right I haven't been here since 2017!
The rectangle is not drawn because the screen will later be cleared with self.screen.fill(black) later. You must draw the rectangle after self.screen.fill(black) and before pygame.display.update().
Create 2 images and choose the image to be drawn in update:
def __init__(self, ...)
# [...]
self.image = ...
self.original_image = self.image
self.image_and_rect = self.image.copy()
pygame.draw.rect(self.image_and_rect, (255,0,0), self.image_and_rect.get_rect(), 5)
def update(self):
self.hitbox.center = numpy.add(self.rect.center,(8,22))
self.interactionhitbox.center = numpy.add(self.rect.center, (8,16))
if self.showPopup:
self.image = self.image_and_rect
else:
self.image = self.original_image
Okay, so basically what I'm trying to do is keep the main file a little cleaner and I'm starting with the "Zombie" enemy by making it's own file which will most likely contain all enemies, and importing it in.
So I'm confused on how I'd set-up the Class for a sprite, you don't have to tell me how to get it to move or anything like that I just want it to simply appear. The game doensn't break when I run it as is, I just wanted to ask this question before I goto sleep so I can hopefully get a lot done with the project done tomorrow (School related)
Code is unfinished like I said just wanted to ask while I get some actual sleep just a few google searches and attempts.
Eventually I'll take from the advice given here to make a "Hero" class as well, and as well as working with importing other factors if we have the time.
Zombie code:
import pygame
from pygame.locals import *
class ZombieEnemy(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load('images/zombie.png')
# self.images.append(img)
# self.image = self.images[0]
self.rect = self.image.get_rect()
zombieX = 100
zombieY = 340
zombieX_change = 0
Main Code:
import pygame
from pygame.locals import *
import Zombie
# Intialize the pygame
pygame.init()
# Create the screen
screen = pygame.display.set_mode((900, 567))
#Title and Icon
pygame.display.set_caption("Fighting Game")
# Add's logo to the window
# icon = pygame.image.load('')
# pygame.display.set_icon(icon)
# Player
playerImg = pygame.image.load('images/character.png')
playerX = 100
playerY = 340
playerX_change = 0
def player(x,y):
screen.blit(playerImg,(x,y))
Zombie.ZombieEnemy()
def zombie(x,y):
screen.blit()
# Background
class Background(pygame.sprite.Sprite):
def __init__(self, image_file, location):
pygame.sprite.Sprite.__init__(self) #call Sprite initializer
self.image = pygame.image.load('images/background.png')
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = location
BackGround = Background('background.png', [0,0])
# Game Loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# If keystroke is pressed check right, left.
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
#playerX_change = -2.0
BackGround.rect.left = BackGround.rect.left + 2.5
if event.key == pygame.K_RIGHT:
#playerX_change = 2.0
BackGround.rect.left = BackGround.rect.left - 2.5
# if event.type == pygame.KEYUP:
# if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT:
# BackGround.rect.left = 0
screen.blit(BackGround.image, BackGround.rect)
playerX += playerX_change
player(playerX,playerY)
pygame.display.flip()
Your sprite code is basically mostly there already. But as you say, it needs an update() function to move the sprites somehow.
The idea with Sprites in PyGame is to add them to a SpriteGroup, then use the group functionality for handling the sprites together.
You might want to modify the Zombie class to take an initial co-ordinate location:
class ZombieEnemy(pygame.sprite.Sprite):
def __init__( self, x=0, y=0 ):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load('images/zombie.png')
self.rect = self.image.get_rect()
self.rect.center = ( x, y ) # NOTE: centred on the co-ords
Which allows the game to create a Zombie at a particular starting point (perhaps even a random one).
So to have a sprite group, first you need to create the container:
all_zombies = pygame.sprite.Group()
Then when you create a new zombie, add it to the group. Say you wanted to start with 3 randomly-positioned zombies:
for i in range( 3 ):
new_x = random.randrange( 0, WINDOW_WIDTH ) # random x-position
new_y = random.randrange( 0, WINDOW_HEIGHT ) # random y-position
all_zombies.add( Zombie( new_x, new_y ) ) # create, and add to group
Then in the main loop, call .update() and .draw() on the sprite group. This will move and paint all sprites added to the group. In this way, you may have separate groups of enemies, bullets, background-items, etc. The sprite groups allow easy drawing and collision detection between other groups. Think of colliding a hundred bullets against a thousand enemies!
while running:
for event in pygame.event.get():
# ... handle events
# Move anything that needs to
all_zombies.update() # call the update() of all zombie sprites
playerX += playerX_change
# Draw everything
screen.blit(BackGround.image, BackGround.rect)
player(playerX,playerY)
all_zombies.draw( screen ) # paint every sprite in the group
pygame.display.flip()
EDIT: Added screen parameter to all_zombies.draw()
It's probably worthwhile defining your player as a sprite too, and having a single-entry group for it as well.
First you could keep position in Rect() in class. And you would add method which draws/blits it.
import pygame
class ZombieEnemy(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.image.load('images/zombie.png')
self.rect = self.image.get_rect()
self.rect.x = 100
self.rect.y = 340
self.x_change = 0
def draw(self, screen):
screen.blit(self.image, self.rect)
Next you have to assign to variable when you create it
zombie = Zombie.ZombieEnemy()
And then you can use it to draw it
zombie.draw(screen)
in
screen.blit(BackGround.image, BackGround.rect)
player(playerX,playerY)
zombie.draw(screen)
pygame.display.flip()
The same way you can create class Player and add method draw() to class Background and then use .
background.draw(screen)
player.draw(screen)
zombie.draw(screen)
pygame.display.flip()
Later you can use pygame.sprite.Group() to keep all objects in one group and draw all of them using one command - group.draw()
I've just recently become interested in pygame and the thing is my code doesn't work as what I intended: the image that I want to move with my mouse doesn't moving at all. So here's mine (contains code from previous question I saw):
import pygame,sys,os
WHITE = (255,255,255)
BLACK = (0,0,0)
GREY = (128,128,128)
class SilverGeneral:
def __init__(self,rect):
self.click = False
self.rect = pygame.Rect(rect)
def update(self,screen):
if self.click:
self.rect.center = pygame.mouse.get_pos()
pygame.init()
screen=pygame.display.set_mode([1000,600])
pygame.display.set_caption("Test")
silv = SilverGeneral((5,5,40,20))
silv.rect.center=screen.get_rect().center
clock = pygame.time.Clock()
image = pygame.image.load("c:\game\silvergeneral.bmp").convert()
while 1:
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP:
print(silv.rect.collidepoint(event.pos))
if silv.rect.collidepoint(event.pos):
print("True")
silv.click=True
elif event.type == pygame.MOUSEBUTTONDOWN:
print("False")
silv.click=False
elif event.type == pygame.QUIT:
pygame.quit()
sys.exit()
print (pygame.mouse.get_pos())
print (silv.rect.center)
silv.update(screen)
screen.blit(image,silv.rect)
clock.tick(10)
pygame.display.update()
I have been thinking for a whole hour and don't know why the collidepoint(event.pos) doesn't work. Also even if it's tested, the console never prints True.
collidepoint(event.pos) works just fine.
It seems that you want to be able to move the image once you click on it.
I guess your problem is that you expect it to work with clicking anywhere on that image, but you actually check if the mouse position is in the top left 40x20 pixel box of the image.
You can easily verify that by changing
screen.blit(image,silv.rect)
to
pygame.draw.rect(screen, pygame.color.THECOLORS['blue'], silv.rect, 0)
A good starting point is to use pygame's Sprite class and change your code to something like this:
class SilverGeneral(pygame.sprite.Sprite):
def __init__(self, *groups):
pygame.sprite.Sprite.__init__(self, *groups)
self.click = False
self.image = pygame.image.load("image.jpg").convert()
self.rect = self.image.get_rect()
def update(self):
if self.click:
self.rect.center = pygame.mouse.get_pos()
pygame.init()
screen=pygame.display.set_mode([1000,600])
pygame.display.set_caption("Test")
sprites = pygame.sprite.Group()
silv = SilverGeneral(sprites)
silv.rect.center = screen.get_rect().center
clock = pygame.time.Clock()
while 1:
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP:
silv.click = silv.rect.collidepoint(event.pos) and not silv.click
elif event.type == pygame.QUIT:
pygame.quit()
sys.exit()
sprites.update()
sprites.draw(screen)
clock.tick(60)
pygame.display.update()
The important thing here is that the rect is set to the rect of the image, so it will have the right size.
I've found in the pygame docs that the .rect attribute of a sprite is often initialised with the same width and the same height as the .image attribute.
However, you can change the .rect width or height so that the .rect becomes larger or smaller than the .image but the origins (topleft corners) of both .image and .rect are always the same point.
So here is my question:
Is there a way to create an offset between the origins (ie topleft corners) of the .rect and the .image of a sprite?
I know you can avoid this by:
creating and using a second Rect attribute (for instance a .collide_rect) that would be updated each time the .rect would
using transparency in the provided .image
But those methods are quite inefficient and I would really appreciate a "Yes/No" answer.
I've searched in the official docs for hours but I didn't find the way to create such an offset so I suppose the answer to my question is "No" (even the .inflate_ip() method doesn't work since it doesn't keep the center of the .rect in place as written in the docs).
You could give the sprite class an additional offset attribute (a Vector2) and then iterate over the sprites with a for loop and add this offset to the .rect.topleft position when you're blitting the images.
import pygame as pg
from pygame.math import Vector2
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((220, 120))
self.image.fill(pg.Color('steelblue2'))
self.rect = self.image.get_rect(center=pos)
self.rect.inflate_ip(-42, -72) # Make the rect smaller.
self.vel = Vector2(0, 0)
self.pos = Vector2(pos)
# Offset is half of the inflation size,
# so that the rect will be centered.
self.offset = Vector2(-21, -36)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Player((300, 200), all_sprites)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.KEYDOWN:
if event.key == pg.K_d:
player.vel.x = 5
elif event.type == pg.KEYUP:
if event.key == pg.K_d:
player.vel.x = 0
all_sprites.update()
screen.fill((30, 30, 30))
# Assign `blit` to a local variable to improve the performance.
screen_blit = screen.blit
for sprite in all_sprites:
# Now blit the sprites at topleft + offset.
screen_blit(sprite.image, sprite.rect.topleft+sprite.offset)
pg.draw.rect(screen, (250, 30, 0), sprite.rect, 2)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
For the sake of completeness, here's the solution with a second, scaled "hitbox" rect and a custom collided callback function that you can pass to one of the collision detection functions like pygame.sprite.spritecollide.
import pygame as pg
from pygame.math import Vector2
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((70, 40))
self.image.fill(pg.Color('steelblue4'))
self.rect = self.image.get_rect(center=pos)
# A inflated rect as the hitbox.
self.hitbox = self.rect.copy()
self.hitbox.inflate_ip(-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 = Player((300, 200), all_sprites)
enemies = pg.sprite.Group(
Player((100, 250), all_sprites),
Player((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()