Nieuws — Lesstof — Pengo — Projecten — Console API — Links
Pengo © 2002-2003, Joost Ronkes Agerbeek
We hebben de blokken geprogrammeerd, het wordt tijd om ze op het veld te zetten. Deze les schrijven we code om levels uit een bestand te laten en op het scherm te tekenen.
We moeten beginnen met bepalen hoe we het level willen opslaan in het bestand. Ik stel voor: zo simpel mogelijk. Ik ga aan de slag met het volgende bestand, maar voel je vrij om het aan te passen.
40 20 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Je ziet, het bestand bestaat volledig uit getallen. De eerste twee getallen geven de breedte en de hoogte aan van het level. Uit pure luiheid heb ik het level 40 bij 20 gemaakt, maar als je je graag het schompes wil typen, moet je vooral het hele console venster vullen en het level 80 bij 25 maken.
Alle enen in het level zijn blokken en alle nullen zijn lege vakjes. Makkelijk, hè.
Het level staat nu keurig in een bestand, maar als we er mee willen werken, dan moeten we het in het geheugen zien te krijgen. Eerst maar eens een structure schrijven waarin we het level op kunnen slaan. Uiteraard komt alle levelcode in het bestand Level.cpp en daar hoort ook een Level.h bij.
/****************************************************************************** Bestand: Level.h Project: Pengo Copyright: (c) 2003 Joost Ronkes Agerbeek Auteur: Joost Ronkes Agerbeek <joost@ronkes.nl> Datum: 2 februari 2003 ******************************************************************************/ #ifndef __LEVEL_H__ #define __LEVEL_H__ #include "Block.h" #include "Player.h" #include <vector> using namespace std; /****************************************************************************** Structures ******************************************************************************/ /** * Een level. */ struct Level { // de grootte van het level int Width, Height; // de blokken in het level vector<Block> Blocks; // de speler Player Player; }; #endif
Een level heeft een hoogte en een breedte, die we straks uit het bestand kunnen lezen. Alle blokken in het level komen in een vector te staan en ook de speler plaatsen we in het level.
Nu we een structure hebben om het level in op te slaan, openen we het bestand. Het laden van het level gebeurt in de functie LoadLevel, die we in Level.cpp zetten.
Allereerst moeten we het bestand openen waar de levelgegevens in staan.
/****************************************************************************** Bestand: Level.cpp Project: Pengo Copyright: (c) 2003 Joost Ronkes Agerbeek Auteur: Joost Ronkes Agerbeek <joost@ronkes.nl> Datum: 2 februari 2003 ******************************************************************************/ #include "Level.h" #include <fstream> #include <string> using namespace std; /****************************************************************************** Constante variabelen ******************************************************************************/ // het bestand dat de levelgegevens bevat const string LevelFile = "level.dat"; /****************************************************************************** Globale functies ******************************************************************************/ /** * Leest het level uit een bestand en zet het level in het geheugen. * * @return het level dat ingelezen is */ Level LoadLevel() { // maak nieuw level Level myLevel; // open levelbestand ifstream myFile(LevelFile.c_str()); return myLevel; }
Zoals je ziet, maken we gebruik van een ifstream-object. ifstream staat voor input file stream. We voeren dus een stroom gegevens in vanuit een bestand. Om ifstream te kunnen gebruiken, moeten we het bestand fstream includen met de regel #include <fstream>.
Je opent een bestand door de bestandsnaam tussen haakjes te zetten als je een variabele van het type ifstream maakt. In bovenstaande code bevat LevelFile de bestandsnaam. Helaas kunnen we geen string tussen de haakjes zetten, maar moeten we een zogenaam C-string opgeven. Vandaar dat we de memberfunctie c_str gebruiken; die vertaalt de string naar een C-string.
Nu het levelbestand geopend is, kunnen we er gegevens uithalen. Dit gaat op eenzelfde manier als met cin. We gebruiken de >>-operator om alle getallen uit het bestand als integers in onze code te krijgen.
/** * Leest het level uit een bestand en zet het level in het geheugen. * * @return het level dat ingelezen is */ Level LoadLevel() { // maak nieuw level Level myLevel; // open levelbestand ifstream myFile(LevelFile.c_str()); // lees de breedte en de hoogte myFile >> myLevel.Width; myFile >> myLevel.Height; // lees de rijen in for (int y = 0; y < myLevel.Height; y++) { // lees de kolommen in for (int x = 0; x < myLevel.Width; x++) { // lees veld in int myField; myFile >> myField; // wat voor veld is dit? switch (myField) { case 1: { // maak nieuw blok Block myBlock = CreateBlock(x, y); // voeg blok toe aan level myLevel.Blocks.push_back(myBlock); } break; } } } // maak een speler en zet 'm in het level myLevel.Player = CreatePlayer(); // geef level terug return myLevel; }
Deze code leest één voor één de getallen uit het bestand in en controleert of het getal een blok voorstelt. Als dat zo is, dan maakt hij een blok aan en slaat dat blok op in het level.
Ook de speler wordt in het level geplaatst, dus we roepen CreatePlayer aan om een nieuwe speler aan te maken.
We kunnen de code van LoadLevel op twee punten verbeteren. Ten eerste moeten we een enum maken van de mogelijke velden en ten tweede moeten we ervoor zorgen dat we kunnen opgeven waar de speler begint.
In de code controleren we wat voor soort veld we ingelezen hebben. Er is nu nog maar één soort (namelijk: een blok), maar dat kunnen we later uitbreiden. Het is alleen niet onmiddelijk duidelijk dat 1 verbonden is met een blok. Daarom maken we een enum aan.
We hebben de enum alleen maar nodig binnen Level.cpp, dus we hoeven hem niet in Level.h te zetten.
/****************************************************************************** Enums ******************************************************************************/ /** * De soorten velden in een level. */ enum Fields { Empty = 0, BlockField = 1 };
Dit zijn de twee velden die we tot nu toe in ons bestand hebben staan. De code om een veld in te lezen wordt nu:
// lees veld in int myField; myFile >> myField; // wat voor veld is dit? switch (myField) { case BlockField: { // maak nieuw blok Block myBlock = CreateBlock(x, y); // voeg blok toe aan level myLevel.Blocks.push_back(myBlock); } break; }
Merk op dat we myField inlezen als een int, maar dat we in de switch kunnen doen alsof het van het type Fields is.
In de functie CreatePlayer hebben we destijds vastgezet op welke positie de speler start. Dat is nu niet zo handig, want we hebben het veld verkleind. Laten we er maar voor zorgen dat je de startpositie als parameter mee kunt geven.
/** * Maakt een nieuwe speler aan en initialiseert de gegevens van de speler. * * @param de x-coördinaat waarop de speler start * @param de y-coördinaat waarop de speler start * * @return een nieuwe speler */ Player CreatePlayer(int x, int y) { // maak nieuwe speler Player myPlayer; // stel standaardwaarden in myPlayer.X = x; myPlayer.Y = y; myPlayer.IsMoving = false; myPlayer.Direction = Up; // geef speler terug return myPlayer; }
Vergeet niet de functiedeclaratie aan te passen in Player.h. De code in LoadLevel wordt nu:
// maak een speler en zet 'm in het level myLevel.Player = CreatePlayer(20, 10);
Als je je code zonder problemen wilt kunnen compileren, dan moet je ook in main de functie-aanroep van CreatePlayer even van parameters voorzien.
Nu we het level in het geheugen hebben, moeten we ervoor zorgen dat we het vanuit het geheugen op het scherm krijgen. Hiervoor schrijven we een functie DrawField die als parameter het level meekrijgt dat getekend moet worden. Deze functie komt in Graphics.cpp terecht, dus je moet Level.h includen in Graphics.cpp.
Om het veld te tekenen, doorlopen we de vector met de blokken. We tekenen elk blok naar het scherm. Daarna tekenen we de speler.
/** * Tekent het level naar het scherm. * * @param level het level dat getekend moet worden */ void DrawLevel(const Level& level) { // doorloop alle blokken in het level for (int i = 0; i < level.Blocks.size(); i++) { // teken blok DrawBlock(level.Blocks.at(i)); } // teken speler DrawPlayer(level.Player); }
Dat ging best makkelijk, eigenlijk. ;-) Voeg de declaratie van DrawLevel toe aan Graphics.h zodat we de functie straks kunnen aanroepen vanuit main.
De game loop is inmiddels verouderd, dus het wordt hoog tijd dat we 'm aanpassen. Houd je vast, want het gaat hard. (Wheeeee!)
Om te beginnen moeten we een level aanmaken. Een level bevat blokken en een speler, dus de code om een speler en een blok te maken kan weg. Het gedeelte dat voor de game loop staat, wordt dus als volgt.
// laad level Level myLevel = LoadLevel(); // teken level DrawLevel(myLevel);
Dat viel nog wel mee. (Maar bij de Python rijd je ook eerst rustig naar boven voordat je met een noodgang naar beneden zoeft.)
Omdat de variabele myPlayer verdwenen is, kunnen we die ook niet meer gebruiken. De speler staat nu in myLevel.Player. We moeten dus overal in de game loop myPlayer vervangen door myLevel.Player. :-S Tijd om de optie Zoeken en Vervangen van je editor te gebruiken.
Alle code om blokken te tekenen en om de speler te tekenen moet weg. Dat gebeurt namelijk allemaal in DrawLevel. De game loop komt er nu als volgt uit te zien.
do { // vraag huidige tijd op DWORD myTime = timeGetTime(); // wacht op een toets myKey = PeekVirtualKey(); // is er een toets ingedrukt? if (myKey != 0) { // ja, verwijder toets uit buffer GetVirtualKey(); // welke toets is ingedrukt? switch (myKey) { case PengoUp: { // stel richting in myLevel.Player.Direction = Up; // beweeg speler myLevel.Player.IsMoving = true; } break; case PengoDown: { // stel richting in myLevel.Player.Direction = Down; // beweeg speler myLevel.Player.IsMoving = true; } break; case PengoLeft: { // stel richting in myLevel.Player.Direction = Left; // beweeg speler myLevel.Player.IsMoving = true; } break; case PengoRight: { // stel richting in myLevel.Player.Direction = Right; // beweeg speler myLevel.Player.IsMoving = true; } break; } } // verplaats de speler MovePlayer(myLevel.Player); // teken level DrawLevel(myLevel); // wacht tot vertraging om is while (timeGetTime() < (myTime + GameDelay)); } while (myKey != GameExit);
We moeten aan het eind de speler nog wel verplaatsen met MovePlayer, want dat gebeurt nergens anders. Daarna kunnen we alles tekenen met DrawLevel.
Merk op dat ik case PengoKick heb weggehaald. Je kunt dus op het moment niet meer tegen een blok aan schoppen. Nu we meer dan één blok hebben, is dat namelijk wat lastiger geworden. Als we het in een latere les over collision detection gaan hebben, komt ook de code voor het schoppen weer terug.
Hé, die bug heb ik al eens eerder gezien! Was dat niet in de allereerste les over Pengo? Inderdaad.
Met de code voor het tekenen van Pengo, is ook de code voor het verwijderen van Pengo verdwenen. Tijd voor een nieuwe functie: ClearPlayer. Omdat het hier om gaat om een functie die met tekenen te maken heeft, zetten we hem in Graphics.cpp. Uiteraard moet je de functiedeclaratie weer in Graphics.h zetten.
/** * Verwijdert de speler van het scherm. * * @param player de speler die verwijderd moet worden */ void ClearPlayer(const Player& player) { // verwijder speler WriteText(" ", player.X, player.Y); }
Nu moeten we deze functie nog aanroepen in de game loop. Dit moeten we doen voordat de invoer verwerkt wordt.
// verwijder speler van scherm ClearPlayer(myLevel.Player);
En dat is het voor deze keer. We hebben een level met blokken in een bestand gezet en de code geschreven om het bestand in te lezen. Daarna hebben we ervoor gezorgd dat we het level konden tekenen. Dit had een aantal wijzigingen in de game loop tot gevolg.
Tussen neus en lippen door zijn de volgende C++-onderwerpen aan bod geweest:
De volgende keer houden we ons bezig met het programmeren van de vijanden (yeah). Tot die tijd raad ik je aan om veel te experimenteren met de code die we tot nu toe hebben geschreven. Voer wat wijzigingen door en zie wat het effect is.
Programmeren is leuk. :-)
Als je de bovenstaande code af hebt en je wilt Pengo graag uitbreiden, dan kun je de volgende features toevoegen.
De volgende bestanden horen bij deze les.