Hoppa till innehållet

Programmera spel i C++ för nybörjare/Sprites och spelpjäser 9

Från Wikibooks


(Genomgången förutsätter att du har en fungerande installation av Microsoft Visual C++ 2010 Express eller liknande C++ kompilator på din dator. SFML 1.6 behövs egentligen inte.)

Polymorfism

[redigera]

Polymorfism är ett viktigt begrepp i mer avancerad spelprogrammering och det är kanske överkurs att ta up det i en nybörjarbeskrivning, men eftersom man kan ha så mycket nytta av polymorfism tar jag med det ändå. Vi stötte på kommandot ”virtual” när vi jobbade med multipelt arv, men då gällde det klasser. Det man kallar ”polymorfism” när man programmerar C++ (och liknande språk) är då man talar om hur en klass kan ärva själva funktionsanropet, men inte funktionen. Klassens funktion är virtuell, inte själva klassen, och slutresultat beror på uppbyggnaden av koden under programkörningen och inte under kompileringen. (Se i botten hur man hanterar pekare och adresser).

Vad är det för bra med det? Jo, det innebär att man oavsett vilken sorts klass man skapat kan man ange samma funktion men få ut olika resultat beroende på vilken variant av klassen man har. Snurrigt?

Anta att vi har vår fordonsklass från förra kapitlet.

Class fordon
{
public:
//Konstruktordeklaration, definition utanför klassdeklarationen
fordon(int  hastighet, double spelare_x, double spelare_y, int bensin, int pansar);//startvärden

//Destruktion
~fordon(){};

int  hastighet; //Hur snabb är den
double spelare_x; // var är den i sidled i programmet
double spelare_y; //var är den i höjdled i programmet
int bensin; //Hur långt kan den köra
int pansar; //Hur mycket tål den
//sf::Sprite sprite; //Används senare i grafiskt läge
};

Anta att vi lägger till en funktion i klassen som visar vad som skapats, istället för att som tidigare skrika ut att vi har ett nybyggt fordon i konstruktionsdeklarationen kan vi ha den som en egen funktion inuti klassen:

 void Nybyggd() { std::cout << "Ett fordon rullar ut från fabriken!" << std::endl; }

Men... nu skulle det handla om polymorfism. Istället för att se att ett fordon skapats oavsett om man gjort en pansarvagn, pansarbil eller lastbil (eller något annat fordon) vill vi ju kunna anropa klassen och få fram just den speciella klasens funktionsversion. För att det skall fungera måste funktionen bli virtuell:

virtual void Nybyggd(){};

Det finns olika sätt att fylla i att en funktion är virtuell, det går lika bra att skriva:

 void virtual Nybyggd(){};

Man anger bara i funktionsdeklarationen om en funktion är virtuell. Om man har funktionsdeklarationen inuti klassen men funktionsbeskrivningen utanför skriver man:

 virtual void Nybyggd(){}; //Inuti klassen
void fordon::Nybyggd(){std::cout << "Ett fordon rullar ut från fabriken!" << std::endl;} //Utanför klassen

Virtual

[redigera]

Som du såg tidigare i exemplet med olika stridsvagnar som ärver samma klass så skriver den "nya" pansarvagnen över den gamla pansarvagnens funktion om funktionsnamnen är likadana, vill man undvika det skapar man virtuella funktioner. Är det bra? Ja, ta exemplet med skott från stridsvagnar. Tigertanken hade en "override", en funktion med exakt samma namn som basklssens funktionsnamn. I värsta fall kan koden skrivas så att kompilatorn blir osäker på vilken funktion som egentligen skall användas då de har samma namn, och kanske använder fel funktion. Genom att använda polymorfism och virtuella funktioner försvinner det problemet helt. Det går inte att göra alla funktioner i en klass virtuella; kunstruktionsfunktionen kan inte vara virtuell (även om destruktionsfunktionen kan det), funktioner märkta som "inline" kan inte heller göras virtuella och enbart medlemsfunktioner kan göras virtuella. Det innebär att funktioner som ligger under "private:" delen i klassen inte kan göras virtuella. Skriv istället en separat rubrik med namnet "protected:" i klassen och lägg de virtuella funktionerna där så går de att ärva lika enkelt som de som står under "public:".

Abstrakt klass och basklass

[redigera]

Det går (som synes) att skriva virtuella funktioner i en klass så här:

virtual void Nybyggd(){std::cout << "Ett fordon rullar ut från fabriken!" << std::endl;}; //Virtuell funktion

och sedan bara köra en override med funktioner som har samma namn i klasser som ärver från fordonsklassen, men det finns också sätt att göra funktionerna "tomma", dvs. det är upp till programmeraren att fylla dem med vad som skall hända, det gör man genom att göra funktionen "pure virtual" och utan funktionsdefinition.

I samma ögonblick som du anger att en funktion i en klass är "pure virtual" / rent virtuell har klassen också blivit en sk. "abstrakt klass". Då kan du bara göra ett derivat, en klass som ärvt av den abstrakta klassen. I en rent virtuell klass anges de virtuella funktionerna med en nolla:

virtual void Nybyggd()=0; //Rent virtuell funktion som förvandlar hela klassen
                          //till en abstrakt klass. En klass kan ha obegränsat
                         //antal rent virtuella funktioner.

Om du försöker att skapa en kopia av en basklass/abstrakt klass kommer kompilatorn att markera ett fel. Dvs. det blir fel nu om du skriver:

fordon fordon2; //FEEEEEEL!!!!!!

Istället får du skapa en ny klass som ärver från fordonsklassen med:

class lastbil : public fordon
{osv.....

lastbil lastbil2; //ny lastbil skapas

Vanligtvis pratar man om att den klass som innehåller de virtuella funktionerna är en ”basklass” (base class på engelska). Dvs. den klass som alla andra klasser kommer ifrån. För att man skall kunna skapa kopior av en abstrakt klass (så att den blir en basklass) krävs det att samtliga rent virtuella funktioner som ärvts har fått nya definitioner och förklaringar.

Komplett fordonsklass

[redigera]
Class fordon
{
public:
//Konstruktordeklaration, definition utanför klassdeklarationen
fordon(int  hastighet, double spelare_x, double spelare_y, int bensin, int pansar);//startvärden

//Destruktion
~fordon(){};

virtual void Nybyggd(){}; //Virtuell funktion

int  hastighet; //Hur snabb är den
double spelare_x; // var är den i sidled i programmet
double spelare_y; //var är den i höjdled i programmet
int bensin; //Hur långt kan den köra
int pansar; //Hur mycket tål den
//sf::Sprite sprite; //Används senare i grafiskt läge
};

Konstruktionsdeklarationen

//Konstruktionsdeklaration, fordon--------------------------------------------------
fordon::fordon (int  ut_hastighet, double ut_spelare_x, double ut_spelare_y, int ut_bensin, int ut_pansar)  
{
     hastighet=ut_hastighet;
     spelare_x=ut_spelare_x;
     spelare_y=ut_spelare_y;
     bensin = ut_bensin;
     pansar = ut_pansar;
}

Anta nu att vi skapar en lastbil också:


class lastbil : public fordon
{
public:

Int lastkapacitet; //Avgör antalet människor som får plats
Int passagerare; // Avgör hur många som finns i lastbilen förutom chauffören.
lastbil(int  ut_hastighet, double ut_spelare_x, double ut_spelare_y, int ut_bensin, int ut_pansar)
:fordon(ut_hastighet, ut_spelare_x, ut_spelare_y, ut_bensin, ut_pansar)
{
lastkapacitet=11;
passagerare = 0; 
}

//Destruktion
~lastbil(){};

//Funktioner

 void Nybyggd() { std::cout << "En lastbil rullar ut från fabriken!" << std::endl; }

void ilastning()
{
std::cout << "Lastar i ” <<  lastkapacitet << ” soldater!” << std::endl;
}

void urlastning()
{
std::cout << "Lastar ur ” <<  lastkapacitet << ” soldater!” << std::endl;
}


};

Sedan skapar vi en pansarvagn:

class stridsvagn : public fordon
{ 
public:

Int ammunition; // Avgör hur många skott som finns i stridsvagnen.
stridsvagn(int  ut_hastighet, double ut_spelare_x, double ut_spelare_y, int ut_bensin, int ut_pansar)
:fordon(ut_hastighet, ut_spelare_x, ut_spelare_y, ut_bensin, ut_pansar)
{
ammunition=100; 
}

//Destruktion
~stridsvagn(){};

//Funktioner
void Nybyggd() { std::cout << "En pansarvagn rullar ut från fabriken!" << std::endl; }

void skott()
{
std::cout << "Skott kommer!” << std::endl;
}

};

I main-loopen är det sedan bara att anropa funktionen, den har samma namn oavsett vilken klass som ärvt fordon, men ger olika resultat beroende på hur funktionen utformats. Det är sas. kärnan i begreppet polymorfism, att något ser likadant ut men egentligen är olika beroende på situationen.

Komplett kod

[redigera]
#include <iostream>
#include <SFML\System.hpp>
#include <SFML\Graphics.hpp>
#include <SFML\Window.hpp>


using namespace std;

//En moderklass för alla fordon i spelet
class fordon 
{
public:
//Konstruktordeklaration, definition utanför klassdeklarationen
fordon(int  hastighet, double spelare_x, double spelare_y, int bensin, int pansar);//startvärden

//Destruktion
~fordon(){};

virtual void Nybyggd(){}; //Virtuell funktion

int  hastighet; //Hur snabb är den
double spelare_x; // var är den i sidled i programmet
double spelare_y; //var är den i höjdled i programmet
int bensin; //Hur långt kan den köra
int pansar; //Hur mycket tål den
//sf::Sprite sprite; //Används senare i grafiskt läge
};


//Konstruktionsdeklaration, fordon--------------------------------------------------
fordon::fordon (int  ut_hastighet, double ut_spelare_x, double ut_spelare_y, int ut_bensin, int ut_pansar) 
{
    hastighet=ut_hastighet;
    spelare_x=ut_spelare_x;
    spelare_y=ut_spelare_y;
    bensin = ut_bensin;
    pansar = ut_pansar; 
}



//Skapa en klass som ärver fordon
class lastbil : public fordon  
{
public:

int lastkapacitet; //Avgör antalet människor som får plats
int passagerare; // Avgör hur många som finns i lastbilen förutom chauffören.
lastbil(int  ut_hastighet, double ut_spelare_x, double ut_spelare_y, int ut_bensin, int ut_pansar)
:fordon(ut_hastighet, ut_spelare_x, ut_spelare_y, ut_bensin, ut_pansar)
{
lastkapacitet=11;
passagerare = 0;
}

//Destruktion
~lastbil(){};

//Funktioner
void Nybyggd() { std::cout << "En lastbil rullar ut från fabriken!" << std::endl; }

void ilastning()
{
std::cout << "Lastar i " <<  lastkapacitet << " soldater!" << std::endl;
}

void urlastning()
{
std::cout << "Lastar ur " <<  lastkapacitet << " soldater!" << std::endl;
}

};



//Skapa en klass som ärver fordon
class stridsvagn : public fordon
{
public:

int ammunition; // Avgör hur många skott som finns i stridsvagnen.
stridsvagn(int  ut_hastighet, double ut_spelare_x, double ut_spelare_y, int ut_bensin, int ut_pansar):
fordon(ut_hastighet, ut_spelare_x, ut_spelare_y, ut_bensin, ut_pansar) 
{
ammunition=100;
}

//Destruktion
~stridsvagn(){};

//Funktioner
void Nybyggd() { std::cout << "En pansarvagn rullar ut från fabriken!" << std::endl; }

void skott()
{
std::cout << "Skott kommer!" << std::endl; 
}

};




int main() 
  {  //Början av programkörningen

//Skapa först en stridsvagn
stridsvagn stridsvagn1(50, 100.0,100.0,100,100);
cout << "Pansarvagnen har " << stridsvagn1.ammunition << " skott" << endl ;
stridsvagn1.Nybyggd(); //Kolla om vi byggt en stridsvagn

//Skapa sedan en lastbil
lastbil lastbil1(100, 200.0,200.0,200,10);
//Testa med ett funktionsanrop om den finns
lastbil1.ilastning();
lastbil1.Nybyggd(); //Kolla om vi byggt en lastbil


return 0;
} //slut på programkörningen

Polymorfism och pekare

[redigera]

Det går precis lika bra att skapa en lastbil genom att skapa en pekare till den:

lastbil *lastbil2 = new lastbil(100, 200.0,200.0,200,10); 
lastbil2->Nybyggd(); //Testar om det verkligen är en lastbil vi pekar på.

Men den riktiga charmen med polymorfism kommer först när man anropar det man skapar genom att skapa en pekare som pekar på basklassen när man skapar en deriverad klass. Jämför med det här exemplet:

fordon *pfordon = new lastbil(100, 200.0,200.0,200,10); //Det går inte att ta en kopia på en abstrakt klass, 
                                                        // men man kan skapa en pekare av typen abstrakt klass
                                                        // och ange den som mall för en klasskopia.
pfordon->Nybyggd(); //Testar om det verkligen är en lastbil vi pekar på.

Vi anger inte förrän under själva programkörningen att det vi skapar av den abstrakta fordonsklassen faktiskt blir en lastbil. Det är genom den här formen av nyskapande som polymorfismen blir riktigt användbar. Var bara noga med att samtliga funktioner i den klass du anropar verkligen är rent virtuella, t.ex.

virtual void Nybyggd()=0;

man kan t.o.m. skriva på följande sätt:

fordon *b; //Skapar en pekare av typen fordon
lastbil L(100, 200.0,200.0,200,10); //Skapar en kopia av klassen lastbil som heter L
b = &L; //Säger att pekaren av typen fordon skall peka på lastbilen L
b->Nybyggd(); //Testar om det verkligen är en lastbil vi pekar på.

Radera på ett snyggt sätt

[redigera]

Om vi skapat en kopia av en abstrakt klass får vi en minnesläcka när kopian raderas. Enklast är att också se till så att destruktorn i klassen är virtual, dvs:

virtual ~fordon(){};

i fordonsklassen. När man sedan raderar pfordon - pekaren till ett nytt fordon, i exemplet ovan med:

delete pfordon;

så återställs minnet tillbaka till operativsystemet på ett snyggt sätt.

(Om du däremot skriver delete b; får du hemska fel, fast jag vet inte varför...)