pygame-community / pygame-ce

🐍🎮 pygame - Community Edition is a FOSS Python library for multimedia applications (like games). Built on top of the excellent SDL library.
https://pyga.me
937 stars 155 forks source link

Sprite group collide tweaks #3197

Open celeritydesign opened 3 weeks ago

celeritydesign commented 3 weeks ago

As per initial discussion at: https://github.com/pygame-community/pygame-ce/issues/3193

This PR expands two Sprite collide functions, pygame.sprite.spritecollideany() and pygame.sprite.spritecollide() to allow an optional arg, ignore_self that will disregard the sprite being tested, with itself.

Without this arg, both functions report collisions with the sprite's own rect, if that sprite belongs to the group being tested. My feeling is that this could be counter-intuitive, especially to new users. For example, if someone sets up a group "all_ships" and wanted to see if any of them are hitting each other, the result would always be a hit. Even if only one ship was being drawn to the screen, the above functions would report that the ship had hit something (it is hitting itself!).

[it is obviously fairly easy to work around the above situation, for example you could remove a sprite from a group prior to checking it and then add it back in - this PR doesn't dispute this, it is more about making things easier for new users]

With the new "ignore_self" arg, no collisions would be reported unless the sprite were actually overlapping a different sprite within the group, which I would suggest is what a new user would expect.

On the discord, there was talk of expanding this functionality to add an 'exclusion' list of sprites. This PR does not currently do this. My own feeling is that this perhaps adds too much complexity, especially to spritecollideany() which I think of as a "quick and cheap" way of doing initial collision detection, prior to more accurate checks such as using masks. However, I could obviously change the functionality to work like that if that is the consensus.

Finally, please note this is my first PR to the project, so apologies if I have done anything incorrectly (for example, I've done three commits, sorry!). I have read the wiki contribution articles, run ruff on my changes and tested (more details below). I did not know how to regenerate the HTML documentation locally, but I've updated \docs\reST\ref\sprite.rst - is that all that is required?

--

Testing this PR, 1 of 2: Unit Tests

I updated sprite_text.py and added a new function to test both functions, both with default behaviour, and the new arg. I do this twice for each function, once with a callback collide function, and one with it omitted.

Testing this PR, 2 of 2: Messy visual test

I also cobbled together this test script (code below), which toggles between ignore_self = False and ignore_self = True by pressing the space bar. Move the box around using cursor keys. I use visual indicators to show how many sprites each box is hitting. (note orange is using a rectratio scale bigger than the sprite, so you turn orange before you overlap)

image ignore_self = False - every box thinks it is hitting one sprite (themselves!). Orange and Red colours show spritecollideany() hits, black and yellow inner squares denote 1 hit each returned by two calls to spritecollide()

image ignore_self = True - here we are actually overlapping some of the boxes. You can see the two top boxes are only reporting one hit (one black inner box, and one yellow one), and the bottom box is reporting two hits from each call.

(if this is confusing, I wrote it late at night! - look at the code and move the box around yourself and it will make sense :-) )

Test code: ``` import pygame pygame.init() screen = pygame.display.set_mode((600, 600)) clock = pygame.time.Clock() running = True dt = 0 all_squares = pygame.sprite.Group() sprite_size = 50 ignore_self = False def set_window_caption(): if ignore_self: pygame.display.set_caption( "Sprites won't trigger collisions with themselves in group 'all_squares'" ) else: pygame.display.set_caption("Sprites will hit themselves in group 'all_squares'") class Square(pygame.sprite.Sprite): def __init__(self, pos, colour="blue"): pygame.sprite.Sprite.__init__(self, all_squares) self.colour = colour self.image = pygame.Surface((sprite_size, sprite_size)) pygame.draw.rect(self.image, "blue", self.image.get_rect(), 0) self.rect = self.image.get_rect() self.rect.x, self.rect.y = pos[0], pos[1] def update(self): global ignore_self pygame.draw.rect(self.image, self.colour, self.image.get_rect(), 0) #test spritecollideany() with passed collided function, squares will turn orange if they are #within twice the size of the sprite's rect if pygame.sprite.spritecollideany( self, all_squares, pygame.sprite.collide_rect_ratio(2), ignore_self ): pygame.draw.rect(self.image, "orange", self.image.get_rect(), 0) #test spritecollideany() with no collided function, squares will get an inner red square if they #overlap another sprite if pygame.sprite.spritecollideany(self, all_squares, None, ignore_self): pygame.draw.rect( self.image, "red", (5, 5, sprite_size - 10, sprite_size - 10), 0 ) #test spritecollide() with passed collided function, #it will overlay a yellow square on itself for each sprite it hits hits = pygame.sprite.spritecollide( self, all_squares, None, pygame.sprite.collide_rect_ratio(1), ignore_self ) for i, _ in enumerate(hits): pygame.draw.rect(self.image, "yellow", (8 * (i + 1), 40, 5, 5), 0) #test spritecollide() with no collided function, it will overlay a black square on itself for each sprite it hits hits = pygame.sprite.spritecollide(self, all_squares, None, None, ignore_self) for i, _ in enumerate(hits): pygame.draw.rect(self.image, "black", (8 * (i + 1), 20, 5, 5), 0) player_square = Square((screen.get_width() / 2, screen.get_height() / 2), "green") other_square = Square((screen.get_width() / 3, screen.get_height() / 5)) another_square = Square((screen.get_width() / 3 + 70, screen.get_height() / 5)) player_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2) set_window_caption() while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: ignore_self = not ignore_self set_window_caption() screen.fill("gray") keys = pygame.key.get_pressed() if keys[pygame.K_UP]: player_pos.y -= 300 * dt if keys[pygame.K_DOWN]: player_pos.y += 300 * dt if keys[pygame.K_LEFT]: player_pos.x -= 300 * dt if keys[pygame.K_RIGHT]: player_pos.x += 300 * dt #move the player player_square.rect.x = player_pos.x player_square.rect.y = player_pos.y all_squares.update() all_squares.draw(screen) pygame.display.flip() dt = clock.tick(60) / 1000 pygame.quit() ````