Level

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.

Bestanden

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.

Level.dat
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è.

[ Naar boven | Terug naar Pengo ]

Level structure

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.

Level.h
/******************************************************************************
	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.

Bestand openen

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.

Level.cpp
/******************************************************************************
	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.

Level inlezen

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.

Level.cpp
/**
 * 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.

[ Naar boven | Terug naar Pengo ]

Verbeteringen

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.

[ Naar boven | Terug naar Pengo ]

Velden

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.

Level.cpp
/******************************************************************************
	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:

Level.cpp
// 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.

Speler maken

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.

Player.cpp
/**
 * 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:

Level.cpp
// 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.

[ Naar boven | Terug naar Pengo ]

Tekenen

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.

Graphics.cpp
/**
 * 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.

Nieuwe game loop

De game loop is inmiddels verouderd, dus het wordt hoog tijd dat we 'm aanpassen. Houd je vast, want het gaat hard. (Wheeeee!)

Level aanmaken

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.

Main.cpp
// 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.)

Zoeken en vervangen

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.

Main.cpp
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.

[ Naar boven | Terug naar Pengo ]

Oude bug

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.

Graphics.cpp
/**
 * 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.

Main.cpp
// verwijder speler van scherm
ClearPlayer(myLevel.Player);
[ Naar boven | Terug naar Pengo ]

Conclusie

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. :-)

[ Naar boven | Terug naar Pengo ]

Extra

Als je de bovenstaande code af hebt en je wilt Pengo graag uitbreiden, dan kun je de volgende features toevoegen.

[ Naar boven | Terug naar Pengo ]

Downloads

De volgende bestanden horen bij deze les.

[ Naar boven | Terug naar Pengo ]

Valid XHTML 1.0! Correct CSS! Laatst bijgewerkt: dinsdag 15 april 2014