Programmera spel i C++ för nybörjare/Sprites och spelpjäser 4
(Genomgången förutsätter att du har en fungerande installation av Microsoft Visual C++ 2010 Express och SFML 1.6 på din dator.)
Pekare används rikligt i alla former av C++ programmering. Du har en beskrivning för nybörjare här:
Programmera spel i C++ för nybörjare/C++ referenser online/pekare i C++ för nybörjare
-> tecknet
[redigera]När du tittar på andra spelprogrammerares kod kommer du ofta, faktiskt väldigt ofta, att stöta på den här symbolen -> och den är kopplad till hur man kommer åt information inne i klasser med hjälp av pekare.
Du vet nu att om du vill skapa en ny ko i spelet (en sk. Instans av ko) skriver du
spelare ko (50, 'c', 100.f, 400.f);
Dvs, klassnamnet och ett nytt namn på kopian du skapat. För det är vad du gjort, en kopia av en redan existerande klass. Vill du komma åt värdet på hastiheten skriver du bara
cout << ko.hastighet << endl;
Variablerna är på det hela taget enkla att komma åt och både läsa av och ändra.
Det finns ett annat sätt att skapa vår ko, och det är genom att skapa en pekare till kon. Koden för det är:
spelare *ko0 = new spelare(50, 'c', 100.f, 400.f);
pekaren skapar en ko i minnet. Kon finns inte i det minnesutrymme som heter stacken, utan i högen (heap). Det innebär att programmet har tagit eget minnesutrymme och det måste vi ge tillbaka när spelet stängs. Det gör vi med en sk. "destruktor". En destruktor har exakt samma namn som konstruktorn, men med ett tildetecken (~) framför och skrivs in inuti klassdeklarationen efter konstruktorn. Koden blir:
~spelare(){};
Förutom destruktorn måste vi ha en någon form av kod inuti programmet som aktiverar utraderingen av det vi skapat. Det sker med kommandot delete. Raderingen sker vanligtvis vid två olika tillfällen i koden. Det sker naturligtvis i samband med att programmet avslutas, då måste allt minne vi tagit ur "heapen" återställas så att andra program kan använda det. Men det sker också ifall det vi skapat dör. Anta att pojken slår ihjäl kon. Då finns det minnesutrymme för en ny ko om vi raderar ut den döda kon ur minnet. Kommandot för att radera är enkelt, i det här fallet raderar vi ko0:
delete ko0;
Observera att det inte finns någon pekarsymbol (*) i raderingskommandot.
Stacken och högen
[redigera]Datorn arbetar med två olika minnesutrymmen: "stack" och "heap" eller stacken och högen. Anta att du sitter vid ett skrivbord. När du startar ditt program skapas automatiskt en hög med papper på skrivbordet, lika hög som det minne du behöver. Varje papper har ett sidnummer. När du skriver int i = 0; säger du egentligen att du skapar ett heltal på sidan 33 (hittar jag på) och om du bläddrar fram till blad 33 i högen med papper står det en nolla där. Detta kallas på dataspråk för "stacken". Den går fort att bläddra igenom, men nackdelen är att den har färdig storlek. Du kan inte lägga till- eller dra ifrån blad ur stacken. Stackens minne är statiskt.
När du sedan skapar något nytt med "new" kommandot får det inte plats i stacken. Tänk dig istället att du har en stor grön plastbinge för pappersåtervinning bakom ryggen. Där kan man få plats med många papper. När du skriver "new" tar du egentligen fram ett blad. På det skriver du någonting (beroende på new). Du kopplar en pekare till den - sätter fast en klädnypa i bladet som du knyter fast med en tråd till ditt pekfinger. Sedan kastar du bladet i pappersbingen. Så länge pekaren finns där, eller så länge snöret mellan ditt pekfinger och klädnypan finns kvar, kommer du alltid att hitta bladet. Du har lagt till bladet i "heap", högen. Högen är inte lika strukturerad och lätt att hitta i som stacken, men den rymmer mycket mer eftersom minnesutrymmet är dynamiskt.
När du sedan använder kommandot "delete" raderar du det minnesutrymme du tagit i anspråk. Du drar i snöret så att pappersbladet dras ut ur pappersbingen så att andra papper får plats. Om du av misstag ändrar pekaren under programmets gång innebär det att du klipper av tråden mellan dig och klädnypan och bladet försvinner i pappersbingen. Bladet kommer aldrig mer att hittas och det utrymme bladet tar upp kommer att vara ockuperat tills dess att datorn startas om. Du har skapat det man på fackspråk kallar en "minnseläcka", så var rädd om pekarna till objekt du skapat i högen.
Kontakta pekare
[redigera]Om vi vill få den här kon att råma får vi problem. Det går inte att skriva ko0.ljud(ko0.ras) till en ko baserad på en pekare; Istället får du skriva
(*ko0).ljud((*ko0).ras);
Verkar det jobbigt? Det är tillräckligt jobbigt för tillräckligt många programmerare, så istället för att skriva (*ko0.) finns kommandot -> istället. Vill vi få kon att råma kan vi alltså lika gärna skriva:
ko0->ljud(ko0->ras);
Om du själv irriterar dig på alla pilsymboler och tycker att koden blir svårläst kan du alltså lika gärna skriva pekaren i parentes som det första exemplet.
Få in en sprite i class
[redigera]Det finns olika sätt att få in en sprite i vår klass. Det allra enklaste och bästa är att helt enkelt ha en sprite deklarerad inuti spelarklassen. Ett nybörjarfel är att skapa en ny sprite klass som ärver den vanliga spriteklassen, men det är oändligt mycket enklare att bygga ut funktioner i en egen klass. Tänk dig att klassen är spelpjäsen, en orch i sagan om ringen t.ex. Spriten är bara den fysiska bilden som visas upp på spelplanen. All beräkning på snabbhet, hur mycket skada den gör, vika vapen den har osv. görs inuti klassen. Inte i spriten.
//Klassdefinition class spelare { public: //Konstruktordeklaration spelare (int hastighet, char ras, double spelare_x, double spelare_y);//startvärden //Destruktion ~spelare(){}; 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 char ras; sf::Sprite sprite; //Lägg till en sprite //Funktionerna void spelare::ljud(char vem); };
I början av spelet måste vi initiera bilderna (vilket även går att göra inuti klassen).
sf::Image kobild; sf::Image pojkbild; kobild.LoadFromFile("cow.png"); pojkbild.LoadFromFile("boy.png"); //Skapar två bildhållare som fylls med bilderna på ko och pojke
När vi sedan skapar en ko skriver vi:
spelare ko (50, 'c', 100.f, 400.f); ko.ljud(ko.ras); //Skriver ut MUUUUUUU! I det svarta konsollfönstret. ko.sprite.SetImage(kobild); //bild ko.sprite.SetPosition(ko.spelare_x,ko.spelare_y); //placering på spelplanen
Är det en ko som skapats som pekare får vi skriva nästan likadant:
spelare *ko0 = new spelare(50, 'c', 100.f, 400.f); ko0->ljud(ko0->ras); ko0->sprite.SetImage(kobild); ko0->sprite.SetPosition(ko0->spelare_x+100,ko0->spelare_y+100);
När de sedan skall ritas ut får vi skriva:
App.Draw(ko.sprite); //Direktkopierad App.Draw(ko0->sprite); //Skapad med pekare
Sedan är det bara att fortsätta utifrån den kod som finns i sektion 1. Vi flyttar pojken med:
if (App.GetInput().IsKeyDown(sf::Key::Left)) pojke.sprite.Move(-pojke.hastighet * ElapsedTime, 0); if (App.GetInput().IsKeyDown(sf::Key::Right)) pojke.sprite.Move( pojke.hastighet * ElapsedTime, 0); if (App.GetInput().IsKeyDown(sf::Key::Up)) pojke.sprite.Move(0, -pojke.hastighet * ElapsedTime); if (App.GetInput().IsKeyDown(sf::Key::Down)) pojke.sprite.Move(0, pojke.hastighet * ElapsedTime);
Korna är det en helt annan situation med. Eftersom det kan bli många kor på spelplanen måste de ha en egen funktion. Spelare 1 är pojken, spelare 2 en ko, *kospelare är en ko, Elapsed time beror på hur snabbt datorn visar bilderna och kons hastighet finns inuti spelarklassen.
void kojakt(double spelare1_x, double spelare1_y, double spelare2_x, double spelare2_y, sf::Sprite *kospelare, float ElapsedTime, int kofart ) {//spelare 1 är pojken spelare 2 en ko if (spelare2_x <= spelare1_x) //pojken har sprungit förbi kon kospelare->Move(kofart * ElapsedTime, 0); if (spelare2_x > spelare1_x) //pojken har inte sprungit förbi kon kospelare->Move( -kofart * ElapsedTime, 0); if (spelare2_y > spelare1_y ) //kon är nedanför pojken kospelare->Move(0, -kofart * ElapsedTime); if (spelare2_y <= spelare1_y ) //kon är ovanför pojken kospelare->Move(0, kofart * ElapsedTime); } //Slut på funktionen
Det krångliga kommer när vi skall välja ko, funktionen fungerar nämligen olika för kopierade eller nyskapade med pekare:
kopierad:
kojakt(pojke.sprite.GetPosition().x, pojke.sprite.GetPosition().y, ko.sprite.GetPosition().x, ko.sprite.GetPosition().y, &ko.sprite, ElapsedTime, ko.hastighet);
Skapad med pekare:
kojakt(pojke.sprite.GetPosition().x, pojke.sprite.GetPosition().y, ko0->sprite.GetPosition().x, ko0->sprite.GetPosition().y, &ko0->sprite, ElapsedTime, ko0->hastighet);
Bara man håller reda på hur de skapas är det egentligen inget problem, bara du gör likadant varje gång.
Komplett kod:
[redigera]#include <iostream> #include <SFML\System.hpp> #include <SFML\Graphics.hpp> #include <SFML\Window.hpp> using namespace std; //Klassdefinition class spelare { public: //Konstruktordeklaration spelare (int hastighet, char ras, double spelare_x, double spelare_y);//startvärden //Destruktion ~spelare(){}; 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 char ras; sf::Sprite sprite; //Funktion //Deklareras här men beskrivs utanför klassen //Se längre ner void spelare::ljud(char vem); }; //Konstruktionsdeklaration spelare::spelare (int ut_hastighet, char ut_ras, double ut_spelare_x, double ut_spelare_y) { hastighet=ut_hastighet; ras=ut_ras; spelare_x=ut_spelare_x; spelare_y=ut_spelare_y; //Skriv ut i konsollfönstret när en spelare skapats std::cout << "En spelare har fötts!" << endl; } //funktioner void spelare::ljud(char vem) {if (vem =='b') //pojke std::cout << "HJÄÄÄÄLP" << endl; if (vem == 'c') //ko std::cout << "MUUUUUUUU!" << endl; } void kojakt(double spelare1_x, double spelare1_y, double spelare2_x, double spelare2_y, sf::Sprite *kospelare, float ElapsedTime, int kofart ); int main() { //Början av programkörningen sf::RenderWindow App(sf::VideoMode(800, 600, 32), "Test - klasser"); float ElapsedTime = 0.0f; //Skapar en konstant för att hålla hastigheten likvärdig på olika datorer //Bildernas namn, ingen fil ännu sf::Image kobild; sf::Image pojkbild; //Ladda in bilderna kobild.LoadFromFile("cow.png"); pojkbild.LoadFromFile("boy.png"); //Gör kons bakgrundsfärg genomskinlig kobild.CreateMaskFromColor(sf::Color(192,248,0)); //Skapa en ko spelare ko (50, 'c', 100.f, 400.f); ko.ljud(ko.ras); //Skriver ut MUUUUUUU! I det svarta konsollfönstret. ko.sprite.SetImage(kobild); ko.sprite.SetPosition(ko.spelare_x,ko.spelare_y); spelare pojke(100, 'b', 100.f, 100.f); pojke.ljud('b'); //Skriver ut HJÄÄÄÄÄÄLP! I det svarta konsollfönstret. pojke.sprite.SetImage(pojkbild); pojke.sprite.SetPosition(pojke.spelare_x,pojke.spelare_y); spelare *ko0 = new spelare(50, 'c', 100.f, 400.f); (*ko0).ljud((*ko0).ras); ko0->ljud(ko0->ras); ko0->sprite.SetImage(kobild); ko0->sprite.SetPosition(ko0->spelare_x+100,ko0->spelare_y+100); while(App.IsOpened()) { //Spelloop sf::Event Event; ElapsedTime=App.GetFrameTime(); //Skapar en konstant för att hålla hastigheten likvärdig på olika datorer while (App.GetEvent(Event)) // Ta hand om händelser { //while 2 if (Event.Type == sf::Event::Closed) //kryssat på [x] symbolen? stäng programmet App.Close(); if (Event.Type == sf::Event::KeyPressed) // En tangent har tryckts ner { //if 1 if (Event.Key.Code == sf::Key::Escape) // ESC tangenten = stäng programmet App.Close(); } //slut if 1 } //slut, while 2 //Flytta pojken if (App.GetInput().IsKeyDown(sf::Key::Left)) pojke.sprite.Move(-pojke.hastighet * ElapsedTime, 0); if (App.GetInput().IsKeyDown(sf::Key::Right)) pojke.sprite.Move( pojke.hastighet * ElapsedTime, 0); if (App.GetInput().IsKeyDown(sf::Key::Up)) pojke.sprite.Move(0, -pojke.hastighet * ElapsedTime); if (App.GetInput().IsKeyDown(sf::Key::Down)) pojke.sprite.Move(0, pojke.hastighet * ElapsedTime); //Flytta korna kojakt(pojke.sprite.GetPosition().x, pojke.sprite.GetPosition().y, ko.sprite.GetPosition().x, ko.sprite.GetPosition().y, &ko.sprite, ElapsedTime, ko.hastighet); kojakt(pojke.sprite.GetPosition().x, pojke.sprite.GetPosition().y, ko0->sprite.GetPosition().x, ko0->sprite.GetPosition().y, &ko0->sprite, ElapsedTime, ko0->hastighet); // Rensa skärmen App.Clear(sf::Color(0, 255, 0)); //rensa allt i fönstret och ersätt med grönfärg //updatera animation App.Draw(pojke.sprite); App.Draw(ko.sprite); App.Draw(ko0->sprite); //Visa upp allt du gjort på skärmen App.Display(); } //Slut på spelloop return 0; } //slut på programkörningen //---------------------------------------------------------- // Enkel funktion för att få fienden (kon) att jaga pojken //---------------------------------------------------------- void kojakt(double spelare1_x, double spelare1_y, double spelare2_x, double spelare2_y, sf::Sprite *kospelare, float ElapsedTime, int kofart ) {//Funktionsstart, spelare 1 är pojken spelare 2 en ko if (spelare2_x <= spelare1_x) //pojken har sprungit förbi kon kospelare->Move(kofart * ElapsedTime, 0); if (spelare2_x > spelare1_x) //pojken har inte sprungit förbi kon kospelare->Move( -kofart * ElapsedTime, 0); if (spelare2_y > spelare1_y ) //kon är nedanför pojken kospelare->Move(0, -kofart * ElapsedTime); if (spelare2_y <= spelare1_y ) //kon är ovanför pojken kospelare->Move(0, kofart * ElapsedTime); } //Slut på funktionen
Klass i funktioner
[redigera]Nu har du sett hur man gör när en klass har en inbyggd funktion:
void spelare::ljud(char vem);
Men hur gör du om du har en funktion där du t.ex. vill komma åt spriten som finns lagrad inuti klassen? Det finns ett "generellt" kapitel i den här wikibooken om hur man anger klasser (class) som variabler i en funktion. Jag har gjort det så enkelt som möjligt så kika på det kapitlet först innan du "knäcker koden" här nedanför.
Programmera spel i C++ för nybörjare/Klass som funktionsvariabel
Varför skall man ha funktioner utanför klasserna? Skall man inte sträva efter att ha alla funktioner inuti klasserna i OOP? Jo, men om man vill jämföra två spelpjäser som befinner sig i två olika klasser eller klasskopior är det enklast att ha en funktion som läser in bägge klasserna samtidigt och gör en jämförelse. Ett typiskt exempel för den här typen av jämförelser är när man vill se om två sprites kolliderar med varandra. Om kon stångar pojken, t.ex.
Anta att du har en funktion där du vill se var på spelplanen din sprite befinner sig. SFML koden för att hitta en sprites x och y värden är:
Sprite.GetPosition().x; Sprite.GetPosition().y; ("Sprite" är spritens namn och kan vara vad som helst egentligen)
Vill vi ha den i en funktion skriver vi t.ex:
void positionsbeskrivning(class spelare &figur); { Std::cout<< ” Rasen är= ” << figur.ras <<std::endl; Std::cout<< ” X är= ” << figur.sprite.GetPosition().x <<std::endl; Std::cout<< ” Y är= ” << figur.sprite.GetPosition().y <<std::endl; }
För att sedan kunna använda funktionen fyller du i, någonstans i spelloopen:
positionsbeskrivning(pojke); positionsbeskrivning(ko);
I detalj
[redigera]- Vi säger i deklarationen för funktionen att det är en class vi skall använda.
- Vi kallar den för figur, namnet kunde ha varit precis vad som helst, det används enbart inom funktionen.
- Vi måste komma ihåg vad vi kallade spriten i klassdeklarationen. Just här skrev vi att klassens sprite heter sprite (sf::Sprite sprite;) men den kan heta nästan vad som helst. Att skriva fel namn på spriten är ett typiskt nybörjarfel som kan vara klurigt att upptäcka.
- Vi använder oss av klassens minnesadress, det är därför det är ett "&"-tecken framför figur.
- Observera att vi använder "figur.ras" för att få fram ett värde som inte ingår i spriten men som ingår i klassen.
Variant med pekare
[redigera]Nu finns det programmerare som blir överlyckliga när de får använda pekare, så naturligtvis kan en likadan funktion skrivas för användning av pekare:
void positionsbeskrivning(class spelare *figur); { Std::cout<< ” Rasen är= ” << figur->ras <<std::endl; Std::cout<< ” X är= ” << figur->sprite.GetPosition().x <<std::endl; Std::cout<< ” Y är= ” << figur->sprite.GetPosition().y <<std::endl; }
Om man då vill använda sig av funktionen måste vi mata den med pekarens adress istället:
positionsbeskrivning(&pojke); positionsbeskrivning(&ko);
Slutresultatet kommer att bli detsamma.
OBS utan class-ordet i deklarationen
[redigera]Det är fullt möjligt att utelämna "class" i funktionsdeklarationen, t.ex.:
void positionsbeskrivning(spelare figur);
Funktionen fungerar oftast lika bra, men inte alltid. Ta för vana att ha med "class"-ordet, om inte annat så för att skapa mer lättläst kod.
Variant utan adress
[redigera]Om man är extremt slarvig kan man faktiskt skriva:
void positionsbeskrivning(spelare figur) { std::cout<< " Rasen är= " << figur.ras <<std::endl; std::cout<< " X är= " << figur.sprite.GetPosition().x <<std::endl; std::cout<< " Y är= " << figur.sprite.GetPosition().y <<std::endl; }
och sedan anropa funktionen med:
positionsbeskrivning(pojke); positionsbeskrivning(ko);
Jag vet ärligt talat inte varför det fungerar, det borde inte göra det. Men i alla fall i VC++ 2010 express fungerar det. Gör inte så här, även om det verkar riktigt enkelt.
struct istället för class i funktionen?
[redigera]Om du tittar på andra programmeringsexempel på Internet kommer du att hitta liknande exempel men med struct istället för class. Det är ingen större skillnad mellan dem och det går precis lika bra att skicka in en struct i funktionen, bara man kommer ihåg att byta etikett på den så att det t.ex. står:
void positionsbeskrivning(struct spelare &figur);
Den egentliga orsaken till att class inte är lika vanlig i äldre kodexempel är att struct var vanligast i programmeringsspråket C.