Text-gebaseerd spel met OO-technieken
De prijs voor de meest sexy titel gaan we niet winnen. Maar in de aloude traditie van de klassieke tekstgebaseerde adventure-games (zie hier) maken we een eenvoudig objectgeoriënteerd framework waarmee we snel onze eigen games kunnen maken. De nadruk ligt daarbij niet op een perfecte imitatie, maar op hoe je met wat objectgeoriënteerd inzicht tot propere, herbruikbare én onderhoudbare code komt.
We maken het spel uiteraard in een consoleapplicatie. Elke klasse en de enum komen in een eigen bestand.
Main
We willen de Main-methode van Program.cs zo leeg mogelijk houden. Daarom verpakken we de meeste functionaliteit in een klasse GameManager. In Main hoeven we dan enkel nog een loop te starten die telkens 3 zaken doet:
- De huidige locatie beschrijven
- De gebruiker tonen welke acties hij kan uitvoeren
- De gekozen actie van de gebruiker verwerken en uitvoeren
In code:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Welkom bij AP Adventure. Een avontuur voor moedige en minder moedige studenten. Ben je er klaar voor?");
GameManager theGame = new GameManager();
// De gameloop
while (!theGame.Exit)
{
theGame.DescribeLocation();
theGame.ToonActies();
theGame.VerwerkActie();
}
}
}Met de bool-property Exit van het GameManager-object stoppen we de loop wanneer het spel gedaan is.
GameObject
Op de verschillende locaties liggen voorwerpen. Die beschrijven we als GameObject:
class GameObject
{
public string Name { get; set; }
public string Description { get; set; }
public void Describe()
{
Console.WriteLine($"{Name}, {Description}.");
}
}Location
De gebruiker kan van locatie naar locatie gaan. Een locatie bestaat uit een aantal zaken:
- Een titel en een beschrijving
- Een lijst van GameObjecten (items) die zich op deze locatie bevinden
- Een lijst van Exits: de richtingen waarin de gebruiker naar een andere locatie kan gaan
Een eerste versie van onze klasse Location:
class Location
{
public Location()
{
Exits = new List<Exit>();
ObjectsInRoom = new List<GameObject>();
}
public string Title { get; set; }
public string Description { get; set; }
public List<Exit> Exits { get; private set; }
public List<GameObject> ObjectsInRoom { get; private set; }
public void Describe()
{
Console.WriteLine(Title + "\n*********");
Console.WriteLine(Description);
Console.WriteLine("Voorts zie je ook nog:");
foreach (GameObject item in ObjectsInRoom)
{
item.Describe();
}
Console.WriteLine("\n*********");
}
//...
}We gebruiken hier List<> in plaats van arrays. De lijsten hebben een private set: wie een Location gebruikt, kan er items en exits aan toevoegen met Add, maar kan de lijst zelf niet vervangen.
Exit
Elke exit van een locatie legt minstens 2 zaken vast:
- De richting waarin deze uitgang ligt (noord, oost, zuid, west)
- Een referentie naar het locatie-object waar deze uitgang naartoe leidt
We krijgen dus al:
class Exit
{
public Exit()
{
NeedsObject = new List<GameObject>();
}
public Directions ExitDirection { get; set; }
public Location GoesToLocation { get; set; }
public List<GameObject> NeedsObject { get; private set; }
//...
}Waarbij Directions een zelfgemaakt enum-type is:
enum Directions
{
North, South, West, East
}Van locatie veranderen
In de klasse Location voegen we een methode toe waarmee de GameManager te weten komt naar welke locatie de gebruiker gaat. De methode aanvaardt een Directions (de richting waarin de gebruiker wil gaan) en geeft een referentie terug naar het Location-object waar de gebruiker naartoe gaat. Is de richting niet geldig, dan tonen we dat op het scherm:
public Location GetLocationOnMove(Directions direction)
{
foreach (Exit exit in Exits)
{
if (exit.ExitDirection == direction)
{
return exit.GoesToLocation;
}
}
Console.WriteLine("Dat is geen geldige richting");
return this;
}Vinden we in de lijst Exits een exit in de gevraagde richting, dan geven we een referentie terug naar de bijhorende locatie (GoesToLocation). Vinden we er geen en komen we dus aan het einde van de foreach, dan verschijnt de tekst op het scherm en geven we een referentie terug naar de huidige locatie.
GameObjecten als voorwaarde om een exit te gebruiken
Stel dat je een bepaalde locatie pas mag betreden als je een bepaald GameObject bij je hebt. Daarvoor passen we 2 zaken aan:
- In de klasse
Exitstaat welk(e) object(en) je nodig hebt om de exit te gebruiken. - De methode
GetLocationOnMovecontroleert of de speler die objecten heeft wanneer hij naar een nieuwe locatie wil gaan.
De nieuwe, volledige klasse Exit:
class Exit
{
public Exit()
{
NeedsObject = new List<GameObject>();
}
public Directions ExitDirection { get; set; }
public Location GoesToLocation { get; set; }
public List<GameObject> NeedsObject { get; private set; }
public bool TestPassCondition(List<GameObject> playerInventory)
{
int passCount = 0;
for (int i = 0; i < NeedsObject.Count; i++)
{
if (playerInventory.Contains(NeedsObject[i]))
{
passCount++;
}
}
return passCount == NeedsObject.Count;
}
}In deze ietwat knullige code tellen we of de speler alle GameObjecten die je voor deze exit nodig hebt in zijn inventaris (playerInventory) heeft.
TestPassCondition gebruiken we nu in de methode GetLocationOnMove van de klasse Location, om te bepalen of de exit gebruikt mag worden:
public Location GetLocationOnMove(Directions direction, List<GameObject> playerInventory)
{
foreach (Exit exit in Exits)
{
if (exit.ExitDirection == direction)
{
if (exit.TestPassCondition(playerInventory))
{
return exit.GoesToLocation;
}
Console.WriteLine("Je kan hier niet langs (je hebt niet alle vereiste items).");
return this;
}
}
Console.WriteLine("Dat is geen geldige richting");
return this;
}GameManager
Rest ons enkel nog de klasse GameManager. Ruw gezien ziet die er zo uit:
class GameManager
{
private Location currentLocation;
private List<Location> gameLocations = new List<Location>();
private List<GameObject> playerInventory = new List<GameObject>();
public GameManager()
{
InitGame();
}
public bool Exit { get; private set; }
public void DescribeLocation()
{
//...
}
public void VerwerkActie()
{
//...
}
public void ToonActies()
{
//...
}
private void InitGame()
{
//...
}
}Wat meteen opvalt:
- De 3 publieke methoden
DescribeLocation,VerwerkActieenToonActies - Een instantievariabele
currentLocationmet een referentie naar de huidige locatie van de speler - 2 lijsten: de voorwerpen die de speler bij zich heeft (
playerInventory) en alle locaties in het spel (gameLocations) - Een methode
InitGamewaarin we bij de start van het spel alle gameobjecten, exits en locaties aanmaken - Een bool
Exit, zodat de gameloop inMainweet wanneer het spel gedaan is
We vullen nu de afzonderlijke methoden in.
DescribeLocation()
public void DescribeLocation()
{
currentLocation.Describe();
}VerwerkActie()
public void VerwerkActie()
{
string actie = Console.ReadLine();
bool error = false;
if (actie == "n")
{
currentLocation = currentLocation.GetLocationOnMove(Directions.North, playerInventory);
}
else if (actie == "o")
{
currentLocation = currentLocation.GetLocationOnMove(Directions.East, playerInventory);
}
else if (actie == "w")
{
currentLocation = currentLocation.GetLocationOnMove(Directions.West, playerInventory);
}
else if (actie == "z")
{
currentLocation = currentLocation.GetLocationOnMove(Directions.South, playerInventory);
}
else if (actie == "e")
{
Exit = true;
}
else
{
error = true;
}
if (error)
{
Console.WriteLine("Onbekende actie");
}
}De speler typt n, o, w of z, en de methode zoekt uit naar welke locatie hij gaat. De instantievariabele currentLocation wijst daarna naar de nieuwe (of dezelfde) locatie.
ToonActies()
public void ToonActies()
{
Console.WriteLine("Mogelijke acties: (typ bijvoorbeeld n als je naar het noorden wil)");
Console.WriteLine("n = noord");
Console.WriteLine("o = oost");
Console.WriteLine("z = zuid");
Console.WriteLine("w = west");
Console.WriteLine("e = exit");
}InitGame()
In deze methode leggen we de volledige inhoud van het spel vast. Wil je het spel uitbreiden met extra kamers en voorwerpen, dan doe je dat hier. Ter illustratie maken we eerst 2 locaties en hangen we ze met exits aan elkaar (kamer 1 ligt ten zuiden van kamer 2):
private void InitGame()
{
// Locaties maken
Location l1 = new Location()
{
Title = "De poort",
Description = "Je staat voor een grote grijze poort die op een kier staat. Rondom je is prikkeldraad, je kan enkel naar het noorden, door de poort gaan."
};
Location l2 = new Location()
{
Title = "Receptie",
Description = "De receptie... veel blijft er niet meer over van wat eens een bruisende omgeving was. Hier en daar zie je skeletten van, waarschijnlijk, enkele studenten. Een grote poort staat op een kier naar het zuiden. Je ziet twee deuren aan de westelijke en noordelijke zijde."
};
// Exits plaatsen
l1.Exits.Add(new Exit() { ExitDirection = Directions.North, GoesToLocation = l2 });
l2.Exits.Add(new Exit() { ExitDirection = Directions.South, GoesToLocation = l1 });
// Locaties toevoegen
gameLocations.Add(l1);
gameLocations.Add(l2);
currentLocation = l1;
}Vergeet niet om op het einde de 2 kamers toe te voegen aan de lijst gameLocations van de GameManager, en in te stellen in welke kamer het spel begint.
Een voorwerp als voorwaarde om een kamer binnen te gaan
Stel dat we in een kamer een sleutel leggen die je nodig hebt om een andere kamer te openen. In InitGame schrijven we dan:
Location l6 = new Location()
{
Title = "Gang",
Description = "Een brede gang waar makkelijk 5 mensen schouder aan schouder door kunnen. Zowel in het oosten als het westen zie je een deur."
};
Location l7 = new Location()
{
Title = "Computerruimte",
Description = "Eindelijk, je hebt het gehaald. De plek waar iedereen naartoe wil: het computerlabo!"
};
// Voorwerpen in de kamers leggen
GameObject keyToL7 = new GameObject() { Description = "Verroest en groot", Name = "Sleutel" };
l5.ObjectsInRoom.Add(keyToL7);
//...
// De deur naar de computerruimte zit op slot
Exit naarComputerruimte = new Exit() { ExitDirection = Directions.West, GoesToLocation = l7 };
naarComputerruimte.NeedsObject.Add(keyToL7);
l6.Exits.Add(naarComputerruimte);
l6.Exits.Add(new Exit() { ExitDirection = Directions.East, GoesToLocation = l4 });
// Locaties toevoegen
//...
gameLocations.Add(l6);
gameLocations.Add(l7);Een volledige InitGame ter illustratie
De belangrijkste onderdelen zijn geschreven. Daarom tonen we een iets uitgebreider spel, een demo zeg maar, waarin alles samen in actie komt. De kaart ziet er zo uit:
Cafetaria (sleutel)
|
Computerruimte -- Gang -- Tuin
(slot) |
Koffieruimte -- Receptie
|
De poort
private void InitGame()
{
// Locaties maken
Location l1 = new Location()
{
Title = "De poort",
Description = "Je staat voor een grote grijze poort die op een kier staat. Rondom je is prikkeldraad, je kan enkel naar het noorden, door de poort gaan."
};
Location l2 = new Location()
{
Title = "Receptie",
Description = "De receptie... veel blijft er niet meer over van wat eens een bruisende omgeving was. Hier en daar zie je skeletten van, waarschijnlijk, enkele studenten. Een grote poort staat op een kier naar het zuiden. Je ziet twee deuren aan de westelijke en noordelijke zijde."
};
Location l3 = new Location()
{
Title = "Koffieruimte",
Description = "Je staat in de koffieruimte achter de receptie. Menig pralinetje is hier vroeger met veel gusto opgesmikkeld. Een lege pralinedoos is het enige bewijs dat het hier ooit gezellig was. Een deur naar het oosten is de enige uitgang."
};
Location l4 = new Location()
{
Title = "Tuin",
Description = "Het is duidelijk herfst. Een kale boom en veel bruine bladeren op de grond... mistroostiger kan eigenlijk niet. Je ziet een deur in het zuiden en in het westen, en een grote klapdeur naar het noorden."
};
Location l5 = new Location()
{
Title = "Cafetaria",
Description = "Ooit was dit een bruisende locatie: veel eten, geroezemoes en licht door de grote ruiten. Nu enkel stof en lege tafels. Enkel een klapdeur is zichtbaar, naar het zuiden."
};
Location l6 = new Location()
{
Title = "Gang",
Description = "Een brede gang waar makkelijk 5 mensen schouder aan schouder door kunnen. Zowel in het oosten als het westen zie je een deur."
};
Location l7 = new Location()
{
Title = "Computerruimte",
Description = "Eindelijk, je hebt het gehaald. De plek waar iedereen naartoe wil: het computerlabo!"
};
// Voorwerpen in de kamers leggen
GameObject keyToL7 = new GameObject() { Description = "Verroest en groot", Name = "Sleutel" };
l5.ObjectsInRoom.Add(keyToL7);
// Exits plaatsen
l1.Exits.Add(new Exit() { ExitDirection = Directions.North, GoesToLocation = l2 });
l2.Exits.Add(new Exit() { ExitDirection = Directions.South, GoesToLocation = l1 });
l2.Exits.Add(new Exit() { ExitDirection = Directions.West, GoesToLocation = l3 });
l2.Exits.Add(new Exit() { ExitDirection = Directions.North, GoesToLocation = l4 });
l3.Exits.Add(new Exit() { ExitDirection = Directions.East, GoesToLocation = l2 });
l4.Exits.Add(new Exit() { ExitDirection = Directions.South, GoesToLocation = l2 });
l4.Exits.Add(new Exit() { ExitDirection = Directions.West, GoesToLocation = l6 });
l4.Exits.Add(new Exit() { ExitDirection = Directions.North, GoesToLocation = l5 });
l5.Exits.Add(new Exit() { ExitDirection = Directions.South, GoesToLocation = l4 });
// De deur naar de computerruimte zit op slot: daar heb je de sleutel voor nodig
Exit naarComputerruimte = new Exit() { ExitDirection = Directions.West, GoesToLocation = l7 };
naarComputerruimte.NeedsObject.Add(keyToL7);
l6.Exits.Add(naarComputerruimte);
l6.Exits.Add(new Exit() { ExitDirection = Directions.East, GoesToLocation = l4 });
l7.Exits.Add(new Exit() { ExitDirection = Directions.East, GoesToLocation = l6 }); // de winnende kamer
// Locaties toevoegen
gameLocations.Add(l1);
gameLocations.Add(l2);
gameLocations.Add(l3);
gameLocations.Add(l4);
gameLocations.Add(l5);
gameLocations.Add(l6);
gameLocations.Add(l7);
currentLocation = l1;
}What’s missing? Veel!
De sleutel ligt in de cafetaria, maar de speler kan hem nog niet oprapen. De computerruimte blijft dus voorlopig op slot. Een eerste uitdaging: hoe laat je de speler voorwerpen van de grond oprapen en in zijn inventaris steken? Dat raadsel laten we aan jou over!