Speler

Pengo © 2002-2003, Joost Ronkes Agerbeek

Vorige keer hebben we kennis gemaakt met de game loop. Om te zien hoe de game loop werkt, hebben we een bewegende Pengo geprogrammeerd. Dat was echter alleen om wat basisconcepten te laten zien. In deze les programmeren we de speler zoals het hoort.

Datastructuur

We moeten van de speler een aantal gegevens bijhouden. Zo willen we weten waar de speler zich op het veld bevindt, hoeveel levens hij nog heeft en wat zijn score is. Deze gegevens komen uiteraard in variabelen te staan, maar het zou prettig zijn als we de variabelen op een of andere manier bij elkaar konden houden. Dat kan met een structure.

Alle programmacode die te maken heeft met de speler, komt in de bestanden Player.cpp en Player.h te staan.

Player.h
/******************************************************************************
	Bestand:   Player.h
	Project:   Pengo

	Copyright: (c) 2003 Joost Ronkes Agerbeek

	Auteur:    Joost Ronkes Agerbeek <joost@ronkes.nl>
	Datum:     11 januari 2003
******************************************************************************/

#ifndef __PLAYER_H__
#define __PLAYER_H__

/******************************************************************************
	Structures
******************************************************************************/

/**
 * Een speler.
 */
struct Player
{
	/**
	 * De positie van de speler.
	 */
	int X, Y;
	
	/**
	 * De richting die de speler op loopt.
	 */
	int Direction;
};

#endif

We hebben nu een nieuw datatype aangemaakt met de naam Player. Als we nu een variabele maken van het type Player, dan heeft de variabele een positie en een richting.

Later zullen we van de speler ook de score en het aantal levens bij moeten houden en misschien nog wel meer gegevens. Voorlopig hebben we die echter nog niet nodig. Laten we eerst maar eens kijken hoe we gebruik kunnen maken van onze structure.

[ Naar boven | Terug naar Pengo ]

De speler maken

We passen de functie main hier en daar aan (we gaan gewoon verder met Main.cpp van vorige keer).

Main.cpp
/**
 * Het programma start hier.
 *
 * @return	altijd 0
 */
int main()
{
	// maak speler aan
	Player myPlayer;

	myPlayer.X = 40;
	myPlayer.Y = 12;
	myPlayer.Direction = 0;

	// teken Pengo
	MoveCursor(myPlayer.X, myPlayer.Y);
	cout << PengoCharacter;

	// laat speler Pengo besturen
	int myKey;

	do
	{
		// vraag huidige tijd op
		DWORD myTime = timeGetTime();

		// wacht tot vertraging om is
		while (timeGetTime() < (myTime + GameDelay));

		// 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
					myPlayer.Direction = 1;
				} break;
			case PengoDown:
				{
					// stel richting in
					myPlayer.Direction = 2;
				} break;
			case PengoLeft:
				{
					// stel richting in
					myPlayer.Direction = 3;
				} break;
			case PengoRight:
				{
					// stel richting in
					myPlayer.Direction = 4;
				} break;
			}
		}

		// verwijder Pengo van scherm
		MoveCursor(myPlayer.X, myPlayer.Y);
		cout << " ";

		// in welke richting beweegt Pengo?
		switch (myPlayer.Direction)
		{
		case 1:
			{
				// verplaats Pengo omhoog
				myPlayer.Y--;
			} break;
		case 2:
			{
				// verplaats Pengo omlaag
				myPlayer.Y++;
			} break;
		case 3:
			{
				// verplaats Pengo naar links
				myPlayer.X--;
			} break;
		case 4:
			{
				// verplaats Pengo naar rechts
				myPlayer.X++;
			} break;
		}

		// teken Pengo
		MoveCursor(myPlayer.X, myPlayer.Y);
		cout << PengoCharacter;

	} while (myKey != GameExit);

	// wacht op enter
	cout << endl;
	system("pause");

	return 0;
}

Omdat we gebruik maken van de structure Player, moet je Player.h includen in Main.cpp.

De variabelen myX, myY en myDirection zijn verdwenen en er is een nieuwe variabele myPlayer gekomen. myX, myY en myDirection zijn opgenomen in myPlayer. We kunnen ze aanspreken met myPlayer.X, myPlayer.Y en myPlayer.Direction. Zo haal je dus waarden uit een functie.

Het aanmaken van een nieuwe speler is eigenlijk niet iets dat thuis hoort in de functie main. Als we later meer gegevens aan onze speler toe gaan voegen, dan wordt dat al snel onoverzichtelijk. We maken dus een nieuwe functie aan die een speler maakt. Deze functie komt in Player.cpp te staan.

Player.cpp
/******************************************************************************
	Bestand:   Player.cpp
	Project:   Pengo

	Copyright: (c) 2003 Joost Ronkes Agerbeek

	Auteur:    Joost Ronkes Agerbeek <joost@ronkes.nl>
	Datum:     11 januari 2003
******************************************************************************/

#include "Player.h"

/******************************************************************************
	Globale functies
******************************************************************************/

/**
 * Maakt een nieuwe speler aan en initialiseert de gegevens van de speler.
 *
 * @return	een nieuwe speler
 */
Player CreatePlayer()
{
	// maak nieuwe speler
	Player myPlayer;

	// stel standaardwaarden in
	myPlayer.X = 40;
	myPlayer.Y = 12;
	myPlayer.Direction = 0;

	// geef speler terug
	return myPlayer;
}

Je ziet dat we het type Player kunnen gebruiken voor de return value van een functie, net als int of char of float. We moeten nu de declaratie van de functie opnemen in Player.h, zodat we de functie in Main.cpp kunnen gebruiken.

Player.h
/******************************************************************************
	Globale functies - definities
******************************************************************************/

/**
 * Maakt een nieuwe speler aan en initialiseert de gegevens van de speler.
 *
 * @return	een nieuwe speler
 */
Player CreatePlayer();

In Main.cpp vervangen we de code waarmee we zojuist een speler aanmaakte door het volgende.

Main.cpp
// maak speler aan
Player myPlayer = CreatePlayer();
[ Naar boven | Terug naar Pengo ]

De speler verplaatsen

De speler kan tijdens het spel een aantal acties uitvoeren. Deze acties zetten we om in functies. Allereerst schrijven we de functie MovePlayer. Deze functie verplaatst de speler één positie. De code hiervoor staat nu nog in main.

De functie krijgt als parameter mee welke speler verplaatst moet worden. In Pengo is er maar één speler aanwezig, maar niets houd je tegen om nog een variabele van het type Player te maken (ik heb al een prachtig idee voor multi-player Pengo ;-) ). Daarom moet je aangeven om welke speler het gaat.

Player.cpp
/**
 * Verplaatst de speler één positie.
 *
 * @param	player	de speler die verplaatst moet worden
 */
void MovePlayer(Player& player)
{
	// in welke richting beweegt de speler?
	switch (player.Direction)
	{
	case 1:
		{
			// verplaats speler omhoog
			player.Y--;
		} break;
	case 2:
		{
			// verplaats speler omlaag
			player.Y++;
		} break;
	case 3:
		{
			// verplaats speler naar links
			player.X--;
		} break;
	case 4:
		{
			// verplaats speler naar rechts
			player.X++;
		} break;
	}
}

Vergeet niet de declaratie van de functie toe te voegen aan Player.h.

De code in de functie is niet zo verrassend; de code is uit Main.cpp geknipt en in Player.cpp geplakt. Wel opvallend is de ampersand (&) in de parameterspecificatie.

Normaal gesproken maakt C++ bij het doorgeven van parameters een kopie van de variabele die doorgegeven wordt. Kijk maar eens naar de volgende code.

void Functie(int param)
{
	// verhoog parameter met 1
	param++;
}

void main()
{
	// initialiseer variabele
	int myNumber = 4;
	
	// roep functie aan
	Functie(myNumber);	// er wordt een kopie gemaakt van myNumber
	
	// schrijf variabele naar scherm
	cout << myNumber;	// hier komt 4 uit, Functie heeft myNumber niet veranderd
}

Bij het aanroepen van Functie wordt er een kopie gemaakt van myNumber. Die kopie heet binnen de functie param. Dit zou in het geval van de speler niet handig zijn, want alleen de kopie zou verplaatst worden. De speler die naar het scherm getekend wordt, blijft op zijn oude plaats staan. De ampersand zorgt ervoor dat er geen kopie gemaakt wordt en dat de functie de meegegeven variabele kan veranderen.

void Functie(int param)
{
	// verhoog parameter met 1
	param++;
}

void main()
{
	// initialiseer variabele
	int myNumber = 4;
	
	// roep functie aan
	Functie(myNumber);	// de variabele zelf wordt doorgegeven (geen kopie)
	
	// schrijf variabele naar scherm
	cout << myNumber;	// hier komt 5 uit, Functie heeft myNumber veranderd
}

Zo'n variabele die letterlijk wordt doorgegeven, noemen we een reference variabele.

In Main.cpp vervangen we het switch-blok dat we verplaatst hebben naar MovePlayer door een functieaanroep.

Main.cpp
// verplaats de speler
MovePlayer(myPlayer);
[ Naar boven | Terug naar Pengo ]

Beperkingen

Op dit moment kan het nog steeds gebeuren dat de speler van het scherm afloopt. We moeten dus een beperking inbouwen wat betreft de beweging van de speler. Met behulp van een paar if-statements controleren we bij het verplaatsen van de speler of hij wel verplaatst kan worden. Gelukkig hebben we een aparte functie voor het verplaatsen van de speler; zo blijft onze code netjes en overzichtelijk.

Player.cpp
/**
 * Verplaatst de speler één positie.
 *
 * @param	player	de speler die verplaatst moet worden
 */
void MovePlayer(Player& player)
{
	// in welke richting beweegt de speler?
	switch (player.Direction)
	{
	case 1:
		{
			// kan speler nog omhoog?
			if (player.Y > 0)
			{
				// ja, verplaats speler omhoog
				player.Y--;
			}
			else
			{
				// nee, zet speler stil
				player.Direction = 0;
			}
		} break;
	case 2:
		{
			// kan speler nog omlaag?
			if (player.Y < 24)
			{
				// ja, verplaats speler omlaag
				player.Y++;
			}
			else
			{
				// nee, zet speler stil
				player.Direction = 0;
			}
		} break;
	case 3:
		{
			// kan speler nog naar links?
			if (player.X > 0)
			{
				// ja, verplaats speler naar links
				player.X--;
			}
			else
			{
				// nee, zet speler stil
				player.Direction = 0;
			}
		} break;
	case 4:
		{
			// kan speler nog naar rechts?
			if (player.X < 79)
			{
				// ja, verplaats speler naar rechts
				player.X++;
			}
			else
			{
				// nee, zet speler stil
				player.Direction = 0;
			}
		} break;
	}
}
[ Naar boven | Terug naar Pengo ]

Richting aangeven

Tot nu toe hebben we de richting waarin de speler beweegt aangegeven met getallen. Dit werkt prima, maar het is niet erg leesbaar. Onze code zou een stuk duidelijk worden als we in plaats van 0, 1, 2 enz. woorden konden schrijven als Up en Left.

Precies dit kunnen we bereiken met enumerations. Een enumeration, of kortweg enum, maakt het mogelijk om een reeks getallen te vervangen door namen. In plaats van 1 schrijven we dan Up en in plaats van 2 Down. De compiler zorgt er wel voor dat de woorden vertaald worden naar de juiste getallen.

Een enum krijgt een naam en we kunnen die naam gebruiken als datatype. Wij maken voor de richtingen een enum Directions. De definitie komt in Player.h te staan.

Player.h
/******************************************************************************
	Enums
******************************************************************************/

/**
 * De bewegingsrichtingen van een speler.
 */
enum Directions
{
	None,	// de speler staat stil
	Up,
	Down,
	Left,
	Right
};

In de structure Player veranderen we nu het datatype van de variabele Direction van int naar Directions.

Player.h
/**
 * Een speler.
 */
struct Player
{
	/**
	 * De positie van de speler.
	 */
	int X, Y;

	/**
	 * De richting die de speler op loopt.
	 */
	Directions Direction;
};

We passen het gedeelte van de functie CreatePlayer aan dat de standaardwaarden instelt.

Player.cpp
// stel standaardwaarden in
myPlayer.X = 40;
myPlayer.Y = 12;
myPlayer.Direction = None;

Ook in de functie MovePlayer worden alle getallen vervangen door de namen van de richtingen.

Player.cpp
// in welke richting beweegt de speler?
switch (player.Direction)
{
case Up:
	{
		// kan speler nog omhoog?
		if (player.Y > 0)
		{
			// ja, verplaats speler omhoog
			player.Y--;
		}
		else
		{
			// nee, zet speler stil
			player.Direction = None;
		}
	} break;
case Down:
	{
		// kan speler nog omlaag?
		if (player.Y < 24)
		{
			// ja, verplaats speler omlaag
			player.Y++;
		}
		else
		{
			// nee, zet speler stil
			player.Direction = None;
		}
	} break;
case Left:
	{
		// kan speler nog naar links?
		if (player.X > 0)
		{
			// ja, verplaats speler naar links
			player.X--;
		}
		else
		{
			// nee, zet speler stil
			player.Direction = None;
		}
	} break;
case Right:
	{
		// kan speler nog naar rechts?
		if (player.X < 79)
		{
			// ja, verplaats speler naar rechts
			player.X++;
		}
		else
		{
			// nee, zet speler stil
			player.Direction = None;
		}
	} break;
}

Tot slot voeren we de wijzigingen ook door in Main.cpp waar de richting van de speler veranderd wordt in reactie op de ingedrukte toets.

Main.cpp
// welke toets is ingedrukt?
switch (myKey)
{
case PengoUp:
	{
		// stel richting in
		myPlayer.Direction = Up;
	} break;
case PengoDown:
	{
		// stel richting in
		myPlayer.Direction = Down;
	} break;
case PengoLeft:
	{
		// stel richting in
		myPlayer.Direction = Left;
	} break;
case PengoRight:
	{
		// stel richting in
		myPlayer.Direction = Right;
	} break;
}
[ Naar boven | Terug naar Pengo ]

Tekenen

Pengo wordt op dit moment getekend in main, maar ook die code verplaatsen we naar een aparte functie. We zullen de code namelijk wat moeten uitbreiden en als we Pengo later om willen zetten naar een Windows-applicatie is het goed als alle tekenroutines op een aparte plaats staan.

Dat laatste is ook de reden dat we de functie DrawPlayer niet in Player.cpp zetten, maar in Graphics.cpp. In het bestand Graphics.cpp zetten we alle functies die te maken hebben met het tekenen van het spel. Op die manier hoeven we bij een eventuele port naar Windows alleen naar dat bestand te kijken.

Graphics.cpp
/******************************************************************************
	Bestand:   Graphics.cpp
	Project:   Pengo

	Copyright: (c) 2003 Joost Ronkes Agerbeek

	Auteur:    Joost Ronkes Agerbeek <joost@ronkes.nl>
	Datum:     12 januari 2003
******************************************************************************/

#include "Console.h"
#include "Player.h"

/******************************************************************************
	Constante variabelen
******************************************************************************/

/**
 * De ASCII-tekens die Pengo voorstellen.
 */
const char PengoCharacterUp    = 30;
const char PengoCharacterDown  = 31;
const char PengoCharacterLeft  = 17;
const char PengoCharacterRight = 16;
const char PengoCharacterHold  = 4;

/**
 * De kleuren van Pengo.
 */
const Color PengoForegroundColor = Cyan;
const Color PengoBackgroundColor = Black;

/******************************************************************************
	Globale functies
******************************************************************************/

/**
 * Tekent de speler naar het scherm.
 *
 * @param	player	de speler die getekend moet worden
 */
void DrawPlayer(const Player& player)
{
	// welke richting kijkt de speler op?
	switch (player.Direction)
	{
	case None:
		{
			// teken speler
			WriteText(PengoCharacterHold, player.X, player.Y,
				PengoForegroundColor, PengoBackgroundColor);
		} break;
	case Up:
		{
			// teken speler
			WriteText(PengoCharacterUp, player.X, player.Y,
				PengoForegroundColor, PengoBackgroundColor);
		} break;
	case Down:
		{
			// teken speler
			WriteText(PengoCharacterDown, player.X, player.Y,
				PengoForegroundColor, PengoBackgroundColor);
		} break;
	case Left:
		{
			// teken speler
			WriteText(PengoCharacterLeft, player.X, player.Y,
				PengoForegroundColor, PengoBackgroundColor);
		} break;
	case Right:
		{
			// teken speler
			WriteText(PengoCharacterRight, player.X, player.Y,
				PengoForegroundColor, PengoBackgroundColor);
		} break;
	}
}

Dat is ineens heel wat meer code om Pengo te tekenen dat het was toen het nog in main stond. Tijdens het spel is het belangrijk dat de speler kan zien welke kant Pengo op kijkt. Daarom moeten we Pengo zo tekenen, dat de kijkrichting duidelijk te zien is. In plaats van één ASCII-teken definiëren we dus een ASCII-teken voor elke richting die de speler op kan. Met een switch-statement bepalen we welk van de tekens we op het scherm moeten zetten.

Door cout te vervangen door WriteText kunnen we nu ook kleuren gebruiken. De constanten PengoForegroundColor en PengoBackgroundColor bepalen in welke kleur Pengo op het scherm verschijnt.

Het woord const in de parameterspecificatie geeft aan dat de variabele player niet door de functie veranderd wordt. We lezen wel waarden uit player, maar we veranderen ze niet. Als we dat wel proberen te doen, dan krijgen we een foutmelding van de compiler. Het woord const is niet verplicht, maar het voorkomt dat we moeilijk te vinden fouten maken.

Uiteraard hoort bij Graphics.cpp ook Graphics.h.

Graphics.h
/******************************************************************************
	Bestand:   Graphics.h
	Project:   Pengo

	Copyright: (c) 2003 Joost Ronkes Agerbeek

	Auteur:    Joost Ronkes Agerbeek <joost@ronkes.nl>
	Datum:     12 januari 2003
******************************************************************************/

#ifndef __GRAPHICS_H__
#define __GRAPHICS_H__

#include "Player.h"

/******************************************************************************
	Globale functies - definities
******************************************************************************/

/**
 * Tekent de speler naar het scherm.
 *
 * @param	player	de speler die getekend moet worden
 */
void DrawPlayer(const Player& player);

#endif

In Main.cpp vervangen we de code waarmee we Pengo tot nu toe hebben getekend door een aanroep van DrawPlayer. Let op, dit moeten we op twee plaatsen doen: vlak voor de game loop en aan het eind van de game loop.

Main.cpp
// teken speler
DrawPlayer(myPlayer);
[ Naar boven | Terug naar Pengo ]

Stilstaan

Op het moment is het zo dat we alleen kunnen bijhouden in welke richting de speler kijkt als hij beweegt. Zodra de speler stilstaat, kunnen we geen richting meer bijhouden. Dat is een probleem als we ons bezig gaan houden met het duwen van blokken. We moeten er dus voor zorgen dat we op een andere manier bijhouden of de speler stilstaat.

Om dit te kunnen doen, voegen we een variabele toe aan de structure Player met de naam IsMoving. Deze variabele van het type bool is true als de speler beweegt en false als de speler niet beweegt.

Player.h
/**
 * Een speler.
 */
struct Player
{
	/**
	 * De positie van de speler.
	 */
	int X, Y;

	/**
	 * De richting die de speler op loopt.
	 */
	Directions Direction;
	
	/**
	 * Geeft aan of de speler beweegt.
	 */
	bool IsMoving;
};

Door deze wijziging hoeven we de richting niet meer op None te zetten als de speler stilstaat. We kunnen None zelfs helemaal verwijderen uit de enum Direction.

Player.h
/**
 * De bewegingsrichtingen van een speler.
 */
enum Directions
{
	Up,
	Down,
	Left,
	Right
};

Het stilzetten van de speler gebeurt nu dus door IsMoving op false te zetten. Dat moeten we aanpassen in MovePlayer. Bovendien moeten we testen of de speler wel verplaatst moet worden.

Player.cpp
/**
 * Verplaatst de speler ? positie.
 *
 * @param	player	de speler die verplaatst moet worden
 */
void MovePlayer(Player& player)
{
	// beweegt de speler?
	if (player.IsMoving)
	{
		// ja, in welke richting beweegt de speler?
		switch (player.Direction)
		{
		case Up:
			{
				// kan speler nog omhoog?
				if (player.Y > 0)
				{
					// ja, verplaats speler omhoog
					player.Y--;
				}
				else
				{
					// nee, zet speler stil
					player.IsMoving = false;
				}
			} break;
		case Down:
			{
				// kan speler nog omlaag?
				if (player.Y < 24)
				{
					// ja, verplaats speler omlaag
					player.Y++;
				}
				else
				{
					// nee, zet speler stil
					player.IsMoving = false;
				}
			} break;
		case Left:
			{
				// kan speler nog naar links?
				if (player.X > 0)
				{
					// ja, verplaats speler naar links
					player.X--;
				}
				else
				{
					// nee, zet speler stil
					player.IsMoving = false;
				}
			} break;
		case Right:
			{
				// kan speler nog naar rechts?
				if (player.X < 79)
				{
					// ja, verplaats speler naar rechts
					player.X++;
				}
				else
				{
					// nee, zet speler stil
					player.IsMoving = false;
				}
			} break;
		}
	}
}

Omdat het instellen van de richting niet meer genoeg om een speler aan het bewegen te krijgen, moeten we nu ook IsMoving op true zetten als de speler op een toets drukt.

Main.cpp
// welke toets is ingedrukt?
switch (myKey)
{
case PengoUp:
	{
		// stel richting in
		myPlayer.Direction = Up;

		// beweeg speler
		myPlayer.IsMoving = true;
	} break;
case PengoDown:
	{
		// stel richting in
		myPlayer.Direction = Down;

		// beweeg speler
		myPlayer.IsMoving = true;
	} break;
case PengoLeft:
	{
		// stel richting in
		myPlayer.Direction = Left;

		// beweeg speler
		myPlayer.IsMoving = true;
	} break;
case PengoRight:
	{
		// stel richting in
		myPlayer.Direction = Right;

		// beweeg speler
		myPlayer.IsMoving = true;
	} break;
}

Bij het aanmaken van een nieuwe speler met de functie CreatePlayer zetten we de speler stil. Omdat er geen richting None meer is, moeten we een beginrichting kiezen. Het doet er niet echt toe welke richting dit is.

Player.cpp
// stel standaardwaarden in
myPlayer.X = 40;
myPlayer.Y = 12;
myPlayer.IsMoving = false;
myPlayer.Direction = Up;

In DrawPlayer moet je nu nog de case None verwijderen en de constante PengoCharacterHold is ook niet meer nodig.

[ Naar boven | Terug naar Pengo ]

Conclusie

En dat is het voor deze keer. We hebben de code voor het bewegen van de speler netjes ondergebracht in een apart bestand. Bovendien hebben we de code uitgebreid.

Tussen neus en lippen door zijn de volgende C++-onderwerpen aan bod geweest:

De volgende keer houden we ons uitgebreid bezig met het programmeren van de blokken. 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.


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