Hoppa till innehållet

Programmera spel i C++ för nybörjare/Animationsteori

Från Wikibooks


Animationsteori

[redigera]

Denna text gäller inte bara för SFML utan för spelprogrammering i allmänhet.

Rörelser och animationer

[redigera]

Du måste skilja på rörelse och animation. Det ena utesluter inte det andra, men de skall behandlas på två olika sätt. Rörelse är enkelt i spelvärlden. En rörelse är när något rör på sig. Du kan t.ex. ha en bild på en tennisboll som rör sig när den träffas av ett racket som rör sig upp och ner i spelet. Rörelsen är inte en animation.

En animation är när en spelpjäs/sprite byter bild med ett fast tidsintervall. Vanligtvis 60 gånger i sekunden. I exemplet med boll och racket har racketen en enda bild och bollen har också en enda bild. En sprite med en animation kan däremot stå helt stilla samtidigt som animationen pågår. Tänk dig ett hus där rök kommer ur en skorsten. Huset står stilla, det gör faktiskt ingenting alls mer än att det har rök som bolmar upp som är animerad.

Vanligtvis har man en kombination av rörelse och animation eftersom vi människor helt enkelt tycker att det är roligare att se något som är animerat. Det går dock att göra spel helt utan animationer med bara rörelser, vilket är enklast. Tänk dig t.ex. att du gör ett racerbil spel där man ser bilarna uppifrån. Då behöver inte bilarna vara animerade, de kan röra sig ändå.

Enklast möjliga animation i SFML

[redigera]

Enklast möjliga animation, två bildrutor som laddas in från varsin bildfil som alternerar, med kod från SFML. I det här enklaste fallet används inga sprite sheets:

sf::Clock Clock; //En spelklocka för att hålla reda på just den här animationen
                //Du måste ha en klocka för varje sprite med animation i spelet

sf::Image player1;//Hjälte bild 1 i animationen - storlek 32x32
sf::Image player2;//Hjälte bild 2 i animationen

sf::Sprite sprite; //Spriten med vår hjälte

player1.LoadFromFile("bildfil1.png"); //Bild ett i animationen
player2.LoadFromFile("bildfil2.png"); //Bild två i animationen

if ((Clock.GetElapsedTime() > 0.0) && (Clock.GetElapsedTime() < 0.1)) sprite.SetImage(player1);
if ((Clock.GetElapsedTime() > 0.1) && (Clock.GetElapsedTime() < 0.2)) sprite.SetImage(player2);
if (Clock.GetElapsedTime() > 0.2) Clock.Reset();
// Om den tid som gått är större än 0 och mindre än 0.1 sekund visas bild 1.
// Om den tid som gått är större än 0.1 och mindre än 0.2 sekunder visas bild 2.
// Loopen skapas genom att återgå till bild 1 om tiden som passerat är längre än 0.2 sekunder.

Sprite sheet

[redigera]

Visserligen kan man skapa en bild för varje bildruta i animeringen och ladda in alla dessa hundratals bilder i datorns minne vid programstart, men det är ett slöseri på resurser och spelarens tid och lämpar sig bara för extremt enkla animeringar. Ett exempel är spelet ”Pac man” där huvudfigurens mun bara består av två bilder (öppen och stängd) och spökenas ögon består av två bilder (sneglar vänster och sneglar höger). Istället brukar man använda något som kallas ”sprite sheet”. Det är en enda stor bildkarta som innehåller småbilder som representerar alla olika animationsbilder som man kan behöva, beroende på spel. Vanligtvis har alla bilder exakt samma storlek, och du kan göra det till en vana själv att göra så om du skapar egna sprite sheets. Då räcker det med att du vet var vänster överhörn finns på den bild du vill ha på sprite sheeten för att matematiskt kunna räkna ut var de andra tre hörnen finns. Om bilderna däremot har olika storlek måste du alltid ange samtliga fyra hörns positioner vilket blir omständligt i onödan.

En annan god vana är att alltid ha en rörelse på en bildrad. ”Gå uppåt” = alla bilderna på en rad, liksom ”Skjut vänster” = alla bilderna på en rad. Om du låter bilderna till animationen gå över mer än en rad blir det genast mer komplicerat att beräkna. Ibland måste man göra så ändå, en explosion blir t.ex. väldigt futtig om man bara har ett fåtal bilder som visar upp den.


Anta att du har en "sprite sheet", dvs. en karta med bilder där man visar flera bilder från samma karta. Man måste fortfarande ha en sprite, en klocka och en bild, men nu är det en hel bildkarta. Anta att du har fyra bilder i rad på en spriteshet där alla bilder har 32 i bildhöjd och hela sheetens bredd är 128, dvs. 32 breda. Vill man då att den första bilden skall visas skriver man:

sprite.SetSubRect(sf::IntRect(0,0,32,32)); 

Vad är siffrorna i parentesen? Det två första siffrorna = x,y koordinaterna för övre vänstra hörnet (0,0 här i exemplet) och de två andra siffrorna är koordinater för det nedre högra hörnet i den bildruta du vill ge spriten (32,32 här i exemplet).

sf::Clock Clock; //En spelklocka för att hålla reda på just den här animationen
                //Du måste ha en klocka för varje sprite med animation i spelet

sf::Image player1;//Hjälte i animationen, bildhållare
player1.LoadFromFile("bildfil1.png"); //Bildfil med många bilder på samma: sprite sheet
                                     // - storlek =  4 bilder i rad 32x32
sf::Sprite sprite; //Spriten med vår hjälte
sprite.SetImage(player1); // Ge hela bildkartan som bild till spriten

if ((Clock.GetElapsedTime() > 0.0) && (Clock.GetElapsedTime() < 0.1)) 
sprite.SetSubRect(sf::IntRect(0,0,32,32)); //Visa  bild 1 i serien

if ((Clock.GetElapsedTime() > 0.1) && (Clock.GetElapsedTime() < 0.2)) 
sprite.SetSubRect(sf::IntRect(32,0,64,32)); //Visa  bild 2 i serien

if ((Clock.GetElapsedTime() > 0.2) && (Clock.GetElapsedTime() < 0.3)) 
sprite.SetSubRect(sf::IntRect(64,0,96,32)); //Visa  bild 3 i serien

if ((Clock.GetElapsedTime() > 0.3) && (Clock.GetElapsedTime() < 0.4)) 
sprite.SetSubRect(sf::IntRect(96,0,128,32)); //Visa  bild 4 i serien

if (Clock.GetElapsedTime() > 0.4) Clock.Reset();
//Mer än så hamnar klockan på 0 igen.

Funktion

[redigera]

Att göra så här för varje animerad sak i hela spelet blir naturligtvis väldigt stökigt. Det är bättre att ha en funktion som tar utt x- och y- koordinaterna i animationen. Det räcker med att du vet det övre vänstra hörnets koordinater om bilderna är kvadratiska. Den kan se ut så här:

int vaeljXellerY(float starttid, float tidnu, int bildstorlek, int XellerY)
{ 
float animationshastighet = 0.5f; //Hur snabbt skall bilderna växlas?
float elapsedtime = tidnu - starttid; //Hur lång tid har gått från animationsstarten?

if ((elapsedtime  > 0.0) && (elapsedtime  < animationshastighet )) 
{ //bild 1, koordinater för övre vänstra hörnet
x = 0;
y = 0;
}

if ((elapsedtime > animationshastighet ) && (elapsedtime   < animationshastighet * 2)) 
{ //bild 2, koordinater för övre vänstra hörnet en bild in
x = bildstorlek;
y = 0;
}

if ((elapsedtime  > animationshastighet * 2) && (elapsedtime < animationshastighet * 3)) 
{ //bild 3, koordinater för övre vänstra hörnet två bilder in
x = bildstorlek * 2;
y = 0;
}

if ((elapsedtime  > animationshastighet * 3) && (elapsedtime < animationshastighet * 4)) 
{ //bild 4, koordinater för övre vänstra hörnet tre bilder in
x = bildstorlek * 3;
y = 0;
}

if (XellerY == 1)
//om en är 1 får vi tillbaka x koordinaten, annars får vi y koordinaten
return x;
else
return y;

}

Om vi vill få fram en speciell bild i spritesheeten så använder vi koden:

int x = 0;
int y = 0;
x = vaeljXellerY(starttid, tidnu, 32, 1);
y = vaeljXellerY(starttid, tidnu, 32, 2);
sprite.SetSubRect(sf::IntRect(x,y,x+32,y+32)); //Visa  bild i serien

Bool och enum

[redigera]

Vi måste tala om för spelet att vi bara visar en enda sorts animation. För att få det att fungera kan man använda sig av sk. ”Enumerated states” eller boolska värden som bara kan vara sant eller falskt. T.ex. initieringskoden här nedanför:

bool left, right, up, down; //Fyra olika riktningar att gå
left = right = up = down = false; //Avgör om animationen skall visas eller inte 

Vi hade lika gärna kunnat skriva:

bool Walk_Left, Walk_Right, Walk_Up, Walk_Down //Fyra olika riktningar att gå

Om man har en animation för att gå som piltangenterna på tangentbordet är det här ett enkelt men effektivt sätt att inte köra två olika animationer samtidigt. När man t.ex. vill att animationen skall gå uppåt anger man:

up = true; //Figurens animation för att gå uppåt kan visas 
left = right = down = false; //De andra riktningarna blir falska

Poängen är dock att du måste ha en ”state” för varje sorts animation du vill visa. Om du t.ex. har en spelare som är en japansk ninja måste du dessutom ha: hopp, stöta med svärd, svinga svärd, slå med knuten näve, slå med öppen näve, framåtspark, sidospark osv.

Ett annat sätt är att redan i förväg definiera vilken ”state” man kan ha spelaren i:

enum player_state { 
       standstill,
       dead,
       walk_up,
       walk_right, 
       walk_down, 
       walk_left,
       hiding, 
       shoot_up
       shoot_right, 
       shoot_down, 
       shoot_left,
};

Egentligen är dessa ”states” samma som en lista med siffror som börjar på 0 och det är fullt möjligt att både göra beräkningar och jämförelser där man antar att de är int/heltal istället. Genom att ange olika ”states” kan man också ange spelarens hastighet beroende på vilken ”state” den har. Man kan t.ex. anta att om spelaren stillastående eller död är farten 0. Antagligen om den skjuter också. Om den går har den normal fart och om den gömmer sig har den halv fart.

Mardrömmen

[redigera]

Sådana här states kan lätt bli en mardröm för den som gör grafiken. Ta bara exemplet skjuta. Man kan gå och skjuta, stå och skjuta, springa och skjuta, hoppa och skjuta, stå på knä och skjuta, ligga och skjuta. Samtliga dessa states skall också ha fyra olika riktningar med säg fem bilder i varje i varje animation. 5*4=20 bilder per state och 6 states =120 bilder, för en enda figur i spelet. Det finns en anledning till att man i spelet Quake2 hoppade över animationen springa+skjuta och alla som sprang och sköt rörde sig som om de stod på skateboards i full fart när de sköt.

Bildrutor

[redigera]

Om du vet vilka ”states” en figur kan ha kan du också definiera vilka rutor som skall visas i ditt ”sprite sheet”. Är det ”standstill”/stillastående kanske det är en enda ruta, eller två rutor där kroppen är stilla men huvudet rör sig fram och tillbaka. Ibland skall den ”loopa”, dvs. att samma sekvens av bildrutor visas om och om igen. Det är vanligt när en figur springer eller går. I andra sammanhang skall animationen bara visas en enda gång, t.ex. om det är en explosion eller om figuren skjuter ett enda skott med ett vapen.

X,Y koordinater

[redigera]

När man skall lista ut vilken bildruta du skall ha räcker det som sagt med att du vet koordinaten för det övre vänstra hörnet, om bildstorleken är känd. Anta att du har fem bildrutor i rad som visar en animation och alla bilder är 32x32 pixels stora. Då vet du att Y hela tiden är 0 eftersom bildens överkant = 0. Du vet också att X koordinaterna blir:

  • 0 för första bilden
  • 32 för andra bilden
  • 64 för tredje bilden
  • 96 för fjärde bilden
  • 128 för femte bilden.

Om du vill visa bilderna i en annan ordning får du ha t.ex. ha en array med integers och mata den med {1,3,2,5,4} om du skall visa animationen i den ordningen. Då blir X-koordinaterna 0*32, 2*32, 1*32, 4*32 och 3*32 eftersom man börjar att räkna med 0. Att ha alla bilder lika stora och i en enda rad för en animation underlättar ditt liv som programmerare en hel del, främst för att du kan ta ut X-koordinatens värde ur en enkel funktion (du kommer väl ihåg att funktioner bara kan returnera ett enda värde) och Y-koordinaten inte ändras.

Animation med många bildrader

[redigera]

Om du har mer än en bildrad i ditt spritesheet måste du ha någon form av kontroll när man kommit till radslutet. Anta att du har en animation på två rader, då får du tänka: "När X== 128 blir Y=32 och X återställs till 0 igen" i pseudokod.

Loop

[redigera]

En animation baserad på tid har en starttid, från den ser man hur lång tid som gått och visar upp rätt bild i animationen utifrån den tid som passerat. Anta att man visar tio bilder i sekunden. Då skulle samtliga bilder ur exemplet ovanför visas färdigt på en sekund. Men hur gör man om man vill visa samma animation en gång till? Då resettar man starttiden till 0. Det är inte svårare än så.

Kodexempel med enkel animation

[redigera]
  • Du måste ha en bild på en ko som heter cow.png
  • Du måste ha två bilder som heter hero.png och hero2.png som skall alterneras för att skapa en enkel animation.
  • Om du kommenterar bort klockan, sätt kod mellan /* och */, kan du pröva hero och hero2 animationerna med tangenterna A och S.
#include <iostream>
#include <SFML\System.hpp>
#include <SFML\Graphics.hpp>
#include <SFML\Window.hpp>

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

    float  SpriteX=0.0, ko1X = 0.0; //X positioner för pojke och ko
    float  SpriteY=0.0, ko1Y = 0.0; //Y positioner för pojke och ko

   sf::RenderWindow App(sf::VideoMode(800, 600, 32), "SFML grafiktest"); 
  // Skapa fönstret vi skall visa färgerna i
 
   float ElapsedTime = App.GetFrameTime(); //Skapar en konstant för att hålla hastigheten 
   likvärdig på olika datorer

    sf::Clock Clock; //En spelklocka för att hålla reda på just den här animationen
               //Du måste ha en klocka för varje sprite med animation i spelet

    sf::Image player1;//Hjälte bild 1 i animationen - storlek 32x32
    sf::Image player2;//Hjälte bild 2 i animationen

    sf::Sprite sprite; //Spriten med vår hjälte

 if (!player1.LoadFromFile("hero.png")) return EXIT_FAILURE; 
    //fyll den tomma bildhållaren med bilden hero.png
 if (!player2.LoadFromFile("hero2.png")) return EXIT_FAILURE; 
    //fyll den tomma bildhållaren med bilden hero2.png

//bilden är  en 32x32 .png jag hittade ute på Internet med en googlesökning 
        sf::Sprite Sprite(player1); //Skapar den grafiska spelpjäsen Sprite med grafiken från Image
        Sprite.SetPosition(200.f, 100.f); //Placera ut bilden

          
        /*Skapa en ko--*/

        sf::Image kobild; //skapa en tom bildhållare som heter Image
        if (!kobild.LoadFromFile("cow.png")) return EXIT_FAILURE; 
            //fyll den tomma bildhållaren med bilden cow.png
        sf::Sprite ko1(kobild); //Skapar den grafiska spelpjäsen Sprite med grafiken från Image
        ko1.SetPosition(400.f, 100.f); //Placera ut bilden på kon 
             //bilden är  en 32x32 .png jag hittade ute på   
             //  Internet med en googlesökning
       /* slut på skapa ko */

        while (App.IsOpened())  // Start spel-loopen
      {  //while 1
         sf::Event Event; 

            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(); 

                //Testa bild genom att trycka ner en tangent
                if (Event.Key.Code == sf::Key::A) // Visa bild 1
		 Sprite.SetImage(player1);

		 if (Event.Key.Code == sf::Key::S) //Visa bild 2
		 Sprite.SetImage(player2);
                } //slut if 1

              } //slut, while 2

      ElapsedTime = App.GetFrameTime(); //för att få en konstant hastighet på olika datorer

     if (App.GetInput().IsKeyDown(sf::Key::Left)) 
     Sprite.Move(-100 * ElapsedTime, 0); //pojke 
        //Om man trycker ner vänster piltangent går figuren 100 "steg" 
        // längre åt vänster än vad den var tidigare.
               
     if (App.GetInput().IsKeyDown(sf::Key::Right)) 
     Sprite.Move( 100 * ElapsedTime, 0); 
                                          
     if (App.GetInput().IsKeyDown(sf::Key::Up)) 
     Sprite.Move(0, -100 * ElapsedTime); 
                                         
     if (App.GetInput().IsKeyDown(sf::Key::Down)) 
     Sprite.Move(0, 100 * ElapsedTime); 
                                           
//Räkna ut hur kon jagar hjälten
  SpriteX= Sprite.GetPosition().x; // Pojkens xposition, kan användas som AI
  ko1X= ko1.GetPosition().x; // kons xposition, kan användas som AI 

  SpriteY= Sprite.GetPosition().y; // Pojkens yposition, kan användas som AI
  ko1Y= ko1.GetPosition().y; // kons yposition, kan användas som AI

  if (ko1X <= SpriteX) //pojken har sprungit förbi kon
  ko1.Move( 50 * ElapsedTime, 0); 

  if (ko1X > SpriteX) //pojken har inte sprungit förbi kon
  ko1.Move( -50 * ElapsedTime, 0); 

  if (ko1Y > SpriteY ) //kon är nedanför pojken
  ko1.Move(0, -50 * ElapsedTime);

  if (ko1Y <= SpriteY ) //kon är ovanför pojken
  ko1.Move(0, 50 * ElapsedTime); 

 //Ta fram rät bildruta av två möjliga
if ((Clock.GetElapsedTime() > 0.0) && (Clock.GetElapsedTime() < 0.1)) Sprite.SetImage(player1);
if ((Clock.GetElapsedTime() > 0.1) && (Clock.GetElapsedTime() < 0.2)) Sprite.SetImage(player2);
if (Clock.GetElapsedTime() > 0.2) Clock.Reset();
   //Måste kommenteras bort om man skall testa bildrutorna

App.Clear(sf::Color(0, 255, 0)); //rensa allt i fönstret och ersätt med grönfärg
App.Draw(Sprite); //Rita upp figuren på den yta spelaren ser
App.Draw(ko1); //Rita upp kon på den yta spelaren ser
App.Display(); //visa upp ändringarna för användaren

} //slut, while 1

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