Welcome to part 20 of the intermediate Python programming tutorial series. In the previous tutorial, we covered how we can use special methods to do operator overloading, in order to write our own logic for how to handle the +
operation. In this tutorial, we're going to cover how to actually detect that a collision has taken place.
How will we know logically when two blobs are touching? Well, we know their center (x,y) coordinates, and we know their radius. How do we calculate distances between two points on a plane? Euclidean Distance, of course! See the linked tutorial there for more information if you would like to learn more about calculating Euclidean distance, otherwise, you can rest easy knowing Numpy has your back with np.linalg.norm
. For our purposes, the norm is the same as the Euclidean Distance. Let's say we have two blobs, b1
and b2
. How would we calculate the distance, and determine if they're touching? We merely need to calcuate the Euclidean distance between their two centers. Then we can add both blob radius attributes together. If the combined radius value is greater than the Euclidean distance, then we're touching! Something like:
def is_touching(b1,b2): if np.linalg.norm(np.array([b1.x,b1.y])-np.array([b2.x,b2.y])) < (b1.size + b2.size): return True else: return False
Note: You need to import numpy as np
at the top of the script now. We can actually further simplify this with:
def is_touching(b1,b2): return np.linalg.norm(np.array([b1.x,b1.y])-np.array([b2.x,b2.y])) < (b1.size + b2.size)
This is one of those times when I think a function that is purely a return statement is actually useful, since the return statement is a very long, and somewhat confusing-at-a-quick-glance, line.
Okay, great, now we're checking if blobs are touching, now what? We need to check every blue blob, and see if it's touching any other blob. Let's say we're being fed a list containing the colored-blob dictionaries, called blob_list
:
def handle_collisions(blob_list): blues, reds, greens = blob_list for blue_id, blue_blob in blues.copy().items(): for other_blobs in blues, reds, greens: for other_blob_id, other_blob in other_blobs.copy().items(): if blue_blob == other_blob: pass else: if is_touching(blue_blob, other_blob): blue_blob + other_blob if other_blob.size <= 0: del other_blobs[other_blob_id] if blue_blob.size <= 0: del blues[blue_id] return blues, reds, greens
Above, note that, as we iterate through the dictionaries, we are using .copy()
. Why are we doing this? We do this because we're actually modifying the main dictionaries, and you never want to modify something while you iterate through it. All sorts of nasty things can happen, and not necessarily every time (so even if you are testing your code, you might not discover it). At the end, after we've discovered any collisions, done the +
operation, and deleted any blobs that are of size 0 or less, we return the modified dictionaries. Now, in our draw_environment
function, we need to add these changes:
def draw_environment(blob_list): game_display.fill(WHITE) blues, reds, greens = handle_collisions(blob_list) for blob_dict in blob_list: for blob_id in blob_dict: blob = blob_dict[blob_id] pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size) blob.move() blob.check_bounds() pygame.display.update() return blues, reds, greens
Notice there here we're now actually returning something (along with also doing blues, reds, greens = handle_collisions(blob_list)
). This is because we eventually need to pass these new, modified, dictionaries to that main while
loop. On that note, let's modify the main
function now:
def main(): blue_blobs = dict(enumerate([BlueBlob(WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)])) red_blobs = dict(enumerate([RedBlob(WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)])) green_blobs = dict(enumerate([GreenBlob(WIDTH,HEIGHT) for i in range(STARTING_GREEN_BLOBS)])) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() blue_blobs, red_blobs, green_blobs = draw_environment([blue_blobs,red_blobs,green_blobs]) clock.tick(60)
Noting the blue_blobs, red_blobs, green_blobs = draw_environment([blue_blobs,red_blobs,green_blobs])
. It as at this point that our draw_environment
function really should be re-named. I'll save that for later, but you should note that this function is actually spending more code doing things other than drawing stuff, so we should probably break it into two functions, or give it a more fitting name.
Full code up to this point:
import pygame import random from blob import Blob import numpy as np STARTING_BLUE_BLOBS = 15 STARTING_RED_BLOBS = 15 STARTING_GREEN_BLOBS = 15 WIDTH = 800 HEIGHT = 600 WHITE = (255, 255, 255) BLUE = (0, 0, 255) RED = (255, 0, 0) game_display = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Blob World") clock = pygame.time.Clock() class BlueBlob(Blob): def __init__(self, x_boundary, y_boundary): Blob.__init__(self, (0, 0, 255), x_boundary, y_boundary) def __add__(self, other_blob): if other_blob.color == (255, 0, 0): self.size -= other_blob.size other_blob.size -= self.size elif other_blob.color == (0, 255, 0): self.size += other_blob.size other_blob.size = 0 elif other_blob.color == (0, 0, 255): pass else: raise Exception('Tried to combine one or multiple blobs of unsupported colors!') class RedBlob(Blob): def __init__(self, x_boundary, y_boundary): Blob.__init__(self, (255, 0, 0), x_boundary, y_boundary) class GreenBlob(Blob): def __init__(self, x_boundary, y_boundary): Blob.__init__(self, (0, 255, 0), x_boundary, y_boundary) def is_touching(b1,b2): return np.linalg.norm(np.array([b1.x,b1.y])-np.array([b2.x,b2.y])) < (b1.size + b2.size) def handle_collisions(blob_list): blues, reds, greens = blob_list for blue_id, blue_blob in blues.copy().items(): for other_blobs in blues, reds, greens: for other_blob_id, other_blob in other_blobs.copy().items(): if blue_blob == other_blob: pass else: if is_touching(blue_blob, other_blob): blue_blob + other_blob if other_blob.size <= 0: del other_blobs[other_blob_id] if blue_blob.size <= 0: del blues[blue_id] return blues, reds, greens def draw_environment(blob_list): game_display.fill(WHITE) blues, reds, greens = handle_collisions(blob_list) for blob_dict in blob_list: for blob_id in blob_dict: blob = blob_dict[blob_id] pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size) blob.move() blob.check_bounds() pygame.display.update() return blues, reds, greens def main(): blue_blobs = dict(enumerate([BlueBlob(WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)])) red_blobs = dict(enumerate([RedBlob(WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)])) green_blobs = dict(enumerate([GreenBlob(WIDTH,HEIGHT) for i in range(STARTING_GREEN_BLOBS)])) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() blue_blobs, red_blobs, green_blobs = draw_environment([blue_blobs,red_blobs,green_blobs]) clock.tick(60) if __name__ == '__main__': main()