Making games seems intimidating, especially as most of us didn’t grow up exposed to them extensively. It is a far more accessible art form than people realize, learning how to make something simple and fun is quite rewarding, not to mention nice to show off to other people.

Note: If you’re using WSL/WSL2, this won’t work due to windowing issues, so ensure your install of Python is local. In theory WSLg on Windows11 onwards should work, but I haven’t quite tested it out.

print("Hello pygame!")

Hosted by: Harsh Deep

Motivation

Resources

Installation

pip install pygame

If you run into errors with pip, try running pip3 install pygame instead

To test it’s installed, you can try out games from pygame examples (source code can also be found)

$ python3 -m pygame.examples.aliens

Workshop

Starter code: - includes example sprite images and pygame requirement.

First, we will import the following libraries:

import pygame, sys, random

print("It runs")

Now we can begin coding! Every pygame script begins with the following line:

pygame.init()

This initializes the Pygame library and sets up everything we need to do. We can then:

size = (width, height) = 500, 500
screen = pygame.display.set_mode(size)

Here we are declaring our width, height, and size variables. We we then use these values from the built-in pygame function to create our screen. The = thing on the left hand side is useful for setting two variables to the same value

Every game has what is called a game loop. It’s an infinite loop goes on for the game’s lifespan, and it’s where we put all of our game logic and call our objects.

clock = pygame.time.Clock()
while True:
    clock.tick(60)
    screen.fill((0, 0, 0))
    pygame.display.flip()

We first set our tick rate to 60 frames using the clock.tick() function. The frame rate is how often we redraw the scene per second and the main deciding factor in how fluid your game feels. The higher the better, but this means you have to finish running all your code faster. The general convention is 60Hz, but some consoles work near 30Hz and some modern monitors are based on games running at 144Hz.

We then fill our screen with black for the background and update the display. At this point, you should be unable to see a window on your screen. That’s because we are yet to handle the event queue. Add this into the end of the loop:

for event in pygame.event.get():
    if event.type == pygame.QUIT: 
        sys.exit()

The event queue is a queue of all the events that have occurred in your game from the computer system. We can loop through these and determine if we care about any of these events. Here we only care if the event is ‘QUIT’ (Control+C on your keyboard or X icon on the corner) in which case we dip. The main reason we’re doing this is so that your system doesn’t consider the game is responsive and we can close it quickly.

The events queue can support a large variety of events and interrupts, and those can be useful for the types of games you all try out.

Setting up Main Character

We will use object oriented programming to work with defining our characters. Pygame also lets us extend a Sprite class that provides with a baseline of functionality.

class MainCharacter(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./cat.svg")
        self.image = pygame.transform.scale(self.image, (50, 50))

        self.rect = self.image.get_rect()
        self.rect.center = (250,250)

Each sprite has to have an image (how it looks) and a rect (where to put it) so that pygame can draw it out. We’ve also defined self.rect.center so the sprite will start in the middle. Feel free to replace the image with anything of your own.

Putting the main character in a group

In pygame, groups are used to ‘group’ similar characters together to operate on them all at once. Let’s define a group for the main character before the game loop:

main = MainCharacter()

main_sprite = pygame.sprite.Group()
main_sprite.add(main)

These will be useful when working with interactions between different groups of sprites.

Drawing The Character

Once our MainCharacter is in a group, we can also draw it out. Insert the following code inside of your loop before flip(). You should now see an image in the center of your screen.

main_sprite.draw(screen)

At this point your code should look like:

import sys, pygame, random, os

pygame.init()

size = width, height = 500, 500
screen = pygame.display.set_mode(size)
class MainCharacter(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./cat.svg")
        self.image = pygame.transform.scale(self.image, (50, 50))

        self.rect = self.image.get_rect()
        self.rect.center = (250,250)

main = MainCharacter()

main_sprite = pygame.sprite.Group()
main_sprite.add(main)

clock = pygame.time.Clock()
while 1:
    clock.tick(60)
    screen.fill((0, 0, 0))
    main_sprite.draw(screen)
    pygame.display.flip()

Handling keyboard input

Introduce a speed array near the top of your code:

speed = [0,0]

We will use this to handle the main character’s speed as the player moves them with the keyboard. Add the following to your game loop after draw:

key = pygame.key.get_pressed()

if key[pygame.K_a] and speed[0] > -20:
    speed[0] -= 1.5
if key[pygame.K_d] and speed[0] < 20:
    speed[0] += 1.5
if key[pygame.K_w] and speed[1] > -20:
    speed[1] -= 1.5 
if key[pygame.K_s] and speed[1] < 20:
    speed[1] += 1.5 

main.rect = main.rect.move(speed)

pygame.key.get_pressed returns a dictionary with whether or not that key has been pressed. We can then handle the case for each key.

Now the main character should slow down after the player releases the key. But we can still go off screen. To fix that, we’ll make the main character bounce at the boundaries of the screen:

[...]
if main.rect.left < 0 or main.rect.right > width:
    speed[0] = -speed[0] * 1.5
if main.rect.top < 0 or main.rect.bottom > height:
    speed[1] = -speed[1] * 1.5
    
    
main.rect = main.rect.move(speed)

However, once you click, it doesn’t slow down. So let’s introduce some slow-down behavior:

if speed[0] > 0:
    speed[0] -= 0.5
if speed[0] < 0:
    speed[0] += 0.5

if speed[1] > 0:
    speed[1] -= 0.5
if speed[1] < 0:
    speed[1] += 0.5

main.rect = main.rect.move(speed)


Your code so far should look something like:

import pygame, sys, random

print("It runs!")

pygame.init()

size = (width, height) = 500, 500
screen = pygame.display.set_mode(size)

speed = [0, 0]

class MainCharacter(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./cat.svg")
        self.image = pygame.transform.scale(self.image, (50, 50))

        self.rect = self.image.get_rect()
        self.rect.center = (250, 250)


main = MainCharacter()
main_sprite = pygame.sprite.Group()
main_sprite.add(main)

clock = pygame.time.Clock()
while True:
    clock.tick(60)
    screen.fill((0, 0, 0))
    
    main_sprite.draw(screen)

    key = pygame.key.get_pressed()

    if key[pygame.K_a] and speed[0] > -20:
        speed[0] -= 1.5
    if key[pygame.K_d] and speed[0] < 20:
        speed[0] += 1.5
    if key[pygame.K_w] and speed[1] > -20:
        speed[1] -= 1.5 
    if key[pygame.K_s] and speed[1] < 20:
        speed[1] += 1.5 

    if main.rect.left < 0 or main.rect.right > width:
        speed[0] =  -speed[0] * 1.5
    if main.rect.top < 0 or main.rect.bottom > height:
        speed[1] = -speed[1] * 1.5

    if speed[0] > 0:
        speed[0] -= 0.5
    if speed[0] < 0:
        speed[0] += 0.5

    if speed[1] > 0:
        speed[1] -= 0.5
    if speed[1] < 0:
        speed[1] += 0.5
    
    main.rect = main.rect.move(speed)

    pygame.display.flip()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

Enemy Class

Now we want to make a game about avoiding covid19, so we make another type of sprite of which we’ll have 3 of. This will be quite similar to last time and you can parallel most of the logic quite similary.

class Corona(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./image0.jpg")
        self.image = pygame.transform.scale(self.image, (10, 10))
        self.rect = self.image.get_rect()
        self.speed = [3, 3]
        self.rect.center = (random.randint(0,500), random.randint(0,500))


We can then add them to a group. Let’s make a group and add three enemies to it:

main = MainCharacter()
main_sprite = pygame.sprite.Group()
main_sprite.add(main)

bad_sprites = pygame.sprite.Group()

for i in range(0, 3):
    bad_sprites.add(Corona())

And in the game loop we add a draw:

main_sprite.draw(screen)
bad_sprites.draw(screen)

Now you should see them on the screen at random starting points!

If we introduce the line inside of our game loop followed by a bad_sprites.draw(screen) we should see them moving around the screen.

Enemy Movement

This code is similar to the main character code we implemented, except for the update method. The update method is inherited from the sprite class, and we can call update on an entire group using this special named function. It will run that update method for each individual sprite. We are also using random.uniform() to introduce directional variation in our bad guys.

class Corona(pygame.sprite.Sprite):
    [...]

    def update(self):
        self.rect = self.rect.move(self.speed)

        if self.rect.left < 0 or self.rect.right > width:
            self.speed[0] =  -self.speed[0] * 1.5
            self.speed[1] = self.speed[1] * random.uniform(0.5, 1.1)
        if self.rect.top < 0 or self.rect.bottom > height:
            self.speed[1] = -self.speed[1] * 1.5
            self.speed[0] = self.speed[0] * random.uniform(0.5, 1.1)

        if self.speed[0] > 6:
            self.speed[0] -= 0.5
        if self.speed[0] < -6:
            self.speed[0] += 0.5

        if self.speed[1] > 6:
            self.speed[1] -= 0.5
        if self.speed[1] < -6:
            self.speed[1] += 0.5

In the game loop, after we handle main character movement, we can add:

main.rect = main.rect.move(speed)
bad_sprites.update()

Now you should be seeing everything moving!

Your code now should look like this:

import pygame, sys, random

print("It runs!")

pygame.init()

size = (width, height) = 500, 500
screen = pygame.display.set_mode(size)

speed = [0, 0]

class MainCharacter(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./cat.svg")
        self.image = pygame.transform.scale(self.image, (50, 50))

        self.rect = self.image.get_rect()
        self.rect.center = (250, 250)

class Corona(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./germ.svg")
        self.image = pygame.transform.scale(self.image, (20, 20))

        self.rect = self.image.get_rect()
        self.rect.center = (random.randint(0, 500), random.randint(0, 500))
        self.speed = [3, 3]

    def update(self):
        self.rect = self.rect.move(self.speed)

        if self.rect.left < 0 or self.rect.right > width:
            self.speed[0] =  -self.speed[0] * 1.5
            self.speed[1] = self.speed[1] * random.uniform(0.5, 1.1)
        if self.rect.top < 0 or self.rect.bottom > height:
            self.speed[1] = -self.speed[1] * 1.5
            self.speed[0] = self.speed[0] * random.uniform(0.5, 1.1)

        if self.speed[0] > 6:
            self.speed[0] -= 0.5
        if self.speed[0] < -6:
            self.speed[0] += 0.5

        if self.speed[1] > 6:
            self.speed[1] -= 0.5
        if self.speed[1] < -6:
            self.speed[1] += 0.5


main = MainCharacter()
main_sprite = pygame.sprite.Group()
main_sprite.add(main)

bad_sprites = pygame.sprite.Group()
for _ in range(3):
    bad_sprites.add(Corona())

clock = pygame.time.Clock()
while True:
    clock.tick(60)
    screen.fill((0, 0, 0))
    
    main_sprite.draw(screen)
    bad_sprites.draw(screen)

    key = pygame.key.get_pressed()

    if key[pygame.K_a] and speed[0] > -20:
        speed[0] -= 1.5
    if key[pygame.K_d] and speed[0] < 20:
        speed[0] += 1.5
    if key[pygame.K_w] and speed[1] > -20:
        speed[1] -= 1.5 
    if key[pygame.K_s] and speed[1] < 20:
        speed[1] += 1.5 

    if main.rect.left < 0 or main.rect.right > width:
        speed[0] =  -speed[0] * 1.5
    if main.rect.top < 0 or main.rect.bottom > height:
        speed[1] = -speed[1] * 1.5

    if speed[0] > 0:
        speed[0] -= 0.5
    if speed[0] < 0:
        speed[0] += 0.5

    if speed[1] > 0:
        speed[1] -= 0.5
    if speed[1] < 0:
        speed[1] += 0.5
    
    main.rect = main.rect.move(speed)
    bad_sprites.update()

    pygame.display.flip()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

Handling Collisions

When the main character collides with a bad guy, we want them to lose a life. There are several ways of doing this, bbut one way we can do is:

class MainCharacter(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./cat.svg")
        self.image = pygame.transform.scale(self.image, (50, 50))

        self.rect = self.image.get_rect()
        self.rect.center = (250, 250)

        self.total_lives = 5
        self.last_hit = 0

    def update(self):
        self.last_hit = pygame.time.get_ticks()
        self.total_lives -= 1 

        if self.total_lives <= 0:
            print("Game over!")
            sys.exit()

This will close the game once the player runs out of lives. Ticks are a milisecond count of time in the game and we use this to determine intervals.

We can then insert the following into our game loop:

bad_sprites.update()

collisions = pygame.sprite.spritecollideany(main, bad_sprites) 
if collisions != None:
    interval = pygame.time.get_ticks() - main.last_hit 
    if interval > 1000: # one second 
        main_sprite.update()

pygame.sprite.spritecollideany(main, bad_sprites) returns either None or a sprite name that collided. If it’s not None, we know there is a collision, and if it’s outside of the grace period of the last_hit parameter we added, we go ahead and call update to deduct a life. If we don’t add the interval, a single collision will lose even more lives.

Note: This is a simple way of handling collisions which will work for any toy project you have, but there are a lot of techniques that are better here.

Using text to display lives

Insert at towards the end of your game loop:

bad_sprites.update()

myFont = pygame.font.SysFont("Times New Roman", 18)
numLivesDraw = myFont.render(f"{main.total_lives} remaining", 1, (250, 250, 250))
screen.blit(numLivesDraw, (30, 30))

In line 2, we declare how we want to render the text. In line 3, we’re actually adding it to the screen by using blit() to draw things on the existing screen.

And we’re done! Final Code

The overall code should look like:

import pygame, sys, random

print("It runs!")

pygame.init()

size = (width, height) = 500, 500
screen = pygame.display.set_mode(size)

speed = [0, 0]

class MainCharacter(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./cat.svg")
        self.image = pygame.transform.scale(self.image, (50, 50))

        self.rect = self.image.get_rect()
        self.rect.center = (250, 250)

        self.total_lives = 5
        self.last_hit = 0

    def update(self):
        self.last_hit = pygame.time.get_ticks()
        self.total_lives -= 1 

        if self.total_lives <= 0:
            print("Game over!")
            sys.exit()

class Corona(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load("./germ.svg")
        self.image = pygame.transform.scale(self.image, (20, 20))

        self.rect = self.image.get_rect()
        self.rect.center = (random.randint(0, 500), random.randint(0, 500))
        self.speed = [3, 3]

    def update(self):
        self.rect = self.rect.move(self.speed)

        if self.rect.left < 0 or self.rect.right > width:
            self.speed[0] =  -self.speed[0] * 1.5
            self.speed[1] = self.speed[1] * random.uniform(0.5, 1.1)
        if self.rect.top < 0 or self.rect.bottom > height:
            self.speed[1] = -self.speed[1] * 1.5
            self.speed[0] = self.speed[0] * random.uniform(0.5, 1.1)

        if self.speed[0] > 6:
            self.speed[0] -= 0.5
        if self.speed[0] < -6:
            self.speed[0] += 0.5

        if self.speed[1] > 6:
            self.speed[1] -= 0.5
        if self.speed[1] < -6:
            self.speed[1] += 0.5


main = MainCharacter()
main_sprite = pygame.sprite.Group()
main_sprite.add(main)

bad_sprites = pygame.sprite.Group()
for _ in range(3):
    bad_sprites.add(Corona())

clock = pygame.time.Clock()
while True:
    clock.tick(60)
    screen.fill((0, 0, 0))
    
    main_sprite.draw(screen)
    bad_sprites.draw(screen)

    key = pygame.key.get_pressed()

    if key[pygame.K_a] and speed[0] > -20:
        speed[0] -= 1.5
    if key[pygame.K_d] and speed[0] < 20:
        speed[0] += 1.5
    if key[pygame.K_w] and speed[1] > -20:
        speed[1] -= 1.5 
    if key[pygame.K_s] and speed[1] < 20:
        speed[1] += 1.5 

    if main.rect.left < 0 or main.rect.right > width:
        speed[0] =  -speed[0] * 1.5
    if main.rect.top < 0 or main.rect.bottom > height:
        speed[1] = -speed[1] * 1.5

    if speed[0] > 0:
        speed[0] -= 0.5
    if speed[0] < 0:
        speed[0] += 0.5

    if speed[1] > 0:
        speed[1] -= 0.5
    if speed[1] < 0:
        speed[1] += 0.5
    
    main.rect = main.rect.move(speed)
    bad_sprites.update()

    myFont = pygame.font.SysFont("Times New Roman", 18)
    numLivesDraw = myFont.render(f"{main.total_lives} remaining", 1, (250, 250, 250))
    screen.blit(numLivesDraw, (30, 30))

    collisions = pygame.sprite.spritecollideany(main, bad_sprites) 
    if collisions != None:
        interval = pygame.time.get_ticks() - main.last_hit 
        if interval > 1000: # one second 
            main_sprite.update()

    pygame.display.flip()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

Congrats! Have fun with your game and feel free to expand on it or try something fun on your own.

Learn more

Ethics

  • What we still haven’t learned from Gamergate - the rise of gaming and gaming subcultures is a really cool way computer science has hit the world today, but it also has had many issues with gender diversity. Here’s a hindsight look from a large scale issue from 2014 and its long term impact.

Open Source - Permissive vs Copyleft Licenses

There are two broad categories in open-source licenses that most licenses fall under: Permissive and Copyleft.

Permissive licenses include MIT and ApacheV2. These basically say do whatever you want with the code, and if you use it somewhere else, you often have to include the license. These are popular for small projects when people don’t care too much about what others do and want their open-source software to be good enough for commercial use. The workshop repos use this, for example.

Copyleft licenses are stronger, such as GPLv3, where they want future software development to also have the same freedoms the software was available under. Basically, that means that any work including it also has to be open source. This is often used by large projects like Linux or by people who want to increase the spread of Open Source. However, this type of license makes it hard for commercial use since most commercial software is proprietary (i.e., not open source). The repo for this website actually uses GPLv3, as the template we used to make this uses GPLv3.

For these your open source projects, use any license as long as it’s open source.

There is much debate on these two and these two links are a good start to read more:

Ideas

Feel free to come up with anything you want as long as it’s game related. Here are some ideas to help you get started, but feel free to come up with more. Don’t worry if it’s already been done or if someone else is doing it. The point is learning and fun. :)

  • What kind of games do you like to play? Could you make a simplified version of one of them?

  • Were there any examples that you found interesting? Could you recreate some of that functionality on your own (without copying, of course)?

  • It doesn’t have to be a game (though that is more fun), it can even just be some animation or some interactive art.

  • Have fun with images and custom backgrounds too, for example a main character can have your profile picture or something.

  • Try to add some humor to it. Often a simple game becomes a lot more fun if you add something unique or some type of inside joke to it.

Requirements

  • It should use pygame.

  • You should show it off in your README.md file, with animations/video and an explanation of the game.

  • Has to use an Open Source license via a LICENSE file

  • Have a README.md with explanation about what your project does, how to install it, how to contribute, and how to use it. Check out HackerGirl’s Art of Readme.

Contributors: Ahmed, Harsh, Maaheen, Saurav