Collision & Hit Boxes
Subscribe to Tech with Tim
In many games we need to check for different types of collision. Whether that be collision of our mouse with a button or collision between images and/or objects. In our case we want to check if our goblin has hit the player or if our bullets have hit the goblin.
Hit Boxes
The term "hit box" is often used to represent the box around an object which represents its "hittable space". Since we often use complex objects and shapes to depict characters or other items in a game we create a hit box for all of these items. This makes it much easier to check for collision as collision between non-rectangular shapes is extremely complicated. We attempt to make these boxes fit the characters shape as precisely as possible but it is difficult to make them perfect.
We will start by creating hit boxes for all of our objects. Then we check if these boxes collide with one another using some basic math.
The first hit box we define will be for our player class.
# This goes inside the player class in the __init__ method self.hitbox = (self.x + 20, self.y, 28, 60) # The elements in the hitbox are (top left x, top left y, width, height)
Since our player moves we will have to constantly redefine the hit box from within the draw method of our player class. To do this we will simply copy the line from above into the draw method.
Our player class should now look like the following.
class player(object): def __init__(self,x,y,width,height): self.x = x self.y = y self.width = width self.height = height self.vel = 5 self.isJump = False self.left = False self.right = False self.walkCount = 0 self.jumpCount = 10 self.standing = True self.hitbox = (self.x + 17, self.y + 11, 29, 52) # NEW def draw(self, win): if self.walkCount + 1 >= 27: self.walkCount = 0 if not(self.standing): if self.left: win.blit(walkLeft[self.walkCount//3], (self.x,self.y)) self.walkCount += 1 elif self.right: win.blit(walkRight[self.walkCount//3], (self.x,self.y)) self.walkCount +=1 else: if self.right: win.blit(walkRight[0], (self.x, self.y)) else: win.blit(walkLeft[0], (self.x, self.y)) self.hitbox = (self.x + 17, self.y + 11, 29, 52) # NEW pygame.draw.rect(win, (255,0,0), self.hitbox,2) # To draw the hit box around the player
We will repeat this process in the enemy class. And after adding in the hit box we will also define a new method called hit.
class enemy(object): walkRight = [pygame.image.load('R1E.png'), pygame.image.load('R2E.png'), pygame.image.load('R3E.png'), pygame.image.load('R4E.png'), pygame.image.load('R5E.png'), pygame.image.load('R6E.png'), pygame.image.load('R7E.png'), pygame.image.load('R8E.png'), pygame.image.load('R9E.png'), pygame.image.load('R10E.png'), pygame.image.load('R11E.png')] walkLeft = [pygame.image.load('L1E.png'), pygame.image.load('L2E.png'), pygame.image.load('L3E.png'), pygame.image.load('L4E.png'), pygame.image.load('L5E.png'), pygame.image.load('L6E.png'), pygame.image.load('L7E.png'), pygame.image.load('L8E.png'), pygame.image.load('L9E.png'), pygame.image.load('L10E.png'), pygame.image.load('L11E.png')] def __init__(self, x, y, width, height, end): self.x = x self.y = y self.width = width self.height = height self.end = end self.path = [self.x, self.end] self.walkCount = 0 self.vel = 3 self.hitbox = (self.x + 17, self.y + 2, 31, 57) # NEW def draw(self,win): self.move() if self.walkCount + 1 >= 33: self.walkCount = 0 if self.vel > 0: win.blit(self.walkRight[self.walkCount //3], (self.x, self.y)) self.walkCount += 1 else: win.blit(self.walkLeft[self.walkCount //3], (self.x, self.y)) self.walkCount += 1 self.hitbox = (self.x + 17, self.y + 2, 31, 57) # NEW pygame.draw.rect(win, (255,0,0), self.hitbox,2) # Draws the hit box around the enemy def move(self): if self.vel > 0: if self.x + self.vel < self.path[1]: self.x += self.vel else: self.vel = self.vel * -1 self.walkCount = 0 else: if self.x - self.vel > self.path[0]: self.x += self.vel else: self.vel = self.vel * -1 self.walkCount = 0 # NEW METHOD def hit(self): # This will display when the enemy is hit print('hit')
If we run the program we can see the hit boxes for our character.
Collision
The fist collision we will check for is between the bullets and the enemy. Every time we move a bullet we will check if it has collided with the enemy. Since we already have a for loop setup to check if the bullets leave the screen we will do our collision check in there.
We are going to say these objects have collided if the x and y coordinate of the bullet sit inside the hit box of the enemy. We check this with the following code.
if bullet.y - bullet.radius < goblin.hitbox[1] + goblin.hitbox[3] and bullet.y + bullet.radius > goblin.hitbox[1]: # Checks x coords if bullet.x + bullet.radius > goblin.hitbox[0] and bullet.x - bullet.radius < goblin.hitbox[0] + goblin.hitbox[2]: # Checks y coords goblin.hit() # calls enemy hit method bullets.pop(bullets.index(bullet)) # removes bullet from bullet list
Bullet Glitch
There is a small glitch you may have noticed which causes our bullets to stick together or shoot multiple at the same time. To fix this we must do the following.
First we are going to create a variable called shootLoop outside of our main loop.
shootLoop = 0
After that we will place the following code at the top of our while loop.
if shootLoop > 0: shootLoop += 1 if shootLoop > 3: shootLoop = 0
Then we will modify our space bar event check the following way.
if keys[pygame.K_SPACE] and shootLoop == 0: ... # Add the "and shootLoop == 0"
Full Code
After all this the final code should look like:
import pygame pygame.init() win = pygame.display.set_mode((500,480)) pygame.display.set_caption("First Game") walkRight = [pygame.image.load('R1.png'), pygame.image.load('R2.png'), pygame.image.load('R3.png'), pygame.image.load('R4.png'), pygame.image.load('R5.png'), pygame.image.load('R6.png'), pygame.image.load('R7.png'), pygame.image.load('R8.png'), pygame.image.load('R9.png')] walkLeft = [pygame.image.load('L1.png'), pygame.image.load('L2.png'), pygame.image.load('L3.png'), pygame.image.load('L4.png'), pygame.image.load('L5.png'), pygame.image.load('L6.png'), pygame.image.load('L7.png'), pygame.image.load('L8.png'), pygame.image.load('L9.png')] bg = pygame.image.load('bg.jpg') char = pygame.image.load('standing.png') clock = pygame.time.Clock() class player(object): def __init__(self,x,y,width,height): self.x = x self.y = y self.width = width self.height = height self.vel = 5 self.isJump = False self.left = False self.right = False self.walkCount = 0 self.jumpCount = 10 self.standing = True self.hitbox = (self.x + 17, self.y + 11, 29, 52) def draw(self, win): if self.walkCount + 1 >= 27: self.walkCount = 0 if not(self.standing): if self.left: win.blit(walkLeft[self.walkCount//3], (self.x,self.y)) self.walkCount += 1 elif self.right: win.blit(walkRight[self.walkCount//3], (self.x,self.y)) self.walkCount +=1 else: if self.right: win.blit(walkRight[0], (self.x, self.y)) else: win.blit(walkLeft[0], (self.x, self.y)) self.hitbox = (self.x + 17, self.y + 11, 29, 52) pygame.draw.rect(win, (255,0,0), self.hitbox,2) class projectile(object): def __init__(self,x,y,radius,color,facing): self.x = x self.y = y self.radius = radius self.color = color self.facing = facing self.vel = 8 * facing def draw(self,win): pygame.draw.circle(win, self.color, (self.x,self.y), self.radius) class enemy(object): walkRight = [pygame.image.load('R1E.png'), pygame.image.load('R2E.png'), pygame.image.load('R3E.png'), pygame.image.load('R4E.png'), pygame.image.load('R5E.png'), pygame.image.load('R6E.png'), pygame.image.load('R7E.png'), pygame.image.load('R8E.png'), pygame.image.load('R9E.png'), pygame.image.load('R10E.png'), pygame.image.load('R11E.png')] walkLeft = [pygame.image.load('L1E.png'), pygame.image.load('L2E.png'), pygame.image.load('L3E.png'), pygame.image.load('L4E.png'), pygame.image.load('L5E.png'), pygame.image.load('L6E.png'), pygame.image.load('L7E.png'), pygame.image.load('L8E.png'), pygame.image.load('L9E.png'), pygame.image.load('L10E.png'), pygame.image.load('L11E.png')] def __init__(self, x, y, width, height, end): self.x = x self.y = y self.width = width self.height = height self.end = end self.path = [self.x, self.end] self.walkCount = 0 self.vel = 3 self.hitbox = (self.x + 17, self.y + 2, 31, 57) def draw(self,win): self.move() if self.walkCount + 1 >= 33: self.walkCount = 0 if self.vel > 0: win.blit(self.walkRight[self.walkCount //3], (self.x, self.y)) self.walkCount += 1 else: win.blit(self.walkLeft[self.walkCount //3], (self.x, self.y)) self.walkCount += 1 self.hitbox = (self.x + 17, self.y + 2, 31, 57) pygame.draw.rect(win, (255,0,0), self.hitbox,2) def move(self): if self.vel > 0: if self.x + self.vel < self.path[1]: self.x += self.vel else: self.vel = self.vel * -1 self.walkCount = 0 else: if self.x - self.vel > self.path[0]: self.x += self.vel else: self.vel = self.vel * -1 self.walkCount = 0 def hit(self): print('hit') def redrawGameWindow(): win.blit(bg, (0,0)) man.draw(win) goblin.draw(win) for bullet in bullets: bullet.draw(win) pygame.display.update() #mainloop man = player(200, 410, 64,64) goblin = enemy(100, 410, 64, 64, 450) shootLoop = 0 bullets = [] run = True while run: clock.tick(27) if shootLoop > 0: shootLoop += 1 if shootLoop > 3: shootLoop = 0 for event in pygame.event.get(): if event.type == pygame.QUIT: run = False for bullet in bullets: if bullet.y - bullet.radius < goblin.hitbox[1] + goblin.hitbox[3] and bullet.y + bullet.radius > goblin.hitbox[1]: if bullet.x + bullet.radius > goblin.hitbox[0] and bullet.x - bullet.radius < goblin.hitbox[0] + goblin.hitbox[2]: goblin.hit() bullets.pop(bullets.index(bullet)) if bullet.x < 500 and bullet.x > 0: bullet.x += bullet.vel else: bullets.pop(bullets.index(bullet)) keys = pygame.key.get_pressed() if keys[pygame.K_SPACE] and shootLoop == 0: if man.left: facing = -1 else: facing = 1 if len(bullets) < 5: bullets.append(projectile(round(man.x + man.width //2), round(man.y + man.height//2), 6, (0,0,0), facing)) shootLoop = 1 if keys[pygame.K_LEFT] and man.x > man.vel: man.x -= man.vel man.left = True man.right = False man.standing = False elif keys[pygame.K_RIGHT] and man.x < 500 - man.width - man.vel: man.x += man.vel man.right = True man.left = False man.standing = False else: man.standing = True man.walkCount = 0 if not(man.isJump): if keys[pygame.K_UP]: man.isJump = True man.right = False man.left = False man.walkCount = 0 else: if man.jumpCount >= -10: neg = 1 if man.jumpCount < 0: neg = -1 man.y -= (man.jumpCount ** 2) * 0.5 * neg man.jumpCount -= 1 else: man.isJump = False man.jumpCount = 10 redrawGameWindow() pygame.quit()