PyGame AI Guide¶
This guide is meant to explain the basic concepts you need to know to integrate this library into your game; it is not meant to explain how to create games with PyGame, although it does include a very basic game structure that you can use as a template.
Contents¶
Game Structure¶
This is the basic game structure that I’ll be working with in this guide, it contains the basic things that any PyGame game should have.
import sys
import pygame
from pygame.locals import *
import pygame_ai as pai
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# Custom classes and definitions
# ...
def main():
# Create screen
screen_width, screen_height = 800, 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('PyGame AI Guide')
# Create white background
background = pygame.Surface((screen_width, screen_height)).convert()
background.fill((255, 255, 255))
# Initialize clock
clock = pygame.time.Clock()
# Variables that you will use in your game loop
# ...
# Game loop
while True:
# Get loop time, convert milliseconds to seconds
tick = clock.tick(60)/1000
# Handle input
for event in pygame.event.get():
if event.type == QUIT:
sys.exit(2)
# Erase previous frame by bliting background
screen.blit(background, background.get_rect())
# Update the entities in your game
# ...
# Blit all your entities
# . . .
# Update display
pygame.display.update()
if __name__ == '__main__':
pygame.init()
main()
pygame.quit()
With this in place we can move on.
Game Objects¶
I used the word entities before, by that I mean all the moving things
in your game, like the player, enemies, NPCs, you name it. The way these
entities will be better represented (and the only way they should be,
unless you really know what you’re doing) in a game that uses this library is
by using the GameObject
class, it contains all the
necessary properties and methods the library uses to do all its calculations.
The way you create your own entities is by subclassing GameObject
like this:
class Player(pai.gameobject.GameObject):
def __init__(self, pos = (0, 0)):
# First we create the image by filling a surface with blue color
img = pygame.Surface( (10, 15) ).convert()
img.fill(BLUE)
# Call GameObject init with appropiate values
super(Player, self).__init__(
img_surf = img,
pos = pos,
max_speed = 15,
max_accel = 40,
max_rotation = 40,
max_angular_accel = 30
)
Note that the first thing I do is create a pygame.Surface; this is the
image that will be displayed in the game, in this case it is a blue rectangle.
Also note that we hand picked most of the parameters for the GameObject
;
this is unfortunately still done through trial and error, the values that I
used work sort of smoothly but they can be improved.
Now, that is not enough to call it done; we still need to implement a way for
this entity to move, this is usually done through an update function, in fact,
every GameObject
has one, but it doesn’t do anything.
This is an example Player with its update function:
class Player(pai.gameobject.GameObject):
def __init__(self, pos = (0, 0)):
# First we create the image by filling a surface with blue color
img = pygame.Surface( (10, 15) ).convert()
img.fill(BLUE)
# Call GameObject init with appropiate values
super(Player, self).__init__(
img_surf = img,
pos = pos,
max_speed = 15,
max_accel = 40,
max_rotation = 40,
max_angular_accel = 30
)
def update(self, steering, tick):
self.steer(steering, tick)
self.rect.move_ip(self.velocity)
Essentially what it does is to accelerate the entity in the direction and strength dictated by the steering parameter and then move the entity’s rect with the direction and strength of the entity’s velocity.
Steering¶
Steering Algorithms are the core of movement in this library; the
SteeringOutput
is the way these algorithms communicate how
an object should accelerate in order to achieve its goal.
In the previous example we saw that the player does not produce its
own steering, that is because normally the player is controlled by
user input; we’ll see later how we can create and modify our own
SteeringOutput
to move the player, for now let’s
move on and actually implement something useful with this library.
Steering as an AI¶
You can have NPCs whose behavior is only composed by a KinematicSteeringBehavior
,
this would be a very simple but often useful AI design, this is how you can
implement an NPC whose only AI behavior is a KinematicSteeringBehavior
:
class CircleNPC(pai.gameobject.GameObject):
def __init__(self, pos = (0, 0)):
# First create the circle image with alpha channel to have transparency
img = pygame.Surface( (10, 10) ).convert_alpha()
img.fill( (255, 255, 255, 0) )
# Draw the circle
pygame.draw.circle(img, RED, (5, 5), 5)
# Call GameObject init with appropiate values
super(CircleNPC, self).__init__(
img_surf = img,
pos = pos,
max_speed = 25,
max_accel = 40,
max_rotation = 40,
max_angular_accel = 30
)
# Create a placeholder for the AI
self.ai = pai.steering.kinematic.NullSteering()
def update(self, tick):
steering = self.ai.get_steering()
self.steer(steering, tick)
self.rect.move_ip(self.velocity)
The main differences between this and the Player entity are that:
- The image is a circle
- The update actually generates its own steering
The way that point (2) is achieved is by calling the KinematicSteeringBehavior
’s
get_steering()
method; this returns
the behaviors’ SteeringOutput
; it is then applied to the
GameObject
with the steer()
method.
This is not the only steering method that exists, we will see more about these
methods later.
Very Simple Game¶
With all we have learned so far, we can make a very simple game that consists only of one input-controlled player and one NPC that chases the player.
First we need to see how to make the player input-controlled, for that
we need to create an artificial SteeringOutput
that we can modify, let’s add that:
# . . .
# Variables that you will use in your game loop
# Create player steering
player_steering = pai.steering.kinematic.SteeringOutput()
# Game loop
while True:
# Get loop time, convert milliseconds to seconds
tick = clock.tick(60)/1000
# Restart player steering
player_steering.reset()
# Handle input
for event in pygame.event.get():
if event.type == QUIT:
sys.exit(2)
# . . .
With that we simply created an empty SteeringOutput
that gets reset()
every frame, this
is to guarantee that it will not grow infinitely.
Then we need to catch user input and modify the steering accordingly, this piece of code is horrible but I’ll use it to avoid complicating the guide with things that do not relate to the library.
# . . .
# Handle input
for event in pygame.event.get():
if event.type == QUIT:
sys.exit(2)
keys = pygame.key.get_pressed()
if keys[K_w]:
player_steering.linear[1] -= player.max_accel
if keys[K_a]:
player_steering.linear[0] -= player.max_accel
if keys[K_s]:
player_steering.linear[1] += player.max_accel
if keys[K_d]:
player_steering.linear[0] += player.max_accel
# . . .
Now it is time to actually apply this to the player using the update method that we wrote, for that we first need to instantiate the Player class that we created. We will also need to instantiate the CircleNPC class and do the same with it:
# . . .
# Variables that you will use in your game loop
# Create player steering
player_steering = pai.steering.kinematic.SteeringOutput()
# Instantiate game objects
player = Player(pos = (screen_width//2, screen_height//2))
circle = CircleNPC(pos = (screen_width//4, screen_height//2))
# Set the NPC AI
circle.ai = pai.steering.kinematic.Arrive(circle, player)
# Game loop
while True:
# . . .
# Erase previous frame by bliting background
screen.blit(background, background.get_rect())
# Update player and NPCs
player.update(player_steering, tick)
circle.update(tick)
# Blit all your entities
screen.blit(player.image, player.rect)
screen.blit(circle.image, circle.rect)
pygame.display.update()
Apart from instantiating the CircleNPC we changed its ai property to be
Arrive
, this will make the NPC arrive near the
player and then stop accelerating, We also added the code necessary to
blit our entity’s images to the screen.
If you were to run this code now, it should run properly, you should
see the player in the center of the screen and the NPC chasing the player,
the problem is, if you try to move the player, well… it won’t stop moving.
That is because when we steer()
the player its
velocity increases, and since we are moving its position
based on its velocity, it will never stop moving (unless you steer it correctly
to negate its current velocity). The NPC will also never stop beside
the player as it should.
Drag¶
To avoid this behavior we need to apply some sort of Drag to our
entities, luckily, I’ve implemented a KinematicSteeringBehavior
that does just that, enter Drag
.
The only particular thing about this behavior is that you will not normally create an individual instance for every entity, instead you should create one for every surface or environment your entity is in. This is because an entity will have less drag trying to run in plain land than trying to run with its body half-submerged in water.
For this example we are using only one instance of Drag
and
applying it to all entities, but you can get creative.
# . . .
# Create drag
drag = pai.steering.kinematic.Drag(15)
# Game loop
while True:
# . . .
# Erase previous frame by bliting background
screen.blit(background, background.get_rect())
# Update player and NPCs
player.update(player_steering, tick)
circle.update(tick)
# Apply drag
player.steer(drag.get_steering(player), tick)
circle.steer(drag.get_steering(circle), tick)
# Blit all your entities
screen.blit(player.image, player.rect)
screen.blit(circle.image, circle.rect)
pygame.display.update()
Now you will be able to run the code and it should behave as expected. Note that you can totally add the drag instructions in your entity’s update function to make the code less cluttered; I just added it there to avoid having to pass it as an argument or putting it inside the class.
That was a very basic game, and you should be able to use most of the library’s
movement behaviors only with that, the only thing that is a little different
is the Path
class used by the FollowPath
behavior.
Paths¶
You can create very light-weight paths using the Path
class,
the only “problem” is that the paths are defined as mathematic functions,
for people unfamiliar with that it can be quite spooky, and I would recommend them
to use the pre-implemented paths. Otherwise it is very easy to define paths
with this class; let’s define a very simple cosine-wave-shaped path and
make an NPC follow it:
import math
# . . .
class PathCosine(pai.steering.path.Path):
def __init__(self, start, height, length):
self.start = start
self.height = height
self.length = length
def cosine_path(self, x):
y = self.start[1] + math.cos(x) * self.height
return x, y
super(PathCosine, self).__init__(
path_func = cosine_path,
domain_start = int(self.start[0]),
domain_end = int(self.start[0] + length),
increment = 30
)
And that is it, the Path
class handles everything, you just
need to specify the function, its domain and a discrete increment (for each
point to be generated).
Now we need to put it into the game, let’s create another NPC instance
and assign FollowPath
with our PathCosine to its
AI behavior.
# . . .
# Instantiate game objects
player = Player(pos = (screen_width//2, screen_height//2))
circle = CircleNPC(pos = (screen_width//4, screen_height//2))
circle2 = CircleNPC(pos = (screen_width//5, screen_height//2))
# Set the NPC AI
circle.ai = pai.steering.kinematic.Arrive(circle, player)
path_cosine = PathCosine(
start = circle2.position,
height = 200,
length = 500
)
circle2.ai = pai.steering.kinematic.FollowPath(circle2, path_cosine)
# . . .
Remember to also update, blit and apply drag to circle2.
# . . .
# Update player and NPCs
player.update(player_steering, tick)
circle.update(tick)
circle2.update(tick)
# Apply drag
player.steer(drag.get_steering(player), tick)
circle.steer(drag.get_steering(circle), tick)
circle2.steer(drag.get_steering(circle2), tick)
# Blit all your entities
screen.blit(player.image, player.rect)
screen.blit(circle.image, circle.rect)
screen.blit(circle2.image, circle2.rect)
# . . .
Now you should see an NPC that follows a cosine-wave like path, it will only
go trough it once, you can use reset()
to make the path
reset at any point, or you can take a look into CyclicPath
and MirroredPath
for special Path implementations.
Other Behaviors¶
Finally, this library also implements a couple of different kinds of
KinematicSteeringBehavior
s which are BlendedSteering
and PrioritySteering
. These allow you to combine different
basic behaviors to create more complicated ones. Take a look at the
pre-implemented behaviors to see what is possible by trying them out.
Gravity¶
Many games include gravity as a core feature (so core that most people won’t consider it a feature). There are a couple of things we need to consider when adding gravity into our game, but here I’ll show a very basic NPC that has gravity applied.
First, if we are going to have falling entities, we need to make sure they don’t fall off-screen. For that we can add a very simple check to make sure nothing moves under the screen:
# . . .
# Entities affected by gravity
gravity_entities = []
# . . .
# Game loop
while True:
# . . .
# Update player and NPCs
player.update(player_steering, tick)
circle.update(tick)
circle2.update(tick)
# Check if our gravity-affected entities are falling off-screen
for gentity in gravity_entities:
if gentity.rect.bottom > screen_height:
gentity.rect.bottom = screen_height
# . . .
Now we need to actually implement an entity that is affected by gravity, let’s make that an NPC:
class GravityCircleNPC(pai.gameobject.GameObject):
def __init__(self, pos = (0, 0)):
# First create the circle image with alpha channel to have transparency
img = pygame.Surface( (10, 10) ).convert_alpha()
img.fill( (255, 255, 255, 0) )
# Draw the circle
pygame.draw.circle(img, RED, (5, 5), 5)
# Call GameObject init with appropiate values
super(GravityCircleNPC, self).__init__(
img_surf = img,
pos = pos,
max_speed = 10,
max_accel = 40,
max_rotation = 40,
max_angular_accel = 30
)
# Create a placeholder for the AI
self.ai = pai.steering.kinematic.NullSteering()
def update(self, tick):
# Gravity steering
gravity = pai.steering.kinematic.SteeringOutput()
gravity.linear[1] = 300 # This value is arbitrary, it just works
# Steer only along x axis
steering = self.ai.get_steering()
self.steer_x(steering, tick)
# Get total velocity considering gravity
velocity = self.velocity + gravity.linear * tick
# Move with that velocity
self.rect.move_ip(velocity)
The only difference between this and the regular CircleNPC is in the
update function; in this one we create a SteeringOutput
to act as the gravity; we then only consider the AI steering along the x axis,
finally we get a total velocity composed of the NPC’s velocity
plus the velocity induced by gravity. This way we separate the velocity
produced by the actual NPC from the one produced by any external force
(in this case gravity).
Now we only need to instantiate this NPC and do all necessary actions to have it function like the rest of the entities (add it to the gravity entities list, assign it an AI behavior, update, blit and apply drag).
# . . .
# Instantiate game objects
player = Player(pos = (screen_width//2, screen_height//2))
circle = CircleNPC(pos = (screen_width//4, screen_height//2))
circle2 = CircleNPC(pos = (screen_width//5, screen_height//2))
circle3 = GravityCircleNPC(pos = (screen_width//6, screen_height//2))
# Remember to add it to our gravity_entitites list for collision
gravity_entities.append(circle3)
# Set the NPC AI
circle.ai = pai.steering.kinematic.Arrive(circle, player)
path_cosine = PathCosine(
start = circle2.position,
height = 200,
length = 500
)
circle2.ai = pai.steering.kinematic.FollowPath(circle2, path_cosine)
circle3.ai = pai.steering.kinematic.Seek(circle3, player)
# . . .
# Game loop
while True:
# . . .
# Update player and NPCs
player.update(player_steering, tick)
circle.update(tick)
circle2.update(tick)
circle3.update(tick)
# Check if our gravity-affected entities are falling off-screen
for gentity in gravity_entities:
if gentity.rect.bottom > screen_height:
gentity.rect.bottom = screen_height
# Apply drag
player.steer(drag.get_steering(player), tick)
circle.steer(drag.get_steering(circle), tick)
circle2.steer(drag.get_steering(circle2), tick)
circle3.steer(drag.get_steering(circle3), tick)
# Blit all your entities
screen.blit(player.image, player.rect)
screen.blit(circle.image, circle.rect)
screen.blit(circle2.image, circle2.rect)
screen.blit(circle3.image, circle3.rect)
# . . .
You can now run the code again and you should see a falling NPC that also seeks the player while staying stuck to the ground.
More Complex Stuff¶
This was a very simple game to show the basic concepts that this library uses. You can download the game here. You only need to run main.py while having pygame and pygame_ai installed.
If you are interested in knowing what else you can do with this library you should check out the Example Game. You can take a look at how I implement things there, but you will find a lot of gibberish that is not directly related to the library.