Intro to gaming with Python

The last post ended with a link to a Python game.  In the talk I gave introducing Python there was a lot more delving into what the actual game did, but it was a more interactive approach were there could be more dialog on how the intro slides were actually applying to the small snake game.  Blogging does not lend itself to that same sort of interactivity.

However blogging does allow me to actually break down the game in a way that would have just run out of time during the live talk. Which is what I did in this entry.

Here is another slide from the talk I gave:

Slide17

This slide was not my own creation.  MikeD helped me explain the actual high level architecture of a simple video game.  The snake game itself mostly follows these principles.  I will refer back to the diagram on the slide during most of the rest of this entry.

Again here is the full source: https://bitbucket.org/sirchristian/snakes and the version of the code that this entry talks about specifically can be found at the ‘presentation version’ tag here: https://bitbucket.org/sirchristian/snakes/src/083c6db7b76e

Start

The main file for the snake program is ‘python.py’ (aside: naming your python file python.py really makes tab completion more difficult than it should be). Open that up and you will see the first thing that the Python interpreter will execute.

import pygame
from random import randint
from myobjects.snake import snake
from myobjects.food import food

# Settings
SCREENSIZE = (1024,768)
BGCOLOR = (0xff, 0xa5, 0x00)
FRAMERATE = 30

The first few statements are telling the interpreter what other libraries the program will need to use. The two recommended ways to include other libraries are using the ‘import x’ statement which will import the whole module/package and the ‘from x import y’ statement which lets you pick specific items to import and import them directly into the namespace of the module importing them.

The second block of code is defining what the program will use as constants. Note: the constants are only a convention in Python.

if __name__ == '__main__':
playGame()

The next code that will get run is at the bottom of the file.  Everything in between the imports/constants and the block of code above is a function definition. The functions are loaded into the namespace, but they are not executed. One thing to notice in the above code are ‘__name__’ and ‘__main__’. Identifiers that start and end in double underscores is a convention that says they are special Python symbols.  In this case all we are checking to see is if the current file is the file that was passed directly to the interpreter (not imported from anywhere else) then if it is we will execute the playGame() function.

Load Assets

Next let’s look at the playGame function.

def playGame():
# define the arrow keys
arrow_keys = (pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT)

# init pygame
pygame.init()
game_surface = pygame.display.set_mode(SCREENSIZE)
# set the key repeat speed
pygame.key.set_repeat(10, 25)
# set the caption
pygame.display.set_caption('SNAKES!!!! - Press <Esc> to Quit, <R> to Restart')

The code above is the first part of the playGame function. The first thing playGame does is initialize everything needed to play the game. Most of the hardware assets get loaded/initialized during the pygame.init() call. There is a game surface that is being created. By default pygame disables key repeating so pressing a key and holding it gets counted as 1 key press. This functionality is not desirable for the snake game as the controls for moving the snake will be the arrow keys. Most people will be used to just pressing and holding the arrow keys to create motion. The last bit if initialization is setting a caption for our game window.

Game Loops(s)

In the snake game there are 2 nested loops.  The outer loop is a ‘replay’ loop. This allows for the game to be played multiple times (the game does not allow the user to actually win, so allowing a replay at least makes losing more fun). The inner loop is what is more like a typical ‘main game loop’.


replay = True
while replay:
replay = False
# ... snip ...
# objects that need to be instantiated
# ... end snip ...
game_clock = pygame.time.Clock()
playing = True
while playing:
game_clock.tick(FRAMERATE)

That is the main outline of the game structure. Nothing too fancy there you can see the main playing loop nested inside a replay loop. There is some extra initialization that goes on between the two loops. If you go to the game on bitbucket the objects that are instantiated are the 2 snakes that are playing against each other [the ‘good’ snake is the python (obviously) the ‘bad’ snake is a rattle snake].

There is also some clock logic going on in there. This is the game clock. Its not strictly necessary to use it, but the human brain/eyes are limited to seeing things 30 times per second (generally). Rather than chew up CPU and other resources displaying things that nobody will see we just tick the clock 30 times a second.

Artificial Intelligence

Artificial Intelligence in terms of gaming is where the computer figured out what it needs to do. The simple snake game using the word ‘intelligence’ is a stretch.


# always move the bad snake
rattle_num_frames_in_dir = rattle_num_frames_in_dir + 1
moved_rattle = rattle_num_frames_in_dir % 2 == 0
while not moved_rattle:
if rattle_num_frames_in_dir > rattle_max_frames_in_dir:
curr_dir = rattle_dir
while curr_dir == rattle_dir:
rattle_dir = arrow_keys[randint(0,3)]
rattle_max_frames_in_dir = randint(25,50)
rattle_num_frames_in_dir = 0
moved_rattle = rattle.move(rattle_dir,game_surface)
if not moved_rattle:
rattle_num_frames_in_dir = rattle_max_frames_in_dir+1

The rattle snake in the snake game is the snake that is controlled by the computer. Every frame we want to move the the rattle snake just a little bit. The rattle snake has very simple rules. It must move in the same direction for 25-50 frames, then change to a random direction. The rattle.move call will return false if the snake could not move for some reason (like it is trying to move off screen). Pretty basic. An interesting modification to the snake program would be adding some more logic that feels more like AI to the rattle snake (have the rattle snake actively try and eat the food, or actively try to hunt the other snake would be cool, however I leave this as a later exercise).

Physics

Here is where things get pretty interesting from the point of view of making the video game feel like a video game. In the snake program there are 2 kinds of physics going on. The first is moving all the objects where the need to be moved in the frame. The second form of physics is the collision detection.

# detect collisions with the bad snake
for r1,r2 in ((r1,r2)
for r1 in python.get_rects()
for r2 in rattle.get_rects()):
if r1.colliderect(r2):
playing = False
replay = gameOver(game_surface)
break

This piece of code is demonstrating how to use generator expressions in Python. Which is closely related to list comprehension with Python. In Python syntax like this: [x for x in range(10)] will return a list. Replace the outer square brackets with parentheses and you get a generator object. Looking at the piece of code above there is a call to get_rects (which returns a generator object) for the python and a call to get_rects for the rattle. The calls are being used inside a generator expressions that will return a new generator object. That new generator object is what is being looped over. This allows us to check each rectangle of the python with each rectangle of the rattle for a collision.

If the concepts of list generation/generator expressions are new to you I recommend looking over that piece of collision detection code again. There is a lot going on there, but once you ‘get it’ there are some powerful things that you can do. In an earlier version of the code this piece of code was a complex nested loop. The new version I find must more readable.


# detect collisions with ourself
if python.head_hit_body():
playing = False
replay = gameOver(game_surface)
break

# eat
python.try_eat(food_items)
rattle.try_eat(food_items)

# replace food
create_food()

The next set of collision detections are mostly encapsulated inside head_hit_body and try_eat. Those calls are simple collisions detections that iterating over the objects and detect of there was a collision on each iteration.

The call to create_food is a stretch to call physics. It creates objects if needed. The interesting piece to note in terms of Python development is that create_food is technically a closure. The other thing I would like to call out is if you pull down the snake game, trying to replace the hard coded ‘2’ in the create_food function with something large (like 50) creates a very different game play. Trust me, try it!

Draw Frame

Here is where we get more into the technical aspects of a game. Everything we did above with moving objects, deleting objects and creating objects were done in the background. At this point in the frame the objects are all updated to the new state for the frame, but are not yet visible to the user. In order to do that we need to blit the objects onto the game surface. Then we need to update the display.

# update display & objects
python.update(game_surface)
rattle.update(game_surface)
for f in food_items:
f.update(game_surface)
pygame.display.update()

Every object we have has an update method that take the surface in as an argument. Those functions know what rectangles they used and can blit them onto the surface. Take a look at the food update() function.

def update(self, game_surface):
""" Updates the food """
rect = self.surface.get_rect().move(self.pos)
game_surface.blit(self.surface, rect)

All the update function does is make sure the rectangles are in position and blits them. The update function in snake is slightly more invoked, but not by much. While the code for this looks pretty simplistic, it is actually very important to get this right. Most times when developing your game (at least in pygame) when the objects aren’t moving, or aren’t behaving properly it is prudent to make sure that this steps is being done correctly.

Once the game surface is updated a call to pygame.display.update() will actually update the display that the user sees

Detect and handle user input

The snake game actually does all its event processing at the beginning of the main game loop.

# handle events
for e in pygame.event.get():
if e.type == pygame.QUIT:
playing = False
elif e.type == pygame.KEYDOWN:
if e.key == pygame.K_ESCAPE:
playing = False
replay = False
if e.key == pygame.K_r:
playing = False
replay = True
if e.key in arrow_keys:
python.move(e.key,game_surface)

The snake game handles a quit event (i.e. the user closed the game by using the ‘X’ on the game window) and a few keyboard events. If the escape key was his the game will exit. If the ‘R’ key was hit the game will replay. If any arrow keys were hit the python snake will be asked to move. The move function was already touched upon above during the AI. All it does is move the snake in a direction as long as it doesn’t go off screen.

Closing

That the end of the brief tour of game development with Python. I encourage you to download the snake game a play with it a bit. Once you are comfortable with the basic concepts pygame has much more to offer.

Leave a Reply