Programmera spel i C++ för nybörjare/Sprites och spelpjäser3
(Genomgången förutsätter att du har en fungerande installation av Microsoft Visual C++ 2010 Express och SFML 1.6 på din dator.)
Använda klasser
redigeraDå har vi en sprite som heter boy och en sprite som heter cow. Om vi har ett enkelt spel med få spelpjäser, som det här som bara har två, räcker det. Anta att vi skall göra spelet ”Risk” istället. Det där spelet där man skall erövra världen och fyra olika spelare har hundratals spelpjäser. Om man skulle programmera Risk på samma sätt skulle samtliga pjäser för alla spelarna vara fördefinierade innan spelet börjar. Visst, man kan göra så, men det är onödigt omständligt.
En annan väg att gå vore om man hade en ”ritning” över hur en spelare skall se ut och varje gång en ny skall ut på spelplanen skapas en spelpjäs från den ritningen i datorns minne för att sedan placeras ut på spelplanen. När spelpjäsen ”dör” raderas den och försvinner ut från spelet så att det minnet kan återställas. Låter inte det så mycket bättre?
C++ stora styrka är just att det är ”Objekt Orienterat”, dvs. variablerna skall helst ligga inom de olika objekten och inte i själva main-loopen. På det sättet innehåller varje objekt sina egna värden. Just när man skapar spel är detta ovärderligt och den absolut största styrkan jämfört med andra programmeringsspråk som inte har den här möjligheten.
Om du lär dig hur man skapar klasser i spel kan du skapa klasser i alla andra program och du har kommit ett stooooooooooort steg på vägen till att bli professionell C++ programmerare, det kommer du däremot aldrig att bli om du inte förstår dig på klasser. Är klasser något för nybörjare? I vilket annat programspråk som helst skulle jag säga nej eftersom klasser är så svåra att förstå sig på, men just i C++ är det fundamentalt, det som hela programspråket egentligen vilar på, och just därför är du tvungen att lära dig. Att det blir så oändligt mycket enklare att programmera spel med klasser, än utan, gör inte saken sämre.
Så, vi har en pojke och en ko, dessa kan man tänka har en gemensam ritning som vi kan kalla spelare. Det som är gemensamt för både pojken och kon är att de har en hastighet och en position baserad på ett x,y värde. Den skall dessutom få en ras (b eller c för boy eller cow). Vi behöver alltså ge varje figur fyra värden som alltid är gemensamma.
Int hastighet; //Hur snabb är den double x; // var är den i sidled i programmet double y; //var är den i höjdled i programmet. char ras; //kan vara b eller c
Dessutom kan vi ha med en funktion som säger något om spelaren.
Void ljud(char vem) {if (vem ==b) cout << ”HJÄÄÄÄLP” << endl; if (vem == c) cout << ”MUUUUUUUU!” << endl; }
Skapa en class
redigeraEn klass för en spelpjäs definieras på ett sätt som är likadant för alla klasser i C++,
class klassnamn { private: variabler public: variabler funktionsddefinitioner }; //obs semikolon här //funktioner klassnamn::funktionsdefinition() { funktion som används av klassen }
Det är väl ganska enkelt att förstå, men det finns två saker som är konstiga. Dels orden private och public.
Private eller public
redigeraEnkelt sagt är det så att det som står under public kan man komma åt utifrån i programkoden, men det som står under private kan bara förändras av funktioner inom klassen. Som standard är allting i en klass private, så om du inte skriver någonting alls är allting i klassen private. En del programmerare skriver bara pulic: och sedan det som är öppet för alla och låter resten vara utan etikett, det är ju ändå private som standard.
Om du är den enda programmeraren kan du säkert hålla reda på allting i klassen, så du skulle kunna kalla allt public istället bara för att underlätta din egen programmering. Om man däremot är en grupp programmerare vill man ha vissa variabler som man inte kan förändra hur som helst och då är just att man kan skydda dem med private den stora räddaren.
Funktioner utanför klassen
redigeraman kan skriva funktionerna inuti klasserna, men skrivs de utanför går det lättare att felsöka klasserna om det blir någonting galet. Enklast är att skriva funktionsdeklarationen inuti klassen och sedan skriva funktionen utanför med koden:
klassnamn::funktionsnamn(variabler) { gör något här }
Ibland står det
Inline klassnamn::funktionsnamn(variabler) { gör något här }
för att visa att funktionen egentligen tillhör en klass. Varför dubbla kolon ::? Det visar att vi skall köra funktionen inuti klassen. Vi kan göra en jämförelse, kommer du ihåg att man kan skriva:
using namespase std cout << ”hej” << endl;
eller
std::cout <<”hej”<<endl;
i det sista exemplet säger vi att funktionen cout som finns i klassbiblioteket std skall användas.
Klassen spelare
redigera//Klassdefinition class spelare { public: 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; void spelare::ljud(char vem); }; //obs semikolon här //funktioner void spelare::ljud(char vem) {if (vem =='b') std::cout << "HJÄÄÄÄLP" << endl; if (vem == 'c') std::cout << "MUUUUUUUU!" << endl; }
Vi blir tvungna att skriva std::cout eftersom det annars kan uppstå svårigheter då funktioner inte läser av using namespace std och vi behöver cout funktionen.
Char och namn
redigeraI exemplet använder jag mig av en enda bokstav för att visa vilken ras det är. b för boy/pojke och c för cow/ko. Många unga spelprogrammerare vill kunna spara text i variablerna. Skapar man en ko kanske man vill att den skall ha namnet "Rosa". Det är inte överdrivet svårt att göra det. Utgå t.ex. ifrån att inget namn har mer än 20 bokstäver. Sedan fyller du i variabeln i klasssen:
char namn[20];
Problemet som uppstår blir när man vill döpa den. Skriver du
ko.namn="Rosa"; //FEEEEEEEEL!!!
kommer kompilatorn att skrika till och vägra köra koden eftersom Rosa är färre än 20 bokstäver. Då kan man endera räkna bokstäverna i de namn som läggs in och fylla ut dem med "Space" tills det blivit 20 bokstäver. Ett enklare alternativ är att använda funktionen strcpy_s t.ex. så här:
strcpy_s(ko.namn,"Rosa"); //Ge kon ett namn
Skall man arbeta med text bör man alltid fylla i includesatsen för det och skriva:
#include <string>
bland de andra include-satserna i programmets början.
Skapa spelarna
redigeraHur skapar vi någonting av den här klassen? Det är enkelt, skall vi skapa en ko skriver vi helt enkelt i koden:
//Skapa en ko spelare ko; //ko blir en kopia av klassen spelare ko.hastighet = 50; //grundhastighet, skall multipliceras med ”ElapsedTime ”. ko.spelare_x=400.f; ko.spelare_y=100.f; ko.ras='c'; ko.ljud(ko.ras); //Skriver ut MUUUUUU! I det svarta konsollfönstret.
Komma åt värdena
redigeraSom du ser är det lätt att komma åt de olika värdena i klassen, speciellt om du gjort allting public. Det är bara att ange klassnamn.värde för att komma åt det. Och är det public kan du också ändra det hur du vill utifrån. Om t.ex. hastighet hade varit satt som private hade du bara kommit åt det genom att skapa en funktion som läste av det. Enklast vore att låta kon råma ut ”Jag springer 50” istället för MUUUUUU!. Eftersom funktionen tillhör klassen kan funktionen komma åt alla värdena i klassen, även om de är private.
En så enkel funktion som ljud hade lika gärna kunnat finnas helt inuti klassen. Då hade det sett ut så här:
class spelare { public: 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; Void ljud(char vem) { if (vem ==b) std::cout << ”HJÄÄÄÄLP” << endl; if (vem == c) std::cout << ”MUUUUUUUU!” << endl; } }
För att skapa pojken skriver vi:
spelare pojke; //pojke blir en kopia av klassen spelare pojke.hastighet = 100; //grundhastighet, skall multipliceras med ”ElapsedTime ”. pojke.spelare_x=100.f; pojke.spelare_y=100.f; pojke.ras='b'; pojke.ljud(pojke.ras); //Skriver ut HJÄÄÄÄÄÄLP! I det svarta konsollfönstret.
Koden blir alltså:
#include <iostream> #include <SFML\System.hpp> #include <SFML\Graphics.hpp> #include <SFML\Window.hpp> using namespace std; //Klassdefinition class spelare { public: 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; void spelare::ljud(char vem); }; //funktioner void spelare::ljud(char vem) { if (vem =='b') std::cout << "HJÄÄÄÄLP" << endl; if (vem == 'c') std::cout << "MUUUUUUUU!" << endl; } int main() { //Början av programkörningen //Skapa fönstret som spelet skall visas upp i sf::RenderWindow App(sf::VideoMode(800, 600, 32), "Test - klasser"); //Skapa en ko----------------------------------------------------------------- spelare ko; //ko blir en kopia av klassen spelare ko.hastighet = 50; //grundhastighet, skall multipliceras med "ElapsedTime". ko.spelare_x=400.f; ko.spelare_y=100.f; ko.ras='c'; ko.ljud(ko.ras); //Skriver ut MUUUUUU! I det svarta konsollfönstret. //Skapa en pojke--------------------------------------------------------------- spelare pojke; //pojke blir en kopia av klassen spelare pojke.hastighet = 100; //grundhastighet, skall multipliceras med "ElapsedTime". pojke.spelare_x=100.f; pojke.spelare_y=100.f; pojke.ras='b'; pojke.ljud(pojke.ras); //Skriver ut HJÄÄÄÄÄÄLP! I det svarta konsollfönstret. App.Clear(); App.Display(); return 0; } //slut på programkörningen
Kör du programmet kommer det att blinka till. Det saknar programloop, men i det svarta konsollfönstret kommer du att se MUUUUU! Och HJÄÄÄÄÄLP! Så vi vet att de två figurerna (kon och pojken)verkligen har skapats.
En konstruktor
redigeraNu vet vi att det bara finns en ko och en pojke på spelplanen, men anta att vi vill skapa en ny ko varje gång vi klickar med musen på spelplanen. Då behöver vi kunna skriva in värdena för de olika variablerna i klassen redan när kospelaren skapas.
- En konstruktor har exakt samma namn som klassen.
- En konstruktor ser ut som en funktion, men saknar returvärde eller void.
Anta att vi redan när den skapas vill skriva ut att det är en ko och inte en pojke som skapas.
Sä här ser klassdefinitionen ut nu:
//Klassdefinition class spelare { public: 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; //Konstruktordeklaration spelare (char ras = 'c');//ko som standard //Funktionerna void spelare::ljud(char vem); }; //Konstruktordeklarationen spelare::spelare(char ras) { std::cout << "En spelare har fötts!" << endl; } //funktioner void spelare::ljud(char vem) { if (vem =='b') std::cout << "HJÄÄÄÄLP" << endl; if (vem == 'c') std::cout << "MUUUUUUUU!" << endl; }
När vi kör den dyker återigen spelfönstret upp och försvinner, men till varje spelare som skapas finns ytterligare en textrad med ”En spelare har fötts!”
Man behöver ingen deklaration, jag hade kunnat skriva:
spelare ();//ingenting
och längre ner
spelare::spelare() { //std::cout << "En spelare har fötts!" << endl; }
Då har jag en färdig prototyp om jag vill lägga till någonting senare. Vad är egentligen finessen? Blir det inte krångligare på det här sättet? Fördelen är att alla variabler kan deklareras redan vid skapandet. Det går lika bra att skriva:
//I funktionen spelare (int hastighet, char ras, double spelare_x, double spelare_y);//startvärden
och längre ner
//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; std::cout << "En spelare har fötts!" << endl; }
Vad innebär detta? Jo varje gång vi skapar en spelare måste vi se till så att vi förser funktionen med korrekta värden. Värdena på klassens variabler matas alltså in utifrån i samband med att en spelare skapas. Vill vi ha en pojke skriver vi in värdena för pojken när den skapas genom att ange nya konstruktionsvärden för pojken.
Istället för att skriva:
spelare pojke; //pojke blir en kopia av klassen spelare pojke.hastighet = 100; //grundhastighet, skall multipliceras med ”ElapsedTime ”. pojke.spelare_x=100.f; pojke.spelare_y=100.f; pojke.ras='b'; pojke.ljud(pojke.ras);
skriver vi
spelare pojke(100, 'b', 100.f, 100.f); pojke.ljud(pojke.ras);
För kon skriver vi istället
spelare ko (50, 'c', 100.f, 400.f); ko.ljud(ko.ras);
Båda sätten går att använda, men det blir mycket mindre att skriva om man anger värdena direkt vid skapandet. Detta är speciellt viktigt om man skapa en helt ny spelare (mer om det här Programmera spel i C++ för nybörjare/Sprites och spelpjäser 5).
OBS Tänk på att deklarationen:
spelare (int hastighet, char ras, double spelare_x, double spelare_y);//startvärden
måste ligga i den del i funktionen som är public, annars kan man inte komma åt dem utifrån.
Nu har vi sett hur man skapar en ko och en pojke under tiden programmet körs, men om man vill skapa mängder med kor? Vad gör vi då? Det är nästa kapitel.