Linux Format

Code a first-person shooter

Calvin Robinson looks to the iconic PC games Doom and Wolfenstei­n 3D for inspiratio­n, in the latest instalment in his Python coding series.

-

Calvin Robinson looks to the iconic PC games Doom and Wolfenstei­n 3D for inspiratio­n, in the latest instalment in his game coding series.

This issue we’re going to have a go at designing a first-person shooter. Wolfenstei­n 3D (1992) and Doom (1993) are arguably the most notable early successes of the three-dimensiona­l firstperso­n shooter (FPS) genre. Both were released by id Software and designed by John Romero and John Carmack. These games featured the player seeing the game world through the eyes of a protagonis­t, roaming around levels looking for bad guys to eliminate. The graphics were, of course, pseudo-3d because true 3D graphics hardware hadn’t been invented yet. A lot of isomorphic project and trickery with sprites was used to create an illusion of three dimensions. We’re going to replicate that method.

To get started, we’ll need a few things: Python,

Pygame, and pvp-raycast engine. To install Python, open a Terminal and type sudo apt-get install python3 , followed by sudo apt-get install pip3 . Then you can install Pygame with pip3 install pygame . Finally, grab a copy of pvp-raycast by Raul Vieira from github with git clone https://github.com/raulzitoe/pvp-raycast .

Pygame provides a lot of useful modules for drawing shapes and such, and Raul’s raycast engine includes all the fancy techniques for creating a multiplaye­r FPS game world, including textured raycasting with threaded sockets to handle data packets to/from multiple clients.

PVP Raycast includes textured raycasting, which will enable projectile­s and player sprite casting; scoreboard­s to monitor kills and deaths; wall shadowing for perceived depth; respawning; mini maps and a few additional handy features. It’s a great toolset to get us up and running promptly.

Import Python modules

Open up Python IDLE, hit File>new to create a new Script and begin by setting up the modules we’re going to import: import pygame from game import Game import constants as c import socket import time import pickle from sprite import Sprite import threading

Instead of setting up our constants at the top of our code, as we normally would, we’ve set up a module to keep them separate. In an additional file called

constants.py we’ve got our colour variables and other globals, such as screen resolution and the game map.

Next we set up our constant variables:

BLACK = (0, 0, 0)

WHITE = (255, 255, 255)

BLUE = (0, 0, 255)

GREEN = (0, 255, 0)

RED = (255, 0, 0)

GRAY = (127, 127, 127) SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600

TEX_WIDTH = 64

TEX_HEIGHT = 64

MAP_WIDTH = 24

MAP_HEIGHT = 24

Here we’re clearly defining the colours we intend to use in our game, but feel free to custom these as you see fit. The RGB colour scale we’re using incorporat­es red, green and blue pixels much like a paint palette, but in reverse with 0 being black and 255 being full colour. Imagine we’re using a prism and deciding how much of each light to let through. 255,255,255 is white because it allows the full amount of red, green and blue, whereas 0,0,0 is black because it allows no light through whatsoever. 255,0,0 would be red, and using numbers in between would alter the shade until it becomes a different colour entirely.

Get mapping

After setting up the colours, we declared our screen resolution at 800x600, which is a retro pre-high definition screen res. We also decided the size of our map. We now need to define our game map in constants.py. Map is another word for level, and not to be confused with the game’s mini-map.

game_map = ( (8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8), (8,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,8), (8,0,0,0,0,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,8), (8,0,0,0,6,6,6,6,6,6,2,0,0,2,6,6,6,6,6,6,0,0,0,8), (8,0,0,0,6,6,6,6,6,6,0,0,0,0,6,6,6,6,6,6,0,0,0,8), (8,0,0,0,6,6,6,6,6,6,0,0,0,0,6,6,6,6,6,6,0,0,0,8), (8,0,0,0,6,6,6,6,6,6,0,0,0,0,6,6,6,6,6,6,0,0,0,8), (8,0,7,7,6,6,6,6,6,6,0,0,0,0,6,6,6,6,6,6,7,7,0,8), (8,0,0,0,6,6,6,6,6,6,0,0,0,0,6,6,6,6,6,6,0,0,0,8), (8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8), (8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8), (8,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,8), //you get the idea (8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8), Think of this as a birds-eye view, where numbers represent walls with different sprites, and zeroes are paths. We’ve only created one map to begin with, but we could use this array method to produce several different levels for our game. Try implementi­ng game_map2 etc. to introduce more levels. Of course, the challenge of each level could increase as the game progresses if you were to focus on a single player game, but since this is mostly a multiplaye­r game we might want to instead add different perspectiv­es to each level. For example, different vantage points that the players might want to fight over for a slight or even perceived advantage.

The mini-map itself is set up in minimap.py: import pygame import constants as c

class Minimap: def __init__(self, square_size): self.square_size = square_size self.surface = pygame.surface((self.square_size*c. MAP_WIDTH, self.square_size*c.map_height)). convert()

self.x = C.SCREEN_WIDTH - self.square_size*c.

MAP_WIDTH self.y = 0

def draw(self, destinatio­n, world_map, player_x, player_y, dict_sprites):

self.surface.fill(c.gray) for i, row in enumerate(world_map):

for j, elem in enumerate(row):

if elem: pygame.draw.rect(self.surface, C.BLACK, (j*self.square_size, i*self.square_size, self.square_size, self.square_size))

# Draw Player dot_x = (player_x / C.MAP_WIDTH) * self.square_

SIZE*C.MAP_WIDTH dot_y = (player_y / C.MAP_HEIGHT) * self.square_

SIZE*C.MAP_HEIGHT

pygame.draw.circle(self.surface, C.BLUE, (int(dot_y), int(dot_x)), self.square_size//2)

# Draw Sprites for player_id, sprites_list in dict_sprites.items(): for sprite in sprites_list: if not sprite.is_player:

dot_x = (sprite.x / C.MAP_WIDTH) * self. square_size*c.map_width

dot_y = (sprite.y / C.MAP_HEIGHT) * self. square_size*c.map_height

pygame.draw.circle(self.surface, C.RED, (int(dot_y), int(dot_x)), self.square_size//2)

destinatio­n.blit(self.surface, (self.x, self.y))

We’re simply drawing the map on to a surface, as we would any other sprite. We’ve done a similar thing with the game engine itself, called game.py. This features the input handling, sprite casting and drawing. We have a file called ip_port_to_connect and server.py, which we’ll use later for multiplaye­r settings. Minimap.py controls the drawing of our mini map, as the name would imply. Scoreboard.py sets up the fonts ready to draw text on the screen, displaying our player’s score. Sprite.py is a very simply class holding sprite informatio­n for the player and projectile objects. Spriteshee­t.py is the class using Pygame to display images on our screen from a sprite sheet, stored in the assets folder. The assets directory contains all the images we’ll display, including walls, objects, projectile­s, players and the floor. Changing these will alter the look and feel of our game.

Back in our client.py and first let’s get Pygame working, set up the game clock speed, screen resolution and run the game module: pygame.init() screen = pygame.display.set_mode((c.screen_

WIDTH, C.SCREEN_HEIGHT)) clock = pygame.time.clock() game = Game()

We’ll also want to hide our mouse cursor and set

Pygame to ‘grab’ the mouse input and recognise it as the player’s movement:

pygame.mouse.set_visible(false) pygame.event.set_grab(true)

Next, we’ll want to read our config file to work out whether we’re connecting to either a remote game on a server (the ip address file that we referred to earlier) or a local game.

f = open(“ip_port_to_connect.txt”, “r”) CONDITION, PLAYER_NAME, IP, PORT = f.read(). splitlines() print(“1: {} 2: {} 3: {} 4: {}”.FORMAT(CONDITION, PLAYER_

NAME, IP, PORT)) if CONDITION == “YES” or CONDITION == “Yes” or

CONDITION == “yes”:

game.is_connected = True print(“connected? {}”.format(game.is_connected)) PORT = INT(PORT) f.close()

We used standard file opening and line reading functions. This is English A-level Python content, for those interested. See our feature on page 42, for insights into coding at school education level.

Multiplaye­r actions

If we’re planning to run a multiplaye­r game then we’ll need to open a socket for the connection, send some details (name and ID) and thread to establish a realtime connection: if game.is_connected:

client = socket.socket(socket.af_inet, socket.sock_

STREAM) addr = (IP, PORT) client.connect(addr) client.send(str.encode(player_name)) val = client.recv(8) print(“received id: {}”.format(val.decode())) game.my_id = int(val)

 ??  ??
 ??  ?? Our game is up and running, we can move around and shoot bullets.
Our game is up and running, we can move around and shoot bullets.
 ??  ?? Our client and server will confirm the handshake and connection.
Our client and server will confirm the handshake and connection.
 ??  ?? Waiting for the opposition… otherwise known as moving targets!
Waiting for the opposition… otherwise known as moving targets!
 ??  ?? Calvin Robinson
is a former assistant principal and computer science teacher with and a degree in computer games design and programmin­g
Calvin Robinson is a former assistant principal and computer science teacher with and a degree in computer games design and programmin­g
 ??  ?? The Kill Counter in full effect along with a top-right mini map.
The Kill Counter in full effect along with a top-right mini map.
 ??  ?? A second player has joined our game. Prepare to fight!
A second player has joined our game. Prepare to fight!
 ??  ?? Our server monitors connection­s to the game
Our server monitors connection­s to the game

Newspapers in English

Newspapers from Australia