Botsingen

Pengo © 2002-2003, Joost Ronkes Agerbeek

Op dit moment hebben we een klein probleempje met Pengo. Hij kan namelijk overal doorheen lopen. Daarom houden we ons in deze les bezig met het programmeren van de botsingen. De Engelse term hiervoor is collision detection.

Voorbereidend werk

Voordat we botsingen gaan detecteren, moeten we een beetje voorbereidend werk doen. We moeten straks namelijk beschikking hebben over allerlei gegevens van het level. Als we willen dat Pengo niet van het level afloopt, dan moeten we de grootte van het level weten. Als we willen weten of Pengo tegen een blok aan trapt, dan moeten we weten waar de blokken staan.

Al dit soort gegevens staan opgeslagen in het level, dus het level moet overal beschikbaar zijn. Daarom maken we het level globaal. Ten eerste initialiseren we de globale variabele in Level.cpp.

Level.cpp
/******************************************************************************
	Globale variabelen
******************************************************************************/

// het level
Level GlobalLevel = LoadLevel();

We kunnen de naam Level niet meer gebruiken omdat het datatype al zo heet, dus ik heb het level GlobalLevel genoemd. Nu moeten we de declaratie nog opnemen in Level.h. Dit doen we met extern.

Level.h
/******************************************************************************
	Externe globale variabelen
******************************************************************************/

extern Level GlobalLevel;

Het level is nu opgeslagen in een globale variabele. We hoeven dus geen level meer aan te maken in Main.cpp en alle verwijzingen naar myLevel moeten we vervangen door GlobalLevel. Dat is weer een mooie taak voor Zoeken en Vervangen.

Main.cpp
// teken level
DrawLevel(GlobalLevel);

// maak vijand aan
Enemy myEnemy = CreateEnemy(10, 10);

// teken vijand
DrawEnemy(myEnemy);

// start game loop
int myKey;

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

	// verwijder speler van scherm
	ClearPlayer(GlobalLevel.Player);

	// is er een toets ingedrukt?
	if (PeekVirtualKey() != 0)
	{
		// ja, verwijder toets uit buffer
		myKey = GetVirtualKey();

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

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

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

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

				// beweeg speler
				GlobalLevel.Player.IsMoving = true;
			} break;
		case PengoKick:
			{
				// laat speler schoppen
				//Kick(GlobalLevel.Player, myBlock);
			} break;
		}
	}

	// verplaats de speler
	MovePlayer(GlobalLevel.Player);

	// teken level
	DrawLevel(GlobalLevel);

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

} while (myKey != GameExit);
[ Naar boven | Terug naar Pengo ]

Speler

En dan nu het echte werk. Laten we er eerst maar voor zorgen dat de speler geen ongeoorloofde dingen kan doen als het level uitwandelen of door blokken en vijanden heen lopen.

[ Naar boven | Terug naar Pengo ]

Binnen het level blijven

Om te beginnen zorgen we ervoor dat Pengo niet van het veld af kan lopen. Op dit moment zorgen we er in MovePlayer al voor dat de speler niet van het scherm af kan. We hoeven dus alleen maar wat if-statements aan te passen. In plaats van de getallen 79 en 24 (de grootte van het scherm) maken we nu gebruik van de hoogte en de breedte van het veld. Min 1, want we beginnen weer te tellen bij 0.

Player.cpp
// 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 < (GlobalLevel.Height - 1))
		{
			// 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 < (GlobalLevel.Width - 1))
		{
			// ja, verplaats speler naar rechts
			player.X++;
		}
		else
		{
			// nee, zet speler stil
			player.IsMoving = false;
		}
	} break;
}
[ Naar boven | Terug naar Pengo ]

Tegen blokken aanlopen

De speler blijft nu keurig binnen het veld, maar hij kan nog steeds dwars door blokken heenlopen. Om dit op te lossen hebben we iets meer creativiteit nodig. We willen namelijk weten of het vakje waar de speler naartoe loopt al een blok bevat. Hiervoor schrijven we een aparte functie die in Level.cpp terecht komt.

Level.cpp
/**
 * Bepaalt of er op de opgegeven coördinaten een blok staat.
 *
 * @param	x	de x-coördinaat die gecontroleerd moet worden
 * @param	y	de y-coördinaat die gecontroleerd moet worden
 *
 * @return	true als er op de opgegeven coördinaten een blok staat, anders
 *			false
 */
bool IsBlock(int x, int y)
{
	// controleer alle blokken stuk-voor-stuk
	for (int i = 0; i < GlobalLevel.Blocks.size(); i++)
	{
		// is dit het blok dat we zoeken?
		Block myBlock = GlobalLevel.Blocks.at(i);

		if ((myBlock.X == x) && (myBlock.Y == y))
		{
			// ja, er staat dus een blok op de opgegeven coördinaten
			return true;
		}
	}

	// het blok is niet gevonden, dus er staat geen blok op de opgegeven
	// coördinaten
	return false;
}

In de functie IsBlock lopen we alle blokken langs die in het level staan en we vergelijken de coördinaten van de blokken met de opgegeven coördinaten. Neem de definitie van IsBlock op in Level.h.

Hiermee kunnen we controleren of een speler tegen een blok aanloopt. Ook deze wijziging moeten we doorvoeren in MovePlayer. We roepen IsBlock met de coördinaten waar de speler terecht komt en als er al een blok staat, dan mag de speler daar niet heen lopen.

Player.cpp
// ja, in welke richting beweegt de speler?
switch (player.Direction)
{
case Up:
	{
		// zet speler stil
		player.IsMoving = false;

		// kan speler nog omhoog?
		if (player.Y > 0)
		{
			// ja, loopt speler tegen blok aan?
			if (!IsBlock(player.X, player.Y - 1))
			{
				// nee, verplaats speler omhoog
				player.Y--;

				// zet speler weer in beweging
				player.IsMoving = true;
			}
		}
	} break;

case Down:
	{
		// zet speler stil
		player.IsMoving = false;

		// kan speler nog omlaag?
		if (player.Y < (GlobalLevel.Height - 1))
		{
			// ja, loopt speler tegen blok aan?
			if (!IsBlock(player.X, player.Y + 1))
			{
				// nee, verplaats speler omlaag
				player.Y++;

				// zet speler weer in beweging
				player.IsMoving = true;
			}
		}
	} break;

case Left:
	{
		// zet speler stil
		player.IsMoving = false;

		// kan speler nog naar links?
		if (player.X > 0)
		{
			// ja, loopt speler tegen blok aan?
			if (!IsBlock(player.X - 1, player.Y))
			{
				// nee, verplaats speler naar links
				player.X--;

				// zet speler weer in beweging
				player.IsMoving = true;
			}
		}
	} break;

case Right:
	{
		// zet speler stil
		player.IsMoving = false;

		// kan speler nog naar rechts?
		if (player.X < (GlobalLevel.Width - 1))
		{
			// ja, loopt speler tegen blok aan?
			if (!IsBlock(player.X + 1, player.Y))
			{
				// nee, verplaats speler naar rechts
				player.X++;

				// zet speler weer in beweging
				player.IsMoving = true;
			}
		}
	} break;
}

Om de structuur van de code duidelijk te houden, heb ik een kleine wijziging doorgevoerd. Ik zet de speler nu eerst even stel. Alleen als de speler kan verplaatsen, zet ik 'm weer in beweging. Overigens, als je niet wilt dat de speler door blijft lopen, kun je uit bovenstaande functie de regels player.IsMoving = true; schrappen.

[ Naar boven | Terug naar Pengo ]

Speler vs. vijanden

Het is tijd om te zorgen dat je dit spel kunt verliezen. Je kunt nog niet winnen, maar goed. Een spel waarin je alleen maar kunt verliezen is spannender dan een spel waarin je alleen maar doelloos kunt rondlopen.

Je verliest als je tegen een vijand aanloopt. We willen dus weten waar de vijanden staan. Hiervoor schrijven we een functie IsEnemy die wel erg veel lijkt op de functie IsBlock. Vergeet niet de definitie van de functie op te nemen in Level.h.

Level.cpp
/**
 * Bepaalt of er op de opgegeven coördinaten een vijand staat.
 *
 * @param	x	de x-coördinaat die gecontroleerd moet worden
 * @param	y	de y-coördinaat die gecontroleerd moet worden
 *
 * @return	true als er op de opgegeven coördinaten een vijand staat, anders
 *			false
 */
bool IsEnemy(int x, int y)
{
	// controleer alle vijanden stuk-voor-stuk
	for (int i = 0; i < GlobalLevel.Enemies.size(); i++)
	{
		// is dit het vijand dat we zoeken?
		Enemy myEnemy = GlobalLevel.Enemies.at(i);

		if ((myEnemy.X == x) && (myEnemy.Y == y))
		{
			// ja, er staat dus een vijand op de opgegeven coördinaten
			return true;
		}
	}

	// het vijand is niet gevonden, dus er staat geen vijand op de opgegeven
	// coördinaten
	return false;
}

Zodra de speler tegen een vijand aanloopt, is de speler dood. Dat moeten we kunnen opslaan, dus we breiden de struct Player uit met een variabele IsDead die we bij het aanmaken van een speler op false zetten.

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;

	/**
	 * Geeft aan of de speler dood is.
	 */
	bool IsDead;
};
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;
	myPlayer.IsDead = false;

	// geef speler terug
	return myPlayer;
}

Oke, laten we nu maar eens controleren of de speler tegen een vijand is aangelopen. Dit doen we wederom in MovePlayer. De onderstaande code komt nadat de speler al verplaatst is, dus na de switch.

Player.cpp
// is de speler tegen een vijand aangelopen?
if (IsEnemy(player.X, player.Y))
{
	// ja, speler is dood :-(
	player.IsDead = true;
}

Als de speler dood is, moet het spel natuurlijk niet verder gaan. De game loop moet nu dus niet alleen stoppen als er op Escape wordt gedrukt, maar ook als de speler dood is. Met andere woorden, de game loop wordt uitgevoerd zolang er niet op Escape is gedrukt en zolang de speler niet dood is.

Main.cpp
} while ((myKey != GameExit) && (!GlobalLevel.Player.IsDead));
[ Naar boven | Terug naar Pengo ]

Blokken

De speler kan nu niet meer door blokken heen lopen, maar ooit was het zo dat je blokken kon wegschoppen. Het wordt tijd om die mogelijkheid nieuw leven in te blazen. Daarbij mogen de blokken natuurlijk ook niet door andere blokken heen en je moet ze kapot kunnen schoppen en je moet vijanden kunnen pletten. Sjonge, laten we maar snel verder gaan.

[ Naar boven | Terug naar Pengo ]

Schoppen

De functie Kick ligt al een tijdje te slapen, dus laten we 'm maar eens wakker schudden. Nu is het zo dat je aan Kick het blok meegeeft waar je tegenaan trapt. Omdat er meerdere blokken in het level staan, moeten we dat aanpassen. Je geeft geen blok meer mee; Kick controleert of je een blok geraakt hebt.

Om dit te kunnen doen is het niet voldoende om te weten òf Pengo tegen een blok aanschopt, we moeten ook weten welk blok dat is. Hiervoor schrijven we een functie GetBlock die het blok teruggeeft dat op de opgegeven coördinaten staat. Denk aan de definitie in het headerbestand.

Level.cpp
/**
 * Vraagt het blok op dat op de opgegeven positie staat.
 *
 * @param	x	de x-coördinaat van het op te vragen blok
 * @param	y	de y-coördinaat van het op te vragen blok
 *
 * @return	het blok dat op de opgegeven positie staat
 */
Block& GetBlock(int x, int y)
{
	// controleer alle blokken stuk-voor-stuk
	for (int i = 0; i < GlobalLevel.Blocks.size(); i++)
	{
		// is dit het blok dat we zoeken?
		Block& myBlock = GlobalLevel.Blocks.at(i);

		if ((myBlock.X == x) && (myBlock.Y == y))
		{
			// ja, geef het blok terug
			return myBlock;
		}
	}

	// hier zouden we nooit terecht mogen komen, maar om de compiler gerust
	// te stellen geven we het eerste blok terug
	return GlobalLevel.Blocks.at(0);
}

Let op de ampersand (&) achter de return value Block. Deze hebben we al eerder gezien bij parameters. De return value van GetBlock is dus een reference: we krijgen geen kopie terug van het blok, maar het blok zelf. Alle veranderingen die we daarin aanbrengen, worden dus ook doorgevoerd in het level. Dit noemen we return-by-reference en - voor degene die het zich afvraagd - als je de ampersand weghaalt, spreken we van return-by-value.

Waarschuwing: de functie GetBlock werkt alleen goed als je coördinaten meegeeft waarop daadwerkelijk een blok staat. Je moet de coördinaten dus eerst testen met IsBlock. Dit is geen nette manier om dit probleem te programmeren, maar voor de oplossing hebben we òf pointers òf exceptions nodig (of op z'n minst een assert) en die onderwerpen vallen buiten de scope van dit project.

Laten we maar eens kijken hoe de nieuwe Kick functie eruit ziet.

Player.cpp
/**
 * Laat de speler schoppen.
 *
 * @param	player	de speler die schopt
 * @param	block	het blok waar de speler tegenaan kan schoppen
 */
void Kick(const Player& player)
{
	// in welke richting schopt de speler?
	switch (player.Direction)
	{
	case Up:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X, player.Y - 1))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X, player.Y - 1);

			// zet blok in beweging
			myBlock.Direction = Up;
			myBlock.IsMoving  = true;
		} break;
		
	case Down:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X, player.Y + 1))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X, player.Y + 1);

			// zet blok in beweging
			myBlock.Direction = Down;
			myBlock.IsMoving  = true;
		} break;
		
	case Left:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X - 1, player.Y))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X - 1, player.Y);

			// zet blok in beweging
			myBlock.Direction = Left;
			myBlock.IsMoving  = true;
		} break;
		
	case Right:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X + 1, player.Y))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X + 1, player.Y);

			// zet blok in beweging
			myBlock.Direction = Right;
			myBlock.IsMoving  = true;
		} break;
	}
}

Ja, ja, laat dat maar even op je inwerken. :-P

In de game loop testen we weer of de speler Pengo wil laten schoppen...

Main.cpp
case PengoKick:
	{
		// laat speler schoppen
		Kick(GlobalLevel.Player);
	} break;

...en we verplaatsen alle blokken.

Main.cpp
// verplaats de blokken
for (int i = 0; i < GlobalLevel.Blocks.size(); i++)
{
	MoveBlock(GlobalLevel.Blocks.at(i));
}

Compileren, starten en... jawel, de oude vertrouwde bug. De blokken laten sporen achter. We hebben dus een functie ClearBlock nodig (inclusief definitie).

Graphics.cpp
/**
 * Verwijdert een blok van het scherm.
 *
 * @param	block	het blok dat verwijderd moet worden
 */
void ClearBlock(const Block& block)
{
	// verwijder blok
	WriteText(" ", block.X, block.Y);
}

Om nou te voorkomen dat alle blokken op het scherm constant flikkeren, verwijderen we een blok alleen als het in beweging is. En als we dan toch bezig zijn, kunnen we dat met de speler ook wel doen. Je kunt de aanroep van ClearPlayer aan het begin van de game loop dus weghalen.

Main.cpp
// beweegt de speler?
if (GlobalLevel.Player.IsMoving)
{
	// ja, verwijder speler van het scherm
	ClearPlayer(GlobalLevel.Player);

	// verplaats de speler
	MovePlayer(GlobalLevel.Player);
}

// verplaats de blokken
for (int i = 0; i < GlobalLevel.Blocks.size(); i++)
{
	// beweegt het blok?
	if (GlobalLevel.Blocks.at(i).IsMoving)
	{
		// ja, verwijder het van het scherm
		ClearBlock(GlobalLevel.Blocks.at(i));

		// verplaats het blok
		MoveBlock(GlobalLevel.Blocks.at(i));
	}
}

// teken level
DrawLevel(GlobalLevel);
[ Naar boven | Terug naar Pengo ]

Blokken stoppen

Ook blokken mogen niet van het level af of door andere blokken heen bewegen. We moeten in de functie MoveBlock dus dezelfde aanpassingen doen als in MovePlayer.

Block.cpp
/**
 * Verplaatst het blok één positie.
 *
 * @param	block	het blok dat verplaatst moet worden
 */
void MoveBlock(Block& block)
{
	// beweegt het blok?
	if (block.IsMoving)
	{
		// ja, in welke richting beweegt het blok?
		switch (block.Direction)
		{
		case Up:
			{
				// zet blok stil
				block.IsMoving = false;

				// kan blok nog omhoog?
				if (block.Y > 0)
				{
					// ja, loopt blok tegen blok aan?
					if (!IsBlock(block.X, block.Y - 1))
					{
						// nee, verplaats blok omhoog
						block.Y--;

						// zet blok weer in beweging
						block.IsMoving = true;
					}
				}
			} break;

		case Down:
			{
				// zet blok stil
				block.IsMoving = false;

				// kan blok nog omlaag?
				if (block.Y < (GlobalLevel.Height - 1))
				{
					// ja, loopt blok tegen blok aan?
					if (!IsBlock(block.X, block.Y + 1))
					{
						// nee, verplaats blok omlaag
						block.Y++;

						// zet blok weer in beweging
						block.IsMoving = true;
					}
				}
			} break;

		case Left:
			{
				// zet blok stil
				block.IsMoving = false;

				// kan blok nog naar links?
				if (block.X > 0)
				{
					// ja, loopt blok tegen blok aan?
					if (!IsBlock(block.X - 1, block.Y))
					{
						// nee, verplaats blok naar links
						block.X--;

						// zet blok weer in beweging
						block.IsMoving = true;
					}
				}
			} break;

		case Right:
			{
				// zet blok stil
				block.IsMoving = false;

				// kan blok nog naar links?
				if (block.X < (GlobalLevel.Width - 1))
				{
					// ja, loopt blok tegen blok aan?
					if (!IsBlock(block.X + 1, block.Y))
					{
						// nee, verplaats blok naar rechts
						block.X++;

						// zet blok weer in beweging
						block.IsMoving = true;
					}
				}
			} break;
		}
	}
}
[ Naar boven | Terug naar Pengo ]

Blokken vernietigen

Als je tegen een blok schopt dat tegen een ander blok aan staat, dan vernietig je het blok. Dat betekent dus dat we in de functie Kick niet alleen moeten controleren of Pengo tegen een blok aantrapt, maar ook of er een blok direct naast staat. Is dat het geval, dan moet het blok waar Pengo tegenaan schopt van het level verwijderd worden.

Het verwijderen van een blok van het level doen we met een aparte functie, die in Level.cpp terecht komt. Vergeet niet de declaratie op te nemen in Level.h.

Level.cpp
/**
 * Verwijdert een blok van het level.
 *
 * @param	block	het blok dat verwijderd moet worden
 */
void EraseBlock(Block& block)
{
	// controleer alle blokken stuk-voor-stuk
	for (int i = 0; i < GlobalLevel.Blocks.size(); i++)
	{
		// is dit het blok dat we zoeken?
		Block myBlock = GlobalLevel.Blocks.at(i);

		if ((myBlock.X == block.X) && (myBlock.Y == block.Y))
		{
			// ja, verwijder blok van scherm
			ClearBlock(myBlock);
			
			// verwijder blok uit lijst
			GlobalLevel.Blocks.erase(GlobalLevel.Blocks.begin() + i);
		}
	}
}

Voordat je een blok van het level verwijderd, moet je het ook van het scherm afhalen. Vandaar de aanroep van ClearBlock. Het weghalen van een blok uit de vector gaat op een wat rare manier. De memberfunctie erase neemt deze taak op zich, maar de parameter is misschien niet helemaal duidelijk. Je moet een zogenaamde iterator meegeven. Zonder nou uit te leggen wat het is, kun je zeggen: begin() wijst naar het eerste blok in de lijst en met + i wijs je naar het i-de blok in de lijst. Ietwat vreemd, maar het moet nou eenmaal zo.

We moeten nu aan de functie Kick de code toevoegen die controleert of er een blok staat naast het blok waar Pengo tegenaan schopt.

Player.cpp
/**
 * Laat de speler schoppen.
 *
 * @param	player	de speler die schopt
 */
void Kick(const Player& player)
{
	// in welke richting schopt de speler?
	switch (player.Direction)
	{
	case Up:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X, player.Y - 1))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X, player.Y - 1);

			// staat er nog een blok naast?
			if (IsBlock(player.X, player.Y - 2))
			{
				// ja, verwijder blok van level
				EraseBlock(myBlock);
			}
			else
			{
				// nee, zet blok in beweging
				myBlock.Direction = Up;
				myBlock.IsMoving  = true;
			}
		} break;

	case Down:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X, player.Y + 1))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X, player.Y + 1);

			// staat er nog een blok naast?
			if (IsBlock(player.X, player.Y + 2))
			{
				// ja, verwijder blok van level
				EraseBlock(myBlock);
			}
			else
			{
				// zet blok in beweging
				myBlock.Direction = Down;
				myBlock.IsMoving  = true;
			}
		} break;

	case Left:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X - 1, player.Y))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X - 1, player.Y);

			// staat er nog een blok naast?
			if (IsBlock(player.X - 2, player.Y))
			{
				// ja, verwijder blok van level
				EraseBlock(myBlock);
			}
			else
			{
				// zet blok in beweging
				myBlock.Direction = Left;
				myBlock.IsMoving  = true;
			}
		} break;

	case Right:
		{
			// schopt speler tegen blok aan?
			if (!IsBlock(player.X + 1, player.Y))
			{
				// nee, klaar
				return;
			}

			// vraag blok op waar speler tegenaan schopt
			Block& myBlock = GetBlock(player.X + 1, player.Y);

			// staat er nog een blok naast
			if (IsBlock(player.X + 2, player.Y))
			{
				// ja verwijder blok van level
				EraseBlock(myBlock);
			}
			else
			{
				// zet blok in beweging
				myBlock.Direction = Right;
				myBlock.IsMoving  = true;
			}
		} break;
	}
}
[ Naar boven | Terug naar Pengo ]

Vijanden pletten

Tot slot moet je met een blok je vijanden kunnen pletten. Om dit voor elkaar te krijgen hebben we een functie EraseEnemy nodig die vijanden van het level haalt. Deze functie lijkt veel op EraseBlock. Denk aan de declaratie in Level.h.

Level.cpp
/**
 * Verwijdert een vijand van het level.
 *
 * @param	x	de x-coördinaat van de vijand die verwijderd moet worden
 * @param	y	de y-coördinaat van de vijand die verwijderd moet worden
 */
void EraseEnemy(int x, int y)
{
	// controleer alle vijanden stuk-voor-stuk
	for (int i = 0; i < GlobalLevel.Enemies.size(); i++)
	{
		// is dit het blok dat we zoeken?
		Enemy myEnemy = GlobalLevel.Enemies.at(i);

		if ((myEnemy.X == x) && (myEnemy.Y == y))
		{
			// verwijder vijand uit lijst
			GlobalLevel.Enemies.erase(GlobalLevel.Enemies.begin() + i);
		}
	}
}

Omdat we geen functie hebben die ons een Enemy teruggeeft, verwacht EraseEnemy geen Enemy, maar de coördinaten van een Enemy. De wijziging die dit teweeg brengt in onze code is minimaal. (Zoek de verschillen.)

Merk op dat we de vijand niet van het scherm verwijderen, zoals we dat met het blok wel gedaan hadden. Dat hoeft ook niet, want we teken toch een blok over de vijand heen.

In MoveBlock bepalen we nu met behulp van IsEnemy of het blok misschien een vijand raakt. Als dat zo is, dan halen we de vijand weg. Voeg onderstaande code toe net na het bewegen van het blok, dus vlak onder de switch.

Block.cpp
// is het blok tegen een vijand aangekomen?
if (IsEnemy(block.X, block.Y))
{
	// ja, verwijder vijand
	EraseEnemy(block.X, block.Y);
}

We zijn er bijna. Het spel moet stoppen zodra alle vijanden dood zijn. Een kleine aanpassing van het while-statement van de game loop.

Main.cpp
} while ((myKey != GameExit) && (!GlobalLevel.Player.IsDead) &&
		(GlobalLevel.Enemies.size() > 0));

Good grief. Was dat even een lange les!

[ Naar boven | Terug naar Pengo ]

Conclusie

En dat is het voor deze keer. We hebben ervoor gezorgd dat spelers niet meer overal doorheen kunnen lopen. Ook blokken botsen nu tegen elkaar aan. We kunnen zelfs blokken kapot trappen, vijanden pletten en dood gaan.

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

De volgende keer koppelen we de snelheid van de speler en de blokken los en we nemen een klein voorschot op les 8.

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