Programming: Basic AI

An enemy isn't too threatening if they're just standing there menacingly. I'll add some basic AI to make things more interesting


The most basic type of AI for a video game enemy is to follow the player. I'll create a function called "Hunter" that requires two objects input: a hunter and target.

def Hunt(hunter, target):
if hunter.y>target.y:
if hunter.x>target.x: #Move Up-Left
hunter.x -= hunter.speed * 0.707
hunter.y -= hunter.speed * 0.707
elif hunter.x<target.x: #Move Up-Right
hunter.x += hunter.speed * 0.707
hunter.y -= hunter.speed * 0.707
else: #Move Up 
hunter.y -= hunter.speed
elif hunter.y<target.y:
if hunter.x>target.x: #Move Down-Left
hunter.x -= hunter.speed * 0.707 
hunter.y += hunter.speed * 0.707
elif hunter.x<target.x: #Move Down-Right
hunter.x += hunter.speed * 0.707
hunter.y += hunter.speed * 0.707
else: #Move Down
hunter.y += hunter.speed 
elif hunter.x>target.x: #Move Left
hunter.x -= hunter.speed
elif hunter.x<target.x: #Move Right
hunter.x += hunter.speed

This is obviously derived from the "Move_Object" function used for making moving the player object around. Instead of key inputs as triggers, the "Hunt" function reacts to the game conditions. If the target is to it's left (target.x < hunter.x), the hunter will move left (hunter.x -= hunter.speed), and so on for any orientation. Evidently, I also added the "speed" variable to the "Enemy" class so I can adjust for the different enemy speeds.
        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))
Hunt(Blue_Guy, Red_Guy)
By adding the "Hunt" function into the While loop, the "Blue_Guy" springs to life! This is a very primitive type of AI movement since it's just taking the most direct path to the player. This does not even consider pathfinding around level obstacles or other object collision. But this will work for now.

Hit Invulnerability 

When the the hunter finally reaches it's target, something needs to happen. Typically, if the enemy touches the player (their hitboxes collide), the player takes a hit. Unlike the previous collisions which use a single coordinate, both objects have their own widths and heights. Whenever the two object's rectangles overlap, there is a collision. 

This means a variety of conditions need to be considered to trigger the collision:


Hit Invulnerability 

Once the trigger is collision, the "Hit" attribute for the target object will be made "True". If the "Hit" 
attribute is true, then the damage is applied (reducing "Health" attribute) and "Hit" is returned to "False". Similar to the bullet creation, conditions will trigger as fast as the cycle time, so a "cooldown" needs to be added. Many games have these "invincibility" frames so that each hit will only apply damage once.

To address this, applying the damage will also turn on object invulnerability ("Invul") to give a grace period. This "Invul" state will need to be "False" for "Hunt" and "Object_Collision" functions to trigger. That is, no the enemy won't hunt or damage the player while they are invulnerable. Another function called "Invulnerable" also needs to added. It simply ticks down an object's "Invul" attribute when it's above zero. 

I Get Knocked Down

Lastly, we can add a bit of "knock-back" so to give more impact to the enemy collision. This is done by adjusting the target's coordinates when the "Hit" state is triggered, based on direction. Getting hit from the left moves the player to the right, etc. 

When I tested it without, the player object would get knocked diagonally when the enemy was very slightly off-level. Since these diagonal calculations were most often used, I had to add an additional threshold to calculate the which direction to use. It's a calculation that determines if the difference in x or y coordinate is above a certain value. 

Final Code

The code is getting rather lengthy now, and I've had to make several adjustments to the attributes. See below for the complete code: 
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
		self.invul = 0 
		self.health = 1
		self.hit = False 
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
		self.speed = 0.75

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 Object_Collision(hunter, target):
	knock_back = 10
	if not target.invul:
		if hunter.x+hunter.width>target.x and hunter.x<target.x: #Right boundary
			if hunter.y<target.y+target.height and hunter.y+hunter.height>target.y+target.height: 
				target.hit = True #Hit from bottom left
			elif hunter.y+hunter.height>target.y and hunter.y<target.y:
				target.hit = True #Hit from top left
			elif hunter.y == target.y or hunter.y+hunter.height == target.y+target.height:
				target.hit = True #Hit from left 
		elif hunter.x+hunter.width>target.x+target.width and hunter.x<target.x+target.width: #Left boundary
			if hunter.y<target.y+target.height and hunter.y+hunter.height>target.y+target.height: 
				target.hit = True #Hit from bottom right
			elif hunter.y+hunter.height>target.y and hunter.y<target.y:
				target.hit = True #Hit from top right
			elif hunter.y == target.y or hunter.y+hunter.height == target.y+target.height:
				target.hit = True #Hit from right
		elif hunter.y+hunter.height>target.y+target.height and hunter.y<target.y+target.height:
			if hunter.x == target.x or hunter.x+hunter.width == target.x+target.width:
				target.hit = True #Hit from bottom
		elif hunter.y+hunter.height>target.y and hunter.y<target.y: #Bottom boundary
			if hunter.x == target.x or hunter.x+hunter.width == target.x+target.width:
				target.hit = True #Hit from top 
		else:
			target.hit = False

	if target.hit:
		target.invul = 1000
		target.health -= 1
		target.hit = False

		if hunter.x - target.x > 5: #Knock left
			if hunter.y - target.y > 5: #Knock up left
				target.x -= knock_back *.707
				target.y -= knock_back *.707
			elif target.y - hunter.y > 5: #Knock down left
				target.x -= knock_back *.707
				target.y += knock_back *.707				
			else:
				target.x -= knock_back
		elif target.x - hunter.x > 5: #Knock right
			if hunter.y - target.y > 5: #Knock up right
				target.x += knock_back *.707
				target.y -= knock_back *.707
			elif target.y - hunter.y > 5: #Knock down right
				target.x += knock_back *.707
				target.y += knock_back *.707				
			else:
				target.x += knock_back
		elif hunter.y - target.y > 5: #Knock up
			target.y -= knock_back
		elif target.y - hunter.y > 5:  #Knock down
			target.y += knock_back
		else:
			target.y += knock_back		
def Invulnerable(obj):
	if obj.invul > 0:
		obj.invul -= 1

def Hunt(hunter, target):
	if not target.invul: 
		if hunter.y>target.y:
			if hunter.x>target.x: #Move Up-Left
				hunter.x -= hunter.speed * 0.707
				hunter.y -= hunter.speed * 0.707
			elif hunter.x<target.x: #Move Up-Right
				hunter.x += hunter.speed * 0.707
				hunter.y -= hunter.speed * 0.707
			else: #Move Up 
				hunter.y -= hunter.speed
		elif hunter.y<target.y:
			if hunter.x>target.x: #Move Down-Left
				hunter.x -= hunter.speed * 0.707 
				hunter.y += hunter.speed * 0.707
			elif hunter.x<target.x: #Move Down-Right
				hunter.x += hunter.speed * 0.707
				hunter.y += hunter.speed * 0.707
			else: #Move Down
				hunter.y += hunter.speed 
		elif hunter.x>target.x: #Move Left
			hunter.x -= hunter.speed
		elif hunter.x<target.x: #Move Right
			hunter.x += hunter.speed

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))
        		Hunt(Blue_Guy, Red_Guy)
	        	Object_Collision(Blue_Guy, Red_Guy)
		Invulnerable(Red_Guy)

		pygame.display.flip()

main()

Comments