Programming: Enemies & Hitboxes

If we're shooting bullets, we'll need a target! I'll create another class called "Enemy" to create the NPCs for our "Red_Guy" to shoot.

To start, I can copy the same attributes as the "Character" object. I'll also add another variable called "Health":
class Enemies(object):
	def __init__(self,x,y, color):
		self.x = x
		self.y = y
		self.color = color
		self.health = 1
Within the main loop, I can create the object "Blue_Guy" using the "Enemy" class. I'll also use the "pygame.rect" function to draw the object, similar to the "Red_Guy":
Red_Guy = Character(100, 100, (255, 0, 0))
Blue_Guy = Enemies(200, 100, (0, 0, 255))
...
while True:
    pygame.draw.rect(win, Red_Guy.color, (Red_Guy.x, Red_Guy.y, 5, 10))
    pygame.draw.rect(win, Blue_Guy.color, (Blue_Guy.x, Blue_Guy.y, 5, 10))

We can see how the initial coordinates are used for determining where the objects are drawn. The "Red_Guy" is at (100, 100), while the "Blue_Guy" is at (200, 100): 



But when we shoot the "Blue_Guy", nothing happens yet. If we recall the "Collision" function, we note that it's used to check if a bullet's coordinates exceed the game window. Once the condition is met, an action triggered (delete the bullet). 


The function can be updated to take the "Blue_Guy" as another input, and check if the two object's coordinates collide. By visualizing the "Blue_Guy" as a "box", the bullet needs to have both (x, y) coordinates within it to register as a collision. This is known as a "hitbox". 

Since the size of the "Blue_Guy" is a 5 x 10 rectangle, I use those with the "Blue_Guy" coordinates to draw the box. If the bullet coordinate is within the box, then we can trigger the event. 
def Collision(obj, proj, enemy):
    ...
    if proj.x > enemy.x and proj.x < enemy.x+5:
	if proj.y > enemy.y and proj.y < enemy.y+10: 
                obj.bullets.pop(obj.bullets.index(proj)) 
        	enemy.health -= 1
The triggered lines removes the bullet projectile  ticks down the "enemy.health" by 1. This doesn't mean anything on it's own, unless I use the health attribute as condition. If the "Blue_Guy" has over 1 health, then draw it. Otherwise, the object is "dead" and shouldn't be shown. 

Red_Guy = Character(100, 100, (255, 0, 0))
Blue_Guy = Enemies(200, 100, (0, 0, 255))
...
while True:
    pygame.draw.rect(win, Red_Guy.color, (Red_Guy.x, Red_Guy.y, 5, 10))
    if Blue_Guy.health > 0:
        pygame.draw.rect(win, Blue_Guy.color, (Blue_Guy.x, Blue_Guy.y, 5, 10))

 
Fine-Tuning

Despite not being drawn, the "Blue_Guy" object still occupies the (200, 100) coordinate. This means the "Collision" function will still trigger against the "Blue_Guy". This can be corrected by adding an additional condition to the "Collision" function to check if the object is "alive", otherwise it's ignored. 

def Collision(obj, proj, enemy):
    ...

    if enemy.health>0:
        if proj.x > enemy.x and proj.x < enemy.x+5:
	    if proj.y > enemy.y and proj.y < enemy.y+10:
                obj.bullets.pop(obj.bullets.index(proj))
                enemy.health -= 1

Modular Variables

Since I'm making extensive use of the object's width and height, it should be assigned as an implicit variable, rather than an explicit one. I'll make the dimensions required inputs for the classes, rather than entering them into each function. This will make it easy to assign different sizes to each object and retain full functionality. Full code below (I've made adjustments to some variables): 

import pygame
class Characters(object):
def __init__(self, x, y, width, height, color):
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
self.direction = 'Left'
self.bullets=[]
self.cooldown = 0

class Enemies(object):
def __init__(self,x,y, width, height, color):
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
self.health = 1
def Move_Object(obj):
speed = 1
if pygame.key.get_pressed()[pygame.K_w]:
if pygame.key.get_pressed()[pygame.K_a]: #Move Up-Left
obj.x -= speed * 0.707
obj.y -= speed * 0.707
obj.direction = 'Up Left'
elif pygame.key.get_pressed()[pygame.K_d]: #Move Up-Right
obj.x += speed * 0.707
obj.y -= speed * 0.707
obj.direction = 'Up Right'
else: #Move Up 
obj.y -= speed
obj.direction = 'Up' 
elif pygame.key.get_pressed()[pygame.K_s]:
if pygame.key.get_pressed()[pygame.K_a]: #Move Down-Left
obj.x -= speed * 0.707 
obj.y += speed * 0.707
obj.direction = 'Down Left' 
elif pygame.key.get_pressed()[pygame.K_d]: #Move Down-Right
obj.x += speed * 0.707
obj.y += speed * 0.707
obj.direction = 'Down Right' 
else: #Move Down
obj.y += speed 
obj.direction = 'Down' 
elif pygame.key.get_pressed()[pygame.K_a]: #Move Left
obj.x -= speed
obj.direction = 'Left' 
elif pygame.key.get_pressed()[pygame.K_d]: #Move Right
obj.x += speed
obj.direction = 'Right' 
if obj.x < 0: #If object exceeds left boundary
obj.x = 0
elif obj.x > 400-5: #If object exceeds right boundary
obj.x = 400-5
if obj.y < 0: #If object exceeds top boundary
obj.y = 0
elif obj.y > 300-10: #If object exceeds bottom boundary
obj.y = 300-10
class Projectiles(object):
def __init__(self, x, y, direction, color, speed):
self.x = x
self.y = y
self.color = color
self.direction = direction
self.speed = speed
self.radius = 3
def Move(self, win):
if self.direction == 'Up Left':
self.x -= self.speed * 0.707
self.y -= self.speed * 0.707
elif self.direction == 'Up':
self.y -= self.speed
elif self.direction == 'Up Right':
self.x += self.speed * 0.707
self.y -= self.speed * 0.707
elif self.direction == 'Right':
self.x += self.speed
elif self.direction == 'Down Right':
self.x += self.speed * 0.707
self.y += self.speed * 0.707
elif self.direction == 'Down':
self.y += self.speed
elif self.direction == 'Down Left':
self.x -= self.speed * 0.707
self.y += self.speed * 0.707
elif self.direction == 'Left':
self.x -= self.speed
pygame.draw.circle(win, self.color, (round(self.x), round(self.y)), self.radius)
def Shoot(obj):
if pygame.key.get_pressed()[pygame.K_j]:
if obj.cooldown == 0:
obj.bullets.append(Projectiles(obj.x, obj.y, obj.direction, (255,255,255), 0.05))
obj.cooldown = 2500
if obj.cooldown>0:
obj.cooldown -= 1
def Collision(obj, proj, enemy):
if proj.x < 0: #If object exceeds left boundary
obj.bullets.pop(obj.bullets.index(proj)) 
elif proj.x > 400-proj.radius: #If object exceeds right boundary
obj.bullets.pop(obj.bullets.index(proj)) 
if proj.y < 0: #If object exceeds top boundary
obj.bullets.pop(obj.bullets.index(proj)) 
elif proj.y > 300-proj.radius: #If object exceeds bottom boundary
obj.bullets.pop(obj.bullets.index(proj)) 
if enemy.health>0:
if proj.x > enemy.x and proj.x < enemy.x+enemy.width:
if proj.y > enemy.y and proj.y < enemy.y+enemy.height: 
obj.bullets.pop(obj.bullets.index(proj)) 
enemy.health -= 1
def main():
win = pygame.display.set_mode((400, 300))
pygame.init()
Red_Guy = Characters(100, 100, 10, 20,  (255, 0, 0))
Blue_Guy = Enemies(200, 100, 10, 20, (0, 0, 255))
bullets = []
while True:              frames = 30          clock=pygame.time.Clock()          clock.tick(frames)
for event in pygame.event.get(): #Exit game on red button
if event.type == pygame.QUIT:
return
win.fill((0,0,0))

Move_Object(Red_Guy)
pygame.draw.rect(win, Red_Guy.color, (Red_Guy.x, Red_Guy.y, Red_Guy.width, Red_Guy.height))
Shoot(Red_Guy) 
for bullet in Red_Guy.bullets:
bullet.Move(win)
Collision(Red_Guy, bullet, Blue_Guy)

if Blue_Guy.health>0:
pygame.draw.rect(win, Blue_Guy.color, (Blue_Guy.x, Blue_Guy.y, Blue_Guy.width, Blue_Guy.height))

pygame.display.flip()
main()




Comments