Programmera spel i C++ för nybörjare/Funktionsanrop i klasser och polymorfism


Många elever kämpar och sliter med hur man anropar funktioner inuti klasser i C++. Eftersom klasser och arv är grunden för hur man befolkar spel är det viktigt att man greppar hur det hela hänger ihop.

Här nedanför finns flera kodexempel på samma kod som bara blir mer och mer avancerad. Det skadar inte att du läst de olika kapitlen om hur det görs med SFML men koden går att följa med en vanlig C++ kompilator utan SFML installerat.

Version 1

redigera

Den allra enklaste varianten. Här har vi en basklass som heter Living. Ur den låter vi en människa, en katt och en växt ärvas och därefter ser vi hur dessa (inline) funktioner kan anropas utifrån:

#include "stdafx.h"
#include <iostream>

class Living
{
public:
void Live() { std::cout << "Living creature living.\n"; }
};

class Human: public Living
{
public:
void Live() { std::cout << "Human living.\n"; }

};


class Cat: public Living
{
public:
void Live() { std::cout << "Cat living.\n"; }
};

class Plant: public Living
{
public:
void Live(){ std::cout << "Plant living.\n"; }
};



int _tmain(int argc, _TCHAR* argv[])
{

  //Skapar en ny av varje sort utom Living
  Human NewHuman;
  Cat NewCat;
  Plant NewPlant;

//Visa att funktionen anropas

NewHuman.Live();
NewCat.Live();
NewPlant.Live();


return 0;
}

Version 2

redigera

Nu brukar man sällan bara skapa en ny variant, då behövs inte klasser alls, istället skapar man normalt många och lägger dem eftersom i en lista:

#include "stdafx.h"
#include <iostream>

class Living
{
public:
 void Live() { std::cout << "Living creature living.\n"; }
};

class Human: public Living
{
public:
 void Live() { std::cout << "Human living.\n"; }

};


class Cat: public Living
{
public:
 void Live() { std::cout << "Cat living.\n"; }
};

class Plant: public Living
{
public:
 void Live(){ std::cout << "Plant living.\n"; }
};



int _tmain(int argc, _TCHAR* argv[])
{
   const int LIMIT = 1;

   //Skapar en ny av varje sort utom Living och skapar listan samtidigt
   Human listHuman[LIMIT];
   Cat listCat[LIMIT];
   Plant listPlant[LIMIT];

//Räkna igenom allihop
for ( int i = 0; i < LIMIT; ++i )
{
 listHuman[i].Live();
 listCat[i].Live();
 listPlant[i].Live();
}

	return 0;
}

Koden är rätt enkel att följa. LIMIT ges ett värde på 1 bara för att vi skall veta var den nya människan/katten/växten hamnar i listan. Vi stegar igenom listan och anropar vart och ett av de olika arvens funktioner.


Version 3

redigera

Nu gör vi det litet svårare, men mer kopplat till hur man skapar spelare i ett spel. Vi skapar tre olika listor, en för varje sort, som rymmer max 100 av varje. Därefter skapar vi en ny av varje människa, katt och planta och anropar dem därefter.

#include "stdafx.h"
#include <iostream>


class Living
{
public:
 void Live() { std::cout << "Living creature living.\n"; }
};

class Human: public Living
{
public:
 void Live() { std::cout << "Human living.\n"; }

};


class Cat: public Living  
{
public:
 void Live() { std::cout << "Cat living.\n"; }
};

class Plant: public Living
{
public:
 void Live(){ std::cout << "Plant living.\n"; }
};



int _tmain(int argc, _TCHAR* argv[])
{
	const int LIMIT = 1;

	//Människor
	Human *humanlista[100];//max 100 stycken

	humanlista[LIMIT] =  new Human;
	humanlista[LIMIT]->Live();

	//Katter
	Cat *kattlista[100];//max 100 stycken

	kattlista[LIMIT] =  new Cat;
	kattlista[LIMIT]->Live();

	//Växter
	Plant *plantlista[100];//max 100 stycken

	plantlista[LIMIT] =  new Plant;
	plantlista[LIMIT]->Live();

	return 0;
}

Denna kod kanske är ändå lättare att förstå. Vi skapar en ny av varje sort, lägger in den i listan (hade vi gjort fler hade vi varit tvungna att räkna upp värdet från LIMIT) och anropar funktionen. Detta är det andra, vanliga sättet man skapar classarv i listor. Inte heller detta sätt bör vara alltför svårt att förstå sig på.


Version 4

redigera

Anropa originalfunktionen som ärvts från en basklass istället för den som ingår i den ärvda klassen, även när de har exakt samma namn:

#include "stdafx.h"
#include <iostream>


class Living
{
public:
 void Live() { std::cout << "Living creature living.\n"; }
};

class Human: public Living
{
public:
 void Live() { std::cout << "Human living.\n"; }

};


class Cat: public Living
{
public:
 void Live() { std::cout << "Cat living.\n"; }
};

class Plant: public Living
{
public:
 void Live(){ std::cout << "Plant living.\n"; }
};



int _tmain(int argc, _TCHAR* argv[])
{
	const int LIMIT = 1;

	//Människor
	Human *humanlista[100];//max 100 stycken

	humanlista[LIMIT] =  new Human;
	humanlista[LIMIT]->Live();

	//Anropa funktionen från originalklassen
	humanlista[LIMIT]->Living::Live();


	//Katter
	Cat *kattlista[100];//max 100 stycken

	kattlista[LIMIT] =  new Cat;
	kattlista[LIMIT]->Live();

	//Anropa funktionen från originalklassen
	kattlista[LIMIT]->Living::Live();

	//Växter
	Plant *plantlista[100];//max 100 stycken

	plantlista[LIMIT] =  new Plant;
	plantlista[LIMIT]->Live();

	//Anropa funktionen från originalklassen
	plantlista[LIMIT]->Living::Live();
	return 0;
} 

Som du ser kan samtliga olika varianter av människa, katt och växt anropa basklssens funktion också och använda sig av den inuti koden.

Version 5

redigera

Virtuellt arv

Om man ärver från två olika klasser som har samma namn på en funktion är det bäddat för problem. För att undvika ”diamond of doom” som det kallas måste arvet anges som ”virtual”. Här har både katt och människor fått ett arv av Living vilket angetts som virtual så att en ny klass som heter Catpeople kan ärva från bägge och använda samma funktion ”Live()” utan att få felet "ambigious".

#include "stdafx.h"
#include <iostream>


class Living
{
public:
 void Live() { std::cout << "Living creature living.\n"; }
};

//Observera att det är ett virtuellt arv
class Human: public virtual Living
{
public:
 void Live() { std::cout << "Human living.\n"; }

};

 //Observera att det är ett virtuellt arv
class Cat: public virtual Living 
{
public:
 void Live() { std::cout << "Cat living.\n"; }
};

class Plant: public Living 
{
public:
 void Live(){ std::cout << "Plant living.\n"; }
};

//En helt ny klass som ärver två andra klasser som i sin tur ärvt av Living
 class Catpeople: public Human, public Cat
 {
public:
 void Live() { std::cout << "Catpeople living.\n"; }
};

int _tmain(int argc, _TCHAR* argv[]) 
{
	const int LIMIT = 1;

	//Människor
	Human *humanlista[100];//max 100 stycken

	humanlista[LIMIT] =  new Human;
	humanlista[LIMIT]->Live();



	//Katter
	Cat *kattlista[100];//max 100 stycken

	kattlista[LIMIT] =  new Cat;
	kattlista[LIMIT]->Live();


	//Växter
	Plant *plantlista[100];//max 100 stycken

	plantlista[LIMIT] =  new Plant;
	plantlista[LIMIT]->Live();

		//Catpeople
	Catpeople *catpeoplelista[100];//max 100 stycken

	catpeoplelista[LIMIT] =  new Catpeople;
	catpeoplelista[LIMIT]->Live();

	//Anropa funktionen från originalklassen
	catpeoplelista[LIMIT]->Living::Live();

	return 0;
}

Version 6

redigera

Sen bindning och polymorfism

Observera att man kan ändra funktionen ”Live” till en virtuell funktion. Kör du Catpeople koden skriver den inte ut någonting alls när du anropar moderfunktionen. Det kan användas om man behöver göra kopior av både moderklassen och ärvda klasser.

//Skapa en virtuell funktion
virtual void Live() {};


Det är vanligare att man skapar en basklass som man bara har som ritning/mall. Gör då om basklassen till pure abstract och ändra funktionen Live till:

//Skapa en virtuell funktion
virtual void Live() = 0;

Nu får du error om du anropar basklassens funktion i Catpeople om du försöker att anropa basklassens funktion, så du får ta bort raden:

//catpeoplelista[LIMIT]->Living::Live();

Detta eftersom en funktion i en abstract basklass inte kan anropas, bara ärvas.


#include "stdafx.h"
#include <iostream>


class Living
{
public:
	//Skapa en virtuell funktion
	virtual void Live() = 0;
};

class Human: public virtual Living
{
public:
 void Live() { std::cout << "Human living.\n"; }

};


class Cat: public virtual Living 
{
public:
 void Live() { std::cout << "Cat living.\n"; }
};

class Plant: public Living 
{
public:
 void Live(){ std::cout << "Plant living.\n"; }
};

class Catpeople: public Human, public Cat
{
public:
 void Live() { std::cout << "Catpeople living.\n"; }
};

int _tmain(int argc, _TCHAR* argv[])
{
	const int LIMIT = 1;

	//Människor
	Human *humanlista[100];//max 100 stycken

	humanlista[LIMIT] =  new Human;
	humanlista[LIMIT]->Live();



	//Katter
	Cat *kattlista[100];//max 100 stycken

	kattlista[LIMIT] =  new Cat;
	kattlista[LIMIT]->Live();


	//Växter
	Plant *plantlista[100];//max 100 stycken

	plantlista[LIMIT] =  new Plant;
	plantlista[LIMIT]->Live();


	//Catpeople
	Catpeople *catpeoplelista[100];//max 100 stycken

	catpeoplelista[LIMIT] =  new Catpeople;
	catpeoplelista[LIMIT]->Live();

	//Anropa funktionen från originalklassen
	//catpeoplelista[LIMIT]->Living::Live();
       


       /*-------------------------------------------------------*/
       /* Härifrån kommer vi in på sen bindning och polymorfism */
       /*-------------------------------------------------------*/

	//Skapa en tom pekare av typen Living, dvs basklassen typ som är helt virtuell nu
	Living *pLiving;

	//Låt den byta form så att du får fram samtliga klassderivats Live funktion
	// = sen bindning eller polymorfism


       //Först blir pekaren en människa:
	pLiving = humanlista[LIMIT];
	pLiving->Live();


       //Sedan blir pekaren en katt
	pLiving = kattlista[LIMIT];
	pLiving->Live();


       //Sedan blir pekaren en planta
	pLiving = plantlista[LIMIT];
	pLiving->Live();
      

       //Slutligen blir pekaren en kattmänniska
	pLiving = catpeoplelista[LIMIT];
	pLiving->Live();

	return 0;
}

Genom att pekaren pekar på minnesadresserna (en lista är alltid en rad med adresser egentligen) kan man använda sig av en pekare av typen Living, basklassen, som sedan pekar på olika minnesadresser. Även om de ändrats och funktionerna fått olika nya värden kommer man fortfarande att kunna anropa pekaren som tar ny form för varje gång. Detta är en oerhört stor hjälp när man har spel med mängder av olika fiender av olika varianter inom samma släkte. T.ex. orcher: orchhövdingar, orchbågskyttar, orchkrigare, orchspanare osv. De kan samtliga anropas och manipuileras med hjälp av en enda pekare.