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()