Artikel top billede

(Foto: Computerworld)

Del1: Kod et livligt og legendarisk skydespil

Her beskriver vi, hvordan du i Linux koder et skydespil i arkadestil med Python. Spillet er sjovt, men omfattende, så vi deler processen op i to trin. Du får anden del i næste nummer.

Af Torben Okholm, Alt om Data

Denne artikel er oprindeligt bragt på Alt om Data. Computerworld overtog i november 2022 Alt om Data. Du kan læse mere om overtagelsen her.

De nådesløse kræfter fra helvede har indledt en dødbringende invasion i vores del af verdensrummet, og du er blevet rekrutteret til at afvise dette angreb. Menneskeheden er afhængig af dig! Ja, i denne artikel skal vi se på Star Fighter, et ret legendarisk videospil, der blev udviklet af Francis Michael Tayag. Star Fighter er skrevet som et klassisk arkadespil i rumskibsgenren, og det spilles af en enkelt spiller, der kæmper mod computergenererede modstandere.

Videospil-projektet består af gamingmusik, lydeffekter, animationer, skudsalver fra rumskibet og fjendtlige karakterer med bestemte angrebsmønstre – og et fuldt implementeret menusystem til at konfigurere spilfunktionerne, ikke at forglemme. En spiller kan også skrive sit navn på et scorekort til sidst, og det bliver så gemt. Mange af disse funktioner bliver beskrevet i denne artikel. Spillet er ret gennemført og med fine detaljer, så vi har delt processen op i to artikler. Anden del får du i næste nummer.

Før vi begynder, skal vi have fat i nogle få sager: Python, PyGame og kildekoden til Star Fighter. Vi beskriver ikke her, hvordan du koder i Python, eller hvordan kodesproget virker, hvis du ikke er bekendt med det. Det vil vi gøre i en senere udgave af Kodekassen. Så gem eventuelt de to artikler i denne spilserie, så du kan få glæde af dem senere.

Du kan hente den seneste version af Python på Python.org, hvor der også er en introduktion til dette populære kodesprog. Herfra kan du også hente Python til Windows, hvis du foretrækker at arbejde i det miljø.

Installation og opsætning

Du installerer Python i Ubuntu Linux ved at åbne et terminalvindue (Ctr+Alt+T) og skrive sudo apt-get python3 efterfulgt af sudo apt-get install pip3 . Installer så PyGame-modulet ved at skrive pip3 install pygame . For at sikre, at du bruger PyGame version 2.0 (fordi Star Fighter bruger en temmelig ny version af PyGame), skriver du python3 -m pip install pygame==2.0.0 . Nu bør du have version 2.0 af PyGame installeret.

Til sidst henter du et eksemplar af kildekoden til Star Fighter-projektet ved at klone GitHub-lageret. Før du skriver det følgende for at klone (kopiere) GitHub-lageret, går du ind i den mappe på dit system, som du gerne vil have projektet kopieret til.

git clone http://github.com/zyenapz/Star-Fighter

Nu bør du kunne se en mappestruktur som den i skærmskuddet (se den modstående side øverst til højre). Naviger ind i SOURCE-mappen, hvor du finder den afgørende python-fil til spillet, nemlig game.py. Du kan prøve at spille spillet ved at skrive det følgende:

python3 ./game.py

Før du fortsætter, bør du give dig tid til at lære, hvad man kan bruge i spillet såsom det menusystem, der er blevet implementeret, hvilke spolkontroller der er (navnlig piletasterne og Z-tasten til affyring) og i det hele taget, hvordan spillet fungerer ... nå ja, du skal naturligvis også nyde at blæse de afskyelige helvedesagenter hen, hvor peberet gror, før du går videre med denne guide.

Her er en terminal-liste, der viser projekt-strukturen, efter at den er klonet og downloadet.

Selvom game.py er den vigtigste kildefil til Star Fighter, er spilprojektet delt op i forskellige Python-filer, der hver dækker et bestemt aspekt af spillet. Herunder får du et overblik over hver fil, så du kan forstå, hvordan den generelle kildekode er struktureret:


  • game.py – spillets hoved-programfil.

  • defines.py – gemmer værdier for elementer såsom skærmopløsning, spilfunktioner, mapper, forudindstillede valg og så videre.

  • scenes.py – håndterer forskellige stadier af spillet – fra indlæsning af spilmenuen over Options-menuen og det komplette gameplay til spillets endelige resultat.

  • widgets.py – de objekter (normalt omtalt som widgets), der udgør de valg i menuen, som gør det muligt for spilleren at vælge værdier.

  • sprites.py – indstiller værdier og egenskaber for hvert spilobjekt i Star Fighter, herunder spillere.

  • spawner.py – styrer, hvordan spilkarakterer og objekter bliver genoplivet efter døden/forsvinden.

  • muda.py – forskellige funktioner såsom indlæsning/lagring af billeder og data, tegning af tekst og andet.


Vi begynder med at se på det, vi først ser, når vi indlæser spilprogrammet – menusystemet. Når du kører programmet, kan du se, at når du trykker pil op eller ned, bliver den valgte menufunktion markeret og eksekveret, når du trykker Enter. Der er også en Options-menu, som man kan vælge, og den er en undermenu.

Objektorienteret Programmering

Der er blevet brugt teknikker til Object Orien-tated Programming (OOP) i løbet af Star Fighter-projektet. De to vigtigste objektorienterede redskaber, der er blevet brugt, er klasser – altså oprettelse af eksempler på klasser – og arv. Man kan betragte en klasse som en skabe-lon for det objekt, som skal oprettes.

Klassen omfatter operationer, der er kendt som metoder og variabler og kaldes properties i OOP-verdenen. De kan alle hjælpe med at afgøre, hvordan objektet opfører sig, når det er oprettet. Lad os se på et eksempel, som man kan finde i kildekoden:

class PlayerBullet(pygame.sprite.Sprite): def __init__(self, image, position, velocity): super().__init__() self.image = image self.rect = self.image.get_rect() self.rect.centerx = position.x self.rect.bottom = position.y self.position = Vec2(self.rect.centerx, self.rect.bottom) self.velocity = Vec2(velocity.x, velocity.y) self.radius = PLAYER_BULLET_RADIUS …

En klasse i det ovenstående eksempel er defineret som PlayerBullet, hvilket er navnet på selve klassen. Inden for definitionen af klassen er der noget, vi kalder klassekonstruktøren. Her er et eksempel på det:

def __init__(self, image, position, velocity): super().__init__()

Klassekonstruktøren bliver især brugt til at initialisere property-værdier, når et udslag af denne klasse bliver oprettet. I den klasse, der er vist i dette eksempel, bliver konstruktøren brugt til at lagre en afbildning samt definere position og hastighedsværdier. Et eksempel på denne klasse bliver nævnt senere i kildekoden.

def _attack(self): Som man kan se af det ovenstående kodesegment, bliver et eksempel på klassen oprettet ved først at kalde klassen ved dens navn og derefter indsætte værdier for at danne et objekt af den klasse. b = PlayerBullet(self.bullet_image, …..)

Spillets Game Menu System bliver primært håndteret ved hjælp af to filer: scenes.py og widgets.py. Den bedste måde at forstå disse to Python-filer på består i at erindre, at scenes.py primært håndterer begivenheder for hver scene af spilprogrammet, og widgets.py håndterer indholdet for hver scene, for eksempel de kontroller, spilleren interagerer med for at vælge værdier for spillet.

I en IDE (Integrated Development Environment) eller i din foretrukne editor åbner du scenes.py og widgets.py og vælger først at se scenes.py. Mens du ser på scenes.py, blader du ned til eller leder efter OptionsScene. OptionsScene er en Python-klasse, der håndterer begivenheder til den optionsmenu, som man har adgang til fra hovedmenuen.

Den kode, der håndterer begivenhederne for Options-menuen, er vist her:

if self.menu_widget.get_selected_str() == “VIDEO":
self.P_Prefs.options_scene_selected = 0
self.manager.go_to(VideoOptionsScene(self.P_Prefs))
elif self.menu_widget.get_selected_str() == “SOUND":
self.P_Prefs.options_scene_selected = 1
self.manager.go_to(SoundOptionsScene(self.P_Prefs))
elif self.menu_widget.get_selected_str() == “GAME":
self.P_Prefs.options_scene_selected = 2
self.manager.go_to(GameOptionsScene(self.P_Prefs))
elif self.menu_widget.get_selected_str() == “CONTROLS":
self.P_Prefs.options_scene_selected = 3
self.manager.go_to(ControlsOptionsScene(self.P_Prefs))
elif self.menu_widget.get_selected_str() == “BACK":
self.P_Prefs.options_scene_selected = 0
self.manager.go_to(TitleScene(self.P_Prefs))

Ved at iagttage denne kildekode kan man se, at hver option i Options-menuen bliver behandlet som en separat “scene”, hvori en hel liste af andre optioner kan blive præsenteret. Ud fra kildekoden i scenes.py kan man se, at hver “scene” har sin egen klasse, der styrer begivenhederne for denne scene.

Lad os som et eksempel på det se på Game Options. Find en klasse ved navn GameOptionsScene.

class GameOptionsScene(Scene):
def __init__(self, P_Prefs):
# Initialise values
….
def handle_events(self, events):
for event in events:
if event.type == pygame.KEYDOWN:
….

Man vil i det store og hele se, at alle klasser, der er defineret i scenes.py, ligner hinanden, idet alle begivenhederne er blevet håndteret med en funktion (eller metode) ved navn handle_events. Menusystemet bliver udelukkende styret af tastatur-begivenheder, og der er derfor ingen styring af begivenheder via musen.

Når man gennemgår koden i handle_events, ser man, at en lydeffekt bliver udløst ved tastaturtryk, og at widgets (eller inputkontroller) er sat til at følge den option, der nu bliver valgt. Her kan man se et eksempel på det:

elif event.key == self.P_Prefs.key_down:
self.sfx_keypress.play()
# Play sound
self.menu_widget.select_down()

Lad os nu se, hvordan widgets (eller brugerkontroller) forholder sig til dette. Vælg at få widgets.py vist i din IDE eller editor, og blad ned til eller led efter GameOptionsSceneMenuWidget. I den konstruktør, der hører til GameOptionsSceneMenuWidget (___init___), er positionen for hver af disse widgets (kontroller) sat til at følge optionerne i menuen og til også at definere, hvilken form for kontrol der skal bruges til at ændre værdien for den valgte menuoption.

self.ts_hp = TextSelector(self.P_Prefs.hp_pref, HP_
OPTIONS, (x_alignment,self.ts_hp_y), alignment=
"CENTER”, active=True)
self.ts_canpause = TextSelector(self.P_Prefs.can_pause,
YESNO_OPTIONS, (x_alignment, self.ts_canpause_y),
alignment="CENTER")

Som man kan se af det ovenstående kodesegment, er en widget ved navn TextSelector blevet brugt, og den har en klasse, der tidligere er blevet tildelt den i widgets.py. De værdier, man skal vælge på hvert menuvalg, er defineret i HP_OPTIONS og YESNO_OPTIONS , som man kan finde i defines.py.

Efter initialisering af position og værdier (i konstruktøren) bliver der oprettet andre funktioner, som bliver brugt til at styre widgets. De omfatter update, draw (bruges til at trække menuvalg hen på skærmen) og forskellige valgmuligheder. Før du går videre, kan du med fordel gennemgå de andre klasser og danne dig en bedre forståelse af, hvordan andre dele af spillet fungerer.

Skydning med rumskibe

Nu skal vi se på, hvordan skydning fra rumskibe finder sted. Den funktion, der står for skydning fra rumskibene, hedder _shoot(self, keyspressed), og den befinder sig i sprites.py og er en del af den Player-klasse, som er defineret tidligere i filen. Den nøgle, man bruger til at affyre, er arrangeret i P_Prefs i game.py under navnet key_fire, som i øjeblikket er sat til Z-tasten. Lad os nu se på den kode, der styrer affyringen:

if keyspressed[self.P_Prefs.key_fire]:
self._play_shoot_sound()
if self.bullet_increase_timer >= self.bullet_increase_
delay * 2 and self.gun_level == 3:
self._attack3()
elif self.bullet_increase_timer >= self.bullet_
increase_delay and self.gun_level >= 2:
self._attack2()
else:
self._attack1()
self.bullet_increase_timer += self.bullet_increase_tick
else:
self.bullet_increase_timer = 0

Når “affyringstasten” bliver aktiveret, afspilles der først en lydeffekt og derefter en angrebssekvens, der er afhængig af arten af skydevåben og af selve granatens timing.

self._attack2() – to granater affyres fra rumskibet.
self._attack3() – tre granater affyres fra rumskibet.

Det granatobjekt, der bliver affyret fra rumskibet, bliver håndteret af en klasse ved navn PlayerBullet, som under dannelsen af en PlayerBullet-objektbegivenhed inddrager de følgende argumenter for at arrangere en granat: billede, position og hastighed. Man kan se, at positionen er en 2D-vektor (x,y), og hastighed er også en 2D-vektor (x,y).

Glem ikke menusystemet, for det er det første, som man ser.

Sprite-Sheets

Animationer til hver af spillets karakterer og objekter bliver implementeret ved hjælp af sprite-sheets. Det er en metode til at indlæse en karakteranimation ved blot at indlæse én fil af flere billeder og derefter indlæse hvert billede i et array, således at man kan blade igennem det eller manipulere det og outputte det på skærmen.

De sparer en for besværet med at skulle indlæse hvert billede individuelt i hukommelsen; ved hjælp af sprite-sheets bliver billederne indlæst i hukommelsen på én gang. Hvis du går til mappen SOURCE\data\img, finder du filer såsom hellfighter_sheet.png, player_sheet.png og så videre. Det er de sprite-sheets, der bliver brugt i spillet. Et kort eksempel på, hvordan spillerens sprite-sheet bliver indlæst, kan ses herunder i scenes.py. Eksemplet viser også, hvordan andre sprite-sheets bliver håndteret:

PLAYER_SPRITESHEET = load_img("player_ sheet.png”, IMG_DIR, SCALE) PLAYER_IMGS = { “SPAWNING": [ image_at(PLAYER_SPRITESHEET, scale _rect(SCALE, [0,144,16,16]), True), image_at(PLAYER_SPRITESHEET, scale _rect(SCALE, [16,144,16,16]), True), image_at(PLAYER_SPRITESHEET, scale _rect(SCALE, [32,144,16,16]), True), image_at(PLAYER_SPRITESHEET, scale _rect(SCALE, [48,144,16,16]), True) ],

Som det fremgår af denne kode, bliver dette sprite-sheet indlæst ved hjælp af en hjælpefunktuon i muda.py kaldet load_img. Man kan finde de variabler, der bliver indlæst, i kildefilen defines.py. Dette sprite-sheets billeder bliver indlæst af en hjælpefunktion ved navn image_at, og den bliver brugt til at hente billedet fra dette sprite-sheet ind i hukommelsen.

Denne kodesektion viser billeder, der er hentet fra det sprite-sheet, som bliver brugt, når spilleren “spawner” i spillet. Hvis man læser videre i kildekodefilen scenes.py, kan man se, at billeder bliver hentet fra sprite-filen for den spiller, der bevæger sig fremad, tilbage, til venstre eller til højre.

Fjenderne skyder igen

Vi bliver hos Python-scriptfilen, sprites.py: Den fjendtlige beskydning begynder først med den klasse, der hedder EnemyBullet. Denne klasse definerer grundadfærden og egenskaberne for de fjendtlige granater, der bliver brugt i spillet. I stil med PlayerBullet inddrager klassekonstruktøren et billede: positionsværdien er igen en 2D-vektor, og hastigheden er 2D-vektor. Klassen EnemyBullet inddrager også en værdi for den skade, som granaten vil forvolde, hvis den rammer spillerens rumskib.

Der er to formater for granater, som fjenden har til rådighed: EnemyBullet og FattyBullet. Hver af dem er defineret og implementeret med deres egen klasse. Selvom de ligner hinanden, når det gælder struktur og behandling, bliver de brugt på lidt forskellige måder, til trods for at klassen EnemyBullet bliver brugt som grundadfærd for begge fjendtlige karakterer i spillet. Klassen FattyBullet bygger oven på EnemyBullet.

Vi fokuserer på at lave angreb fra dit rumskib, således at du kan skyde på fjendtlige spillere.

Man kan bedst forstå det ved at tænke på, at EnemyBullet repræsenterer standardadfærden og egenskaberne hos den fjendtlige granat. Klassen FattyBullet tilføjer en ekstra funktionalitet, som EnemyBullet ikke har.

Man kan finde værdierne for de variabler, der bliver brugt i klasser som FATTY_BULLET_DIRECTION , SMALL_BULLET_RADIUS og BULLET_DAMAGE , i kildekodefilen defines.py. Når man vil prøve at se dem, er det bedst at bruge IDE’s søgefunktion. Man kan se et eksempel nedenfor i defines.py.

FATTY_LARGE_BULLET_SPEED = {
“EASY": 200,
“MEDIUM": 300,
“HARD": 400
}
FATTY_SMALL_BULLET_SPEED = {
“EASY": 125,
“MEDIUM": 150,
“HARD": 175
}
FATTY_BULLET_DAMAGE = {
“EASY": 0.75,
“MEDIUM": 1,
“HARD": 1.25
}

Det er kun rimeligt at give fjenden mulighed for at forsvare sig.

Værdierne bliver defineret i forhold til det niveau, spilleren vælger at spille på. Man kan sagtens lære, hvordan hver af disse variabler med associerede værdier påvirker spillet, og selv ændre værdierne for derved at forbedre måden, hvorpå spillet bliver spillet og spillets sværhedsgrad. Ud fra din forståelse for det, der er blevet dækket i denne artikel, bør du nu være i stand til at løse enhver af disse opgaver:


  • Oprette menufunktioner.

  • Ændre forskellige spilindstillinger med det formål at

  • forandre spillets ydelse og adfærdsmønster.

  • Tilføje nye fjendekarakterer.

  • Ændre eller forbedre eksisterende animationer.


Eftersom der er megen kildekode at gå igennem, har du muligvis brug for at gå tilbage og studere visse områder af kildekoden for derved at forstå dem bedre, inden du begynder at modificere projektet. Og i næste nummer skal vi lege med multiplayer-netværksmekanik!