By Jelle Pelgrims
Inleiding
Tijdens deze workshop gaan we een klein videospel maken. Dit doen we in Python, stap voor stap.
Voordat je begint is het belangrijk dat alle nodige software op je computer geïnstalleerd is:
-
Python 3: Dit is de programmeertaal die we gaan gebruiken. Het is zéér belangrijk dat je Python 3 installeert, aangezien Python 2.7 een andere syntax heeft en dus niet zal werken met de broncode van deze workshop.
-
PyCharm (of een IDE/editor naar voorkeur): We raden je aan om een IDE of editor te gebruiken waar je vertrouwd mee bent. Zo kan je op je gemak meevolgen met de workshop. Indien je geen tijd had om een IDE of editor te installeren dan kan je gebruik maken van IDLE, een editor die bij elke python-installatie beschikbaar is.
Een deel van het videospel (framework-code en gebruikte media) geven we al mee. Deze bestanden kan je hier downloaden. We bouwen dan tijdens de workshop verder aan de gegeven code tot we een speelbaar gegeven hebben.
Het spel dat we maken zal relatief simpel zijn. Als eerste maken we een spelwereld aan. Die spelwereld vullen we dan met spelmonsters. Als laatste zorgen we ervoor dat de monsters en het spelerskarakter met elkaar kunnen vechten.
Inhoudstafel
1. Spelontwikkeling 101
Videospellen zijn vaak te ingewikkeld om helemaal zelf te maken. Om een competitief videospel te programmeren heeft men kennis nodig van:
- Graphics (voornamelijk 3D)
- Invoermethodes (muis, toetsenbord, controllers, joysticks, ...)
- Netwerken (om verschillende spelclienten met elkaar te verbinden)
- Algoritmen (collisiedetectie, pathfinding, spelwereldgeneratie, ...)
- ....
Als men al deze delen zelf wel implementeren dan kan de ontwikkelingstijd van het spel enorm oplopen. Daarom ook dat veel spelontwikkelaars gebruik maken van videospel-specifieke libraries of zogenaamde game engines.
Wij maken gebruik van de library Pyglet. Je kan Pyglet installeren door een terminal te openen en het systeem-specifieke commando uit te voeren:
# Voor windows-systemen:
pip install pyglet
# Voor Linux-systemen:
sudo apt install python3-pip # Niet nodig indien pip all op je systeem staat
pip3 install pyglet
# Voor Mac-systemen:
sudo python -m ensurepip
pip install pyglet
2. De spelstructuur
De structuur van een videospel kunnen we opdelen in drie belangrijke delen:
- De spellogica
- De graphics
- De inputverwerking
Al deze onderdelen komen samen in de game loop. De game loop is een lus die zal blijven draaien zolang dat het spel bezig is. In de lus wordt opeenvolgend 1) input van de speler verwerkt, 2) de spellogica doorlopen en 3) het spelvenster vernieuwd.
3. De graphics
De graphics voor het spel dat we in deze workshop maken zijn heel eenvoudig. We bezien de spelwereld van boven af, zoals bijvoorbeeld in de eerste pokémon-spellen. We moeten dus geen ingewikkelde formules gebruiken om 3D-objecten op het scherm te laten verschijnen.
Om de spelwereld op het spelvenster te laten verschijnen werken we met een camera-systeem. De camera is gericht op een bepaalde locatie in de spelwereld, en laat enkel een gebied even groot als het spelvenster zien. Bij het omzetten van de spelwereldcoördinaten naar camera/scherm-coördinaten komt er wel nog wat wiskunde kijken, maar die slaan we voor deze workshop over.
De spelobjecten geven we vorm aan de hand van sprites. Sprites zijn kleine, pixelachtige plaatjes van spelobjecten die vooral vroeger veel gebruikt werden in videospellen. We laden deze sprites eenmalig op in het spel (de sprites zijn vaak gewoon .png-bestanden) en gebruiken ze dan op het scherm. Alle sprites van een spel één voor één laden zal echter wel voor veel overbodige code en tijdsverlies zorgen. Daarom ook dat we gebruik maken van spritesheets: Afbeeldingen die meerdere sprites bevatten en die we in één keer kunnen laden.
4. De spelwereld
Als eerste maken we dus een spelwereld aan. Deze wereld is opgedeeld in een raster met verschillende tegels. Op deze manier is het gemakkelijk om de wereld op te slaan in een 2-dimensionale array. De verschillende tegels kunnen we verschillende numerieke waardes geven, die dan hun "type" bepalen.
We gaan de wereld niet handmatig aanmaken. In plaats daarvan gebruiken we het random walk-algoritme. Dit algoritme zal vanaf een startlocatie een bepaald aantal keren in een willekeurige richting bewegen. Om met dit algoritme een interessante spelwereld aan te maken vullen we eerst een wereld met muur-tegels. Dan starten we het algoritme. De tegels waarop het algoritme beweegt vervangen we met grond-tegels. Na verloop van tijd zal het algoritme dan een soort van "pad" hebben uitgegraven in de wereld.
De onderstaande code steken we in een bestand "world.py".
import random
from enum import IntEnum
class tile(IntEnum):
EMPTY, WALL, GROUND = range(3)
class Tile:
def __init__(self, tile_id, x, y):
self.tile_id = tile_id
self.x, self.y = x, y
self.blocked = False
if self.tile_id is tile.WALL:
self.blocked = True
class World:
def __init__(self, player, map_width, map_height):
self.player = player
self.map_width = map_width
self.map_height = map_height
self.tilemap = generate_random_walk_cave((self.map_width, self.map_height), (self.player.x, self.player.y), 5000)
self.entities = [player]
def is_blocked(self, x, y):
if self.tilemap[x][y].blocked:
return True
else:
return any(entity.x == x and entity.y == y for entity in self.entities)
def generate_random_walk_cave(size, start, length):
width, height = size
x, y = start
terrain = [[Tile(tile.WALL, x, y) for x in range(width)] for y in range(height)]
terrain[x][y] = Tile(tile.GROUND, x, y)
for i in range(length):
t = random.choice([1, 2, 3, 4])
if t == 1 and not (x+1) > width-2:
x += 1
elif t == 2 and not (x-1) < 1:
x -= 1
elif t == 3 and not (y+1) > height-2:
y += 1
elif t == 4 and not (y-1) < 1:
y -= 1
terrain[x][y] = Tile(tile.GROUND, x, y)
return terrain
Als je nu "game1.py" (uit het zip-bestand dat je eerder in de workshop gedownload hebt) uitvoert dan zou je iets zoals op de onderstaande afbeelding moeten zien:
5. De spelkarakters
De spelkarakters houden we simpelweg bij in een aparte lijst. Bij het opstarten van het spel plaatsen we ze op willekeurige plaatsen in de spelwereld. De spelmonsters moeten achter elkaar jagen. Hiervoor moet elk monster een update() functie hebben, waarin we dan de logica voor het jagen steken. Elke herhaling van de spellus doorlopen we deze functie dan voor elk monster. De inhoud van de functie bestaat voornamelijk uit het oproepen van een pathfinding-algoritme. Hiermee wordt het pad naar de prooi berekend dat het monster dan kan volgen.
De onderstaande code steken we in een bestand "world.py".
import random
import utilities
class Entity:
def __init__(self, entity_type, coords):
self.name = entity_type
self.direction = (0, 1)
self.hitpoints = 100
self.is_dead = False
self.damaged_counter = 0
self.x, self.y = coords
self.path_queue = []
def damage(self, damage):
self.damaged_counter += 10
if self.hitpoints - damage <= 0:
self.hitpoints = 0
self.is_dead = True
else:
self.hitpoints -= damage
def move(self, world, dx, dy):
self.direction = (dx, dy)
if not world.is_blocked(self.x+dx, self.y+dy) and not self.is_dead:
self.x += dx
self.y += dy
else:
for entity in filter(lambda e: e.x == self.x+dx and e.y == self.y+dy, world.entities):
if not entity.is_dead:
entity.damage(30)
def moveto(self, world, x, y):
if not world.tilemap[x][y].blocked:
self.path_queue = utilities.pathfind(world, (self.x, self.y), (x, y))
def update(self, world):
if len(self.path_queue) != 0:
x, y = self.path_queue.pop(0)
dx, dy = x - self.x, y - self.y
self.move(world, dx, dy)
class Player(Entity):
def __init__(self, coords):
Entity.__init__(self, "Player", coords)
class Monster(Entity):
def __init__(self, coords):
Entity.__init__(self, "Monster", coords)
self.prey = None
def update(self, world):
self.logic(world)
rnd = random.uniform(0.0, 10.0)
if len(self.path_queue) != 0 and rnd > 9:
x, y = self.path_queue.pop(0)
dx, dy = x - self.x, y - self.y
self.move(world, dx, dy)
def logic(self, world):
if not self.prey:
entities = [entity for entity in world.entities if not entity is self and not entity.is_dead]
self.prey = random.choice(entities)
if self.prey.is_dead:
self.prey = None
else:
self.moveto(world, self.prey.x, self.prey.y)
Als je nu "game3.py" (uit het zip-bestand dat je eerder in de workshop gedownload hebt) uitvoert dan zou je iets zoals op de onderstaande afbeelding moeten zien: