Linux Format

Code a 2D shooter in Python

Putting the shooter into top-down shooter, Calvin Robinson brings us another game programmin­g tutorial: Reservoir Bunnies.

- Calvin Robinson is a former Assistant Principal and Computer Science teacher with a degree in Computer Games Design and Programmin­g BSC (Hons). Twitter.com/ Calvinrobi­nson.

Putting the shooter into top-down shooter, Calvin Robinson brings us another game programmin­g tutorial, Reservoir Bunnies.

We’re going to have a go at creating our own 2D shooter. We’ll be using Python for this tutorial, so make sure it’s installed and updated, along with the Pygame module. pip3 install pygame should get everything set up. We’ll need some image and sound resource files which can be downloaded from the LXF archives website, along with a complete copy of the source code for reference.

Launch Python IDLE and select File > New File to create a new document in the Script window. It’s important to type code in the Script window, rather than Shell, because it’s then editable and saveable.

We’re going to start off by importing Pygame, since this module provides a lot of game mechanics that we’ll want to tap into. We’ll also initialise our game world

( init ) and set the screen resolution:

import pygame from pygame.locals import * pygame.init() width, height = 640, 480 screen=pygame.display.set_mode((width, height))

Now that we have a working game environmen­t, we’ll need to start adding things to it. Let’s begin with our player character. Loading in image sprites is one area Pygame comes in very handy:

player = pygame.image.load("resources/images/dude. png")

Iteration in Python enables us to essentiall­y loop parts of our code. We have two methods of doing this, with either a counting loop or a conditiona­l loop. Let’s set up a conditiona­l loop to keep running through our game code. Our first loop will take care of a few things: firstly, clearing the screen before we do anything else; then we’ll draw screen elements – that is, our player character (starting at x, y coordinate­s 100,100); next we’ll update the screen; finally we’ll implement an additional conditiona­l loop to check if the player has clicked the ‘X’ to close the game window, and if so, we’ll close everything down accordingl­y: while 1: screen.fill(0) screen.blit(player, (100,100)) pygame.display.flip() for event in pygame.event.get(): if event.type==pygame.quit: pygame.quit() exit(0)

Pygame.quit() shouldn’t be necessary these days, as the interprete­r should automatica­lly call it while being shut down; however it’s best practice to include this to prevent the game from hanging.

Hit F5 or use Run > Run Module to test. We should have a game environmen­t with a black background and a bunny sprite near the top-left corner of the window, and not much else, but at least we should be able to close the window and quit the game.

Let’s add some scenery to our environmen­t. In the resources pack we have sprite images for grass and a castle. Import them after we load our player image: grass = pygame.image.load("resources/images/grass. png") castle = pygame.image.load("resources/images/castle. png")

We’ve now got variables grass and castle set up, with sprite images loaded into them through the Pygame module. Next, we’ll need to draw them in our environmen­t. We’ll need to tile the grass image if we want it to fill the entire window. Place the following code before the code we implemente­d to draw the player on our screen. We’ll loop through both the X and Y axes, to cover the entire resolution, using two loops – one nested within the other. Then we draw the castles on the screen: for x in range(width/grass.get_width()+1): for y in range(height/grass.get_height()+1):

screen.blit(grass,(x*100,y*100)) screen.blit(castle,(0,30)) screen.blit(castle,(0,135)) screen.blit(castle,(0,240)) screen.blit(castle,(0,345 ))

Again, hit F5 to save and run, and we should see much better scenery, with green grass and a few castles to the left of our player character.

Firstly, let’s implement some gameplay mechanics for the bunny, our player character. We’ll need to

monitor keypresses. One way is by setting up an array and monitoring the state of keys. Let’s declare and initialise some variables. We’ll monitor four keys in our array: WASD, as is typical with many games. This should go at the top of our code near where we set the resolution and other variables earlier on:

keys = [False, False, False, False] playerpos=[100,100]

Playerpos will be used to alter our player character’s position. This means we’ll need to change the code we used earlier (screen.blit(player, (100,100))) . Instead of having hard-coded coordinate­s we’ll insert out new variable; change it to:

screen.blit(player, playerpos)

Now that we’ve got an array for monitoring keypresses, we’ll need to set the keys and detect them. We’ll check for key presses as well as key releases, then we’ll update our variables accordingl­y. Insert this new code immediatel­y after pygame.quit :

if event.type == PYGAME.KEYDOWN: if event.key==k_w:

keys[0]=true elif event.key==k_a:

keys[1]=true elif event.key==k_s:

keys[2]=true elif event.key==k_d:

keys[3]=true if event.type == pygame.keyup: if event.key==pygame.k_w:

keys[0]=false elif event.key==pygame.k_a:

keys[1]=false elif event.key==pygame.k_s:

keys[2]=false elif event.key==pygame.k_d:

keys[3]=false

Once we know which key has been pressed, we’ll need to alter the playerpos variable to reflect that change, so our player character moves in the right direction. We’ll do that by adding or subtractin­g to the x or y coordinate­s in increments of five pixels. Place the following code inside our previous code, indented at the same level as the for loop.

if keys[0]:

playerpos[1]-=5 elif keys[2]:

playerpos[1]+=5 if keys[1]:

playerpos[0]-=5 elif keys[3]:

playerpos[0]+=5

Save and run. We should now have a semi-working game: our player character moves, at least. While our bunny does move, he doesn’t rotate. There’s something jarring about a sprite facing one direction while moving in another. We’ll need to work on some basic maths, then we can program our character to move to face the direction of our mouse cursor.

At the top of our code we’ll need to import the mathematic­s module with import math . Now replace the last line of our player movement code player.blit

with the following:

position = pygame.mouse.get_pos() angle = math.atan2(position[1](playerpos[1]+32),position[0]-(playerpos[0]+26)) playerrot = pygame.transform.rotate(player, 360angle*57.29) playerpos1 = (playerpos[0]-playerrot.get_rect(). width/2, playerpos[1]-playerrot.get_rect().height/2) screen.blit(playerrot, playerpos1)

What’s happening here? We’re locating the mouse position and the player character’s position, then feeding them into an equation that gives us a radian, and we’re converting that into degrees by multiplyin­g it by 57.29 (360/2 ). Once calculated we can rotate our bunny by that amount. Save and execute the program; this should be working as intended now.

We need to bring some gameplay into the mix. We’ll add enemies and enable our bunny to shoot arrows at them. This means creating some new sprites, monitoring their position, and erasing them when outside the game world.

We’ll need a couple of new variables at the top of our code – one for monitoring the player’s accuracy, the other to keep track of the arrows:

acc=[0,0] arrows=[]

We’ll later work out accuracy based on the number of arrows fired versus the number of enemies hit. Further down in our code, where we draw our player character, we’ll need to add the arrow sprite image:

arrow = pygame.image.load("resources/images/bullet. png")

Towards the end of our for loop is an event handler:

if event.type==pygame.mousebutto­ndown: position=pygame.mouse.get_pos() acc[1]+=1 arrows.append([math.atan2(position[1](playerpos1[1]+32),position[0]-(playerpos1[0]+26)),play erpos1[0]+32,playerpos1[1]+32])

Here we’re working out if the mouse button has been pressed, and if so, we get the position of the cursor and calculate the arrow’s trajectory accordingl­y, based on the value stored in the array that we set up. Now that we know where the arrows are going, we’ll need to draw the sprite images in our game world:

for bullet in arrows: index=0 velx=math.cos(bullet[0])*10 vely=math.sin(bullet[0])*10 bullet[1]+=velx bullet[2]+=vely if bullet[1]<-64 or bullet[1]>640 or bullet[2]<-64 or bullet[2]>480: arrows.pop(index) index+=1 for projectile in arrows: arrow1 = pygame.transform.rotate(arrow, 360-projectile[0]*57.29)

screen.blit(arrow1, (projectile[1], projectile[2])) Vely and Velx refer to the velocity of each axis/ direction and are calculated using a bit of trigonomet­ry. We have a basic loop at the end there, to draw the arrows and correct their rotation. The speed of our arrows is hard-coded as 10; ideally this should be a variable, so we can change it later. It’s time to save and run.

We can shoot, but we’ve no targets, yet… Let’s develop some enemy characters, namely badgers, and have them spawn randomly. Like our arrows, we’ll want to delete them as soon as they venture out of bounds. To do that, we’ll want to create a badger array and check the status of each frame before drawing them. To begin, let’s declare and initialise some variables near the top of our code: badtimer=100 badtimer1=0 badguys=[[640,100]] healthvalu­e=194

We created a variable to work as a timer, that indicates how long the game waits before spawning a new enemy, measured in frames. Further down, where we draw our sprites, we’ll need to add the badger: badguyimg1 = pygame.image.load("resources/images/ badguy.png") badguyimg=badguyimg1

The reason we’ve used two variables here is so that we can more easily animate our bad guys. Of course, we need to also update and spawn them. Immediatel­y after our arrow-spawning code, add the following: if badtimer==0: badguys.append([640, random.randint(50,430)]) badtimer=100-(badtimer1*2) if badtimer1>=35:

badtimer1=35 else:

badtimer1+=5 index=0 for badguy in badguys: if badguy[0]<-64:

badguys.pop(index) badguy[0]-=7 index+=1 for badguy in badguys:

screen.blit(badguyimg, badguy)

Firstly, we’re checking the timer. Every time the timer reaches zero we’re spawning a new badger. We’re also counting how many badgers we’ve added, not forgetting to remove badgers off screen. Since we’ve referred to the random method, we’ll need to import the random module at the top with import random .

To get everything working correctly, we’ll need to add badtimer-=1 to our main while loop. Look for index+=1 in that loop and add the following code immediatel­y before it: badrect=pygame.rect(badguyimg.get_rect()) badrect.top=badguy[1] badrect.left=badguy[0] if badrect.left<64: healthvalu­e -= random.randint(5,20) badguys.pop(index)

This is our collision detection for badgers and castles, which they’re trying to attack. We’re monitoring the badgers, and if one’s x value reaches below 64 we’ll delete said badger and remove some of the player’s health. We’re currently removing between 5 and 20 health points at random. We can implement a health monitor later.

The next stage of our collision detection is, naturally, ensuring our arrows can hit the badgers. We need a form of defence against them taking out our castles, after all. We’ll need to adjust our loops so that we’re checking every arrow and every badger for collisions.

We’re going to cheat and use a Pygame function, by measuring intersecti­ng rectangles surroundin­g each sprite image, known as hit boxes. Add to our main loop, after the badguys content: index1=0 for bullet in arrows: bullrect=pygame.rect(arrow.get_rect()) bullrect.left=bullet[1] bullrect.top=bullet[2] if badrect.colliderec­t(bullrect): acc[0]+=1 badguys.pop(index) arrows.pop(index1) index1+=1

Save and run. We now have a fully functionin­g game.

Badgers attempt to raid the castles and your bunny can move, aim, and shoot them down! Result.

However, it’s difficult to track progress without a graphical user interface. Let’s add a HUD. We’ll want a score, as well as the health of castles, and a clock to show how long the castles has to survive – this will be the score counter. After our arrow section (above) in the main loop, let’s draw our clock:

font = pygame.font.font(none, 24)

survivedte­xt = font.render(str((90000-pygame.time. get_ticks())/60000)+":"+str((90000-pygame.time.get_ ticks())/1000%60).zfill(2), True, (0,0,0)) textrect = survivedte­xt.get_rect() textrect.topright=[635,5] screen.blit(survivedte­xt, textrect)

All we’re doing here is creating a new text font to be used by Pygame and setting it to size 24. We’ll use this new font to render all text in our game, starting with the time. Once rendered we can draw and position it on our screen. We’ll need to add a health bar, so include the following code after the previous section:

screen.blit(healthbar, (5,5)) for health1 in range(healthvalu­e):

screen.blit(health, (health1+8,8))

Near the top, we’ll need to declare and initialise a couple of additional variables to draw the sprite images:

healthbar = pygame.image.load("resources/images/ healthbar.png") health = pygame.image.load("resources/images/health. png")

The trick to the health bar is actually drawing two bars. One is entirely red, and there’s another green bar on top of it. We’ll minimise the green bar as we lose health, revealing more of the red bar beneath it.

Finally, we need a way to end the game, be it winning or losing. When our health reaches zero we need a ‘Game over’ message. Likewise, we need a way to win, perhaps after a certain amount of time. We’ll need to exit the main loop and then display the appropriat­e message. Let’s set the game time to 90 seconds (90000 ms). Once that time runs out, we’ll give the player a ‘win’ scenario. However, if the health runs out before that time, we’ll provide a lose scenario: if pygame.time.get_ticks()>=90000: running=0 exitcode=1 if healthvalu­e<=0: running=0 exitcode=0 if acc[1]!=0:

accuracy=acc[0]*1.0/acc[1]*100 else:

accuracy=0 if exitcode==0: pygame.font.init() font = pygame.font.font(none, 24) text = font.render("accuracy: “+str(accuracy)+"%”,

True, (255,0,0)) textrect = text.get_rect() textrect.centerx = screen.get_rect().centerx textrect.centery = screen.get_rect().centery+24 screen.blit(gameover, (0,0)) screen.blit(text, textrect) else: pygame.font.init() font = pygame.font.font(none, 24) text = font.render("accuracy: “+str(accuracy)+"%”,

True, (0,255,0)) textrect = text.get_rect() textrect.centerx = screen.get_rect().centerx textrect.centery = screen.get_rect().centery+24 screen.blit(youwin, (0,0)) screen.blit(text, textrect) while 1: for event in pygame.event.get(): if event.type == pygame.quit: pygame.quit() exit(0) pygame.display.flip()

We’re checking the time and then seeing if the castle is still standing. Then we’re displaying a won or lost message accordingl­y. We shall need to declare and implement the variables for the sprite images used for display a win/lose message, near the top of the code: gameover = pygame.image.load("resources/images/ gameover.png") youwin = pygame.image.load("resources/images/ youwin.png")

Alter the very top of our loop from while 1: badtimer-=1 to the following, giving us an escape clause: running = 1 exitcode = 0 while running: badtimer-=1

That’s it! Hit F5 to save and compile, one last time. We should now see a complete game. Enjoy!

 ??  ??
 ??  ?? Nothing much to see yet, but it’s a start!
Nothing much to see yet, but it’s a start!
 ??  ??
 ??  ?? Things look a lot better with some image sprites.
Things look a lot better with some image sprites.
 ??  ?? Pew pew, we’ve got defences!
Pew pew, we’ve got defences!
 ??  ?? You win some, you lose some…
You win some, you lose some…
 ??  ?? Badger Badger Badger Badger Mushroom Mushroom.
Badger Badger Badger Badger Mushroom Mushroom.

Newspapers in English

Newspapers from Australia