Can't figure out how to check mask collision between two sprites - python

I have two different sprites in the same group, variables 'player' and 'ground'. They both are separate classes, with a mask of their surface. This line is in both of their classes.
self.mask = pygame.mask.from_surface(self.surface)
The images used in their surfaces have 'convert_alpha()' used on them so part of them is transparent, and the mask should work on them. The ground is a few platforms, and I want to check for collision so I can keep the player on the ground, and have them fall when they are not on the non-transparent parts.
if pygame.sprite.collide_mask(player,ground):
print("collision")
else:
print("nope")
This prints "nope", even as the player sprite is falling over where the colored ground sprite pixels are. So the documentation for 'collide_mask()' says that it returns "NoneType" when there is no collision. So I tried this.
if pygame.sprite.collide_mask(player,ground)!= NoneType:
print("collision")
This prints "collision" no matter where the player is(I have jumping, left, and right movements setup for the player). I asked a question about collision yesterday with no answer that helped. And I was told to condense my code submitted in the question so hopefully I explained this well enough without posting all 90 lines. I've checked a lot of other questions on here, and they all seem to do it a little different so I'm very confused (and fairly new).
Emphasis on both sprites being in the same group, I couldn't get spritecollide() to work because of this.

The sprites do not only need the mask attribute, they also need the rect attribute. the mask defines the bitmask and rect specifies the posiotion of the sprite on the screen. See pygame.sprite.collide_mask:
Tests for collision between two sprites, by testing if their bitmasks overla. If the sprites have a mask attribute, it is used as the mask, otherwise a mask is created from the sprite's image. Sprites must have a rect attribute; the mask attribute is optional.
If sprites are used in pygame.sprite.Groups then each sprite should have image and rect attributes. pygame.sprite.Group.draw() and pygame.sprite.Group.update() are methods which are provided by pygame.sprite.Group.
The latter delegates to the update method of the contained pygame.sprite.Sprites — you have to implement the method. See pygame.sprite.Group.update():
Calls the update() method on all Sprites in the Group. [...]
The former uses the image and rect attributes of the contained pygame.sprite.Sprites to draw the objects — you have to ensure that the pygame.sprite.Sprites have the required attributes. See pygame.sprite.Group.draw():
Draws the contained Sprites to the Surface argument. This uses the Sprite.image attribute for the source surface, and Sprite.rect. [...]
Minimal example
import os, pygame
os.chdir(os.path.dirname(os.path.abspath(__file__)))
class SpriteObject(pygame.sprite.Sprite):
def __init__(self, x, y, image):
super().__init__()
self.image = image
self.rect = self.image.get_rect(center = (x, y))
self.mask = pygame.mask.from_surface(self.image)
pygame.init()
clock = pygame.time.Clock()
window = pygame.display.set_mode((400, 400))
size = window.get_size()
object_surf = pygame.image.load('AirPlane.png').convert_alpha()
obstacle_surf = pygame.image.load('Rocket').convert_alpha()
moving_object = SpriteObject(0, 0, object_surf)
obstacle = SpriteObject(size[0] // 2, size[1] // 2, obstacle_surf)
all_sprites = pygame.sprite.Group([moving_object, obstacle])
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
moving_object.rect.center = pygame.mouse.get_pos()
collide = pygame.sprite.collide_mask(moving_object, obstacle)
window.fill((255, 0, 0) if collide else (0, 0, 64))
all_sprites.draw(window)
pygame.display.update()
pygame.quit()
exit()

Related

Blitting a Surface onto another without combining alpha

In a pygame project I'm working on, sprites of characters and objects cast a shadow onto the terrain. Both the shadow and the terrain are normal pygame surfaces so, to show them, the shadow is blitted onto the terrain. When there's no other shadow (only one shadow and the terrain) everything works fine, but when the character walks into the area of a shadow, while casting its own shadow, both shadows combine their alpha values, obscuring the terrain even more.
What I want is to avoid this behaviour, keeping the alpha value stable. Is there any way to do it?
EDIT: This is an image, that I made in Photoshop, to show the issue
EDIT2: #sloth's answer is ok, but I neglected to comment that my project is more complicated than that. The shadows are not whole squares, but more akin to “stencils”. Like real shadows, they are silhouettes of the objects they are cast from, and therefore they need per pixel alphas which are not compatible with colorkey and whole alpha values.
Here is a YouTube video that shows the issue a bit more clearly.
An easy way to solve this is to blit your shadows on another Surface first which has an alpha value, but no per pixel alpha. Then blit that Surface to your screen instead.
Here's a simple example showing the result:
from pygame import *
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
# we create two "shadow" surfaces, a.k.a. black with alpha channel set to something
# we use these to illustrate the problem
shadow = pygame.Surface((128, 128), pygame.SRCALPHA)
shadow.fill((0, 0, 0, 100))
shadow2 = shadow.copy()
# a helper surface we use later for the fixed shadows
shadow_surf = pygame.Surface((800, 600))
# we set a colorkey to easily make this surface transparent
colorkey_color = (2,3,4)
shadow_surf.set_colorkey(colorkey_color)
# the alpha value of our shadow
shadow_surf.set_alpha(100)
# just something to see the shadow effect
test_surface = pygame.Surface((800, 100))
test_surface.fill(pygame.Color('cyan'))
running = True
while running:
for e in pygame.event.get():
if e.type == pygame.QUIT:
running = False
screen.fill(pygame.Color('white'))
screen.blit(test_surface, (0, 150))
# first we blit the alpha channel shadows directly to the screen
screen.blit(shadow, (100, 100))
screen.blit(shadow2, (164, 164))
# here we draw the shadows to the helper surface first
# since the helper surface has no per-pixel alpha, the shadows
# will be fully black, but the alpha value for the full Surface image
# is set to 100, so we still have transparent shadows
shadow_surf.fill(colorkey_color)
shadow_surf.blit(shadow, (100, 100))
shadow_surf.blit(shadow2, (164, 164))
screen.blit(shadow_surf, (400, 0))
pygame.display.update()
You could create a function that tests for shadow collision and adjust the blend values of the shadows accordingly.
You can combine per-pixel alpha shadows by blitting them onto a helper surface and then fill this surface with a transparent white and pass the pygame.BLEND_RGBA_MIN flag as the special_flags argument. The alpha value of the fill color should be equal or lower than the alphas of the shadows. Passing the pygame.BLEND_RGBA_MIN flag means that for each pixel the lower value of each color channel will be taken, so it will reduce the increased alpha of the overlapping shadows to the fill color alpha.
import pygame as pg
pg.init()
screen = pg.display.set_mode((800, 600))
clock = pg.time.Clock()
shadow = pg.image.load('shadow.png').convert_alpha()
# Shadows will be blitted onto this surface.
shadow_surf = pg.Surface((800, 600), pg.SRCALPHA)
running = True
while running:
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
screen.fill((130, 130, 130))
screen.blit(shadow, (100, 100))
screen.blit(shadow, (154, 154))
shadow_surf.fill((0, 0, 0, 0)) # Clear the shadow_surf each frame.
shadow_surf.blit(shadow, (100, 100))
shadow_surf.blit(shadow, (154, 154))
# Now adjust the alpha values of each pixel by filling the `shadow_surf` with a
# transparent white and passing the pygame.BLEND_RGBA_MIN flag. This will take
# the lower value of each channel, therefore the alpha should be lower than
# the shadow alphas.
shadow_surf.fill((255, 255, 255, 120), special_flags=pg.BLEND_RGBA_MIN)
# Finally, blit the shadow_surf onto the screen.
screen.blit(shadow_surf, (300, 0))
pg.display.update()
clock.tick(60)
Here's the shadow.png.

How to get coordinates/area of collision in Pygame

I have a game where the bullet is moving so fast that in a single frame it is already across the screen. That being said, it has already collided with multiple walls. Currently, I have a rectangular image that spans from where the bullet currently is, to where the bullet will be in the next frame, in order to not miss any zombies that may be in between.
I also kill the bullet if it collided with any wall, before checking if it collided with any of the zombies because what happened was that if there was a zombie behind the wall and I checked for the zombie collisions first, it would kill the zombie and then kill the bullet.
So basically, I would like to know a way to find the coordinates of where the bullet collided with the wall so that instead of advancing the bullet at its full speed, I will just advance it to just before where the collision is, check for zombie collisions, and then kill the bullet.
I am using mask collision.
If the bullets travel too fast to collide with the walls or enemies, you need ray casting (or alternatively move the bullets in multiple small steps). Here's a simple ray casting example that returns the closest collision point. I use vectors and pygame.Rect.collidepoint to see if a point along the heading vector collides with an obstacle.
import sys
import pygame as pg
from pygame.math import Vector2
class Wall(pg.sprite.Sprite):
def __init__(self, x, y, w, h, *groups):
super().__init__(*groups)
self.image = pg.Surface((w, h))
self.image.fill(pg.Color('goldenrod4'))
self.rect = self.image.get_rect(topleft=(x, y))
def ray_cast(origin, target, obstacles):
"""Calculate the closest collision point.
Adds the normalized `direction` vector to the `current_pos` to
move along the heading vector and uses `pygame.Rect.collidepoint`
to see if `current_pos` collides with an obstacle.
Args:
origin (pygame.math.Vector2, tuple, list): Origin of the ray.
target (pygame.math.Vector2, tuple, list): Endpoint of the ray.
obstacles (pygame.sprite.Group): A group of obstacles.
Returns:
pygame.math.Vector2: Closest collision point or target.
"""
current_pos = Vector2(origin)
heading = target - origin
# A normalized vector that points to the target.
direction = heading.normalize()
for _ in range(int(heading.length())):
current_pos += direction
for sprite in obstacles:
# If the current_pos collides with an
# obstacle, return it.
if sprite.rect.collidepoint(current_pos):
return current_pos
# Otherwise return the target.
return Vector2(target)
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
walls = pg.sprite.Group()
Wall(100, 170, 90, 20, all_sprites, walls)
Wall(200, 100, 20, 140, all_sprites, walls)
Wall(400, 60, 150, 100, all_sprites, walls)
pos = Vector2(320, 440)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
all_sprites.update()
collision_point = ray_cast(pos, pg.mouse.get_pos(), walls)
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.draw.line(screen, (50, 190, 100), pos, pg.mouse.get_pos(), 2)
pg.draw.circle(screen, (40, 180, 250), [int(x) for x in collision_point], 5)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
sys.exit()
Unfortunately PyGame doesn't give us a built-in means of returning collision points easy way of doing this so there is a bit of leg work to do.
However, before I explain that, you mentioned in your question that the bullet is moving very fast. I'm not sure how fast we are talking so this might not apply, but in my experience collision becomes a hit and miss at high speeds, especially if you're working on a slower computer.
Assuming that ^ isn't applicable:
We can use pygame.Rect.colliderect to trigger an if statement.
if <bullet-rect>.collidepoint(<wall-rect>):
print(bullet_x, bullet_y)
Simply swap out and for the actual rects and you should be good to go. One thing to note is that if the bullet is moving right to left, you will have to add the bullet's width to the x value, and if the bullet is moving top to bottom, you will have to add the bullet's height to the y value.
Note: Remember to add pygame.Rect(<bullet-rect>) and pygame.Rect(<wall-rect>) to each value or you'll get an error.

Python: Drawing a Diagonal Line in Animation

I am trying to make an animation but I am not sure how to draw a diagonal line and move it.
import pygame
import sys
WINDOW=pygame.display.set_mode((800,300))
RED=(255,0,0)
WHITE=(255,255,255)
CRIMSON=(220,20,60)
BURGUNDY=(102,0,0)
CERULEAN=(153,255,255)
PINK=(255,102,102)
FPS=100
fpsClock=pygame.time.Clock()
x=0
pygame.display.set_caption("Animation")
while True:
for event in pygame.event.get():
if event.type=="QUIT":
pygame.quit()
sys.exit()
#Animation
WINDOW.fill(CERULEAN)
x=x+1
pygame.draw.circle(WINDOW, CRIMSON, (x,100),20)
pygame.draw.circle(WINDOW, BURGUNDY, (x, 92),5)
pygame.draw.line(WINDOW, PINK, (x,30),(x,70),3)
pygame.display.update()
fpsClock.tick(FPS)
The drawing is supposed to be a fish with a triangle as its tail. I originally tried to use the polygon function but wasn't sure how to input the x and where to input the x so just decided to draw three lines for the triangle.
I just need help as to how and where I would input the x into the line or even polygon function. Like for the circle one would simply put it first but how would it be for a line and/or polygon function?
Use pygame.draw.aalines() instead.
Give it a list of points forming the triangle.
Your problem here is:
X is increasing indefinitely
It increases too fast.
If you want to make this nice, make a object called Fish, with a method move, which wil move the fish. This way you will have clean code and minimal confusion.
You must remove the old fish from the window, then draw the new one on the new position. That's why you should keep the fish in its own object.
Choose one point of the fish that will act as an universal position indicator. E.g. a nose or centre of the mass, or something. Then you just change that pos and your move method adjusts all other coordinates accordingly.
EDIT:
This is an example. I didn't try it, just put it together to show you how it is done.
I hope this will make it clearer. You see, I draw the fish once on itsown surface, then I move this surface around.
As you move the mouse, the fish will be moved.
It may be slow, and flickery, and stuff. This code has some problem as yours.
The new fish should be drawn every 4 pixels, not each one.
As fish is in an object you can have multiple fishes of different sizes.
Each fish keeps portion of a screen that replaces in an origsurf attribute.
Before it moves, it returns the screen in previous position.
So your fishes can overlap on the screen.
To make all work smooth, you will have to do some more work.
For example, there are no safeguards against going over display bounds.
As I said, it is an example on how it is done, not a full code.
class Fish:
def __init__ (self, pos=(0, 0), size=(60, 40), bgcolour=(255, 255, 255)):
self.pos = pos; self.size = size
# Use a little surface and draw your fish in it:
self.surf = surf = pygame.Surface(size)
surf.fill(bgcolour)
# Draw something:
pygame.draw.aalines(surf, (0, 0, 0), 1, [(0, 0), (0, size[1]), (size[0], size[1]/2)]) # Triangle
# Save how screen looks where fish will be:
self.origsurf = Surface(size)
sub = screen.subsurface((pos, size))
self.origsurf.blit(sub)
# Show the fish:
screen.blit(surf, pos)
pygame.display.flip()
def move (self, newpos):
# Remove the fish from old position
screen.blit(self.origsurf, self.pos)
# Save how screen looks where fish will be:
self.origsurf = Surface(self.size)
sub = screen.subsurface((newpos, self.size))
self.origsurf.blit(sub)
# Then show the fish on new location:
screen.blit(self.surf, newpos)
self.pos = newpos
pygame.display.flip()
screen = pygame.display.set_mode()
fish = Fish()
FPS=100
fpsClock=pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type=="QUIT":
pygame.quit()
sys.exit()
elif event.type==pygame.MOUSEMOTION:
fish.move(event.pos)
fpsClock.tick(FPS)

pygame rect collision smaller than image

How can I define a rect collision detection smaller than image in pygame?
I'd like to have a collision patter like the second image , but I'm having a cut image when a try to set the width and height in the method rect.
When I try to set using image size, I have the collision detection in red
self.rect = pygame.rect.Rect(location, self.image.get_size())
If I set the size using width and height, I just have the third image
self.rect = pygame.rect.Rect(location, (32, 150))
I really wouldn't like to use pixel perfect collision, because is the slowest collision detection, so someone have some idea how can I achieve the second image collision approach using Rect? Thanks.
It seems that you are using pygames built in sprite module. (Please correct me if I am wrong)
You might know that each sprite consist of an image (which is drawn on a surface) and a rect object (sets location and size (!) of the image).
As Luke Taylor suggested, you could create a new rect object in your player class …
self.collideRect = pygame.rect.Rect((0, 0), (32, 150))
… and set its location (according to your graphic) to
self.collideRect.midbottom = self.rect.midbottom
Every time you change the position of your player you must call this line too, so your self.collideRect rect object "moves" with your player on screen.
To test if a point (e.g. the mouse coordinates) is inside the self.collideRect, call
if self.collideRect.collidepoint(x, y) == True:
print("You clicked on me!")
Try drawing a completely new rectangle separate from the image that is behind the image, but who's location is constantly set to that if the image.

python pygame blit function

When I use blit function, it does not delete the previous loaded sprite to make sprites move until I call the "display.fill(bgcolor)" function. The problem is that I have a multicolored background. so how do I update the image without affecting my background?
NOTE - already tried "pygame.display.update()" and "pygame.display.flip()" - it doesn't help :(
class states():
def __init__(self, goku1,goku2, x, y):
self.image=goku1
keys=pygame.key.get_pressed()
if keys[K_RIGHT]:
self.image=goku2
if keys[K_LEFT]:
self.image=goku2
while True:
pygame.display.flip()
pygame.display.update()
obj=states(goku1, goku2, x, y)
call=position()
DISPLAYSURF.blit(obj.image, (x, y))
am stuck for long :(
You would blit the background first, and then blit the new location for the sprite that is moving. It would look something like this:
window= pygame.display.set_mode(WINDOWSIZE, 0, 32)
while True:
#update events
window.blit(your_multi_colored_background, (0, 0))
window.blit(obj.image, (x, y))
pygame.display.update()
Hope this helps.
Blit never delete previous element - it can't - all blitted elements create one bitmap.
You have to blit all elements again in all loop.
Or you have to keep part of background before you blit sprite and use it later to blit this part in place of sprite to remove it.
You can also use pygame.display.update() with arguments to blit only some parts of background.

Categories

Resources