[tutorial][C#][XNA 4.0] 4-Rysowanie cz.2: animacja i tekst oraz obsługa klawiszy



W tym temacie przedstawię wam trochę więcej informacji na temat rysowania w XNA.
Na początku wyświetlimy tekst a potem przejdziemy do wyświetlenia animacji 2D.



Aby móc wyświetlić tekst w naszej grze będziemy potrzebowali zmiennej przechowywującej naszą czcionkę:
 SpriteFont font; // nasza czcionka
oraz samej czcionki. Możemy ją szybko utworzyć w Visualu:

- klikamy prawym na nasz Content a następnie Add i New Item
 
|
\/






 - wybieramy Sprite font, i zmieniamy nazwę na font.spritefont


Teraz wystarczy że wczytamy naszą utworzoną czcionkę do zmiennej font i zainicjujemy nasz bufor grafiki:


I już możemy korzystać ze zmiennej font do wyświetlania tekstu na ekranie:


 Wygląd naszej gry po zdebugowaniu:

Dodatkowo zaznaczyłem na żółto konstruktor nowego obiektu typu Vector2, określa on położenie tekstu i oczywiście można parametry w nim podane edytować w zależności od potrzeb a nawet ustawiać w innych funkcjach jak np. Update() by później je tylko przekazać do tej instrukcji.
 
 Vector2(100, 200)

Pierwszy parametr to położenie na osi X
Drugi parametr to położenie na osi Y


----------------------------------------------------------------------------------------------------
Przy okazji pisania kodu, może być wam uciążliwe wyłączanie aplikacji poprzez konieczność ciągłego klikania w X. Dlatego nim przejdziemy do animacji postaci zaznajomię was z obsługą klawiatury:)

Żeby móc operować naszym programem poprzez klawiaturę potrzebujemy utworzyć obiekt przechowujący stan naszych klawiszy(czyli które z nich są naciśnięte a które zwolnione), umieszczamy go na początku klasy naszej gry:
KeyboardState keybStat;
Teraz chcemy żeby w każdym obiegu pętli był sprawdzany świeży stan klawiszy dlatego musimy użyć przypisania:
keybState = Keyboard.GetState();
I już możemy sprawdzać czy nie został naciśnięty klawisz Esc, a jeśli tak to zakończymy nasza aplikacje:
if (keybState.IsKeyDown(Keys.Escape)) Exit() ; 



Oczywiście w żadnym razie nie na da się takie proste wczytywanie stanu i sprawdzanie jeśli będziemy chcieli poruszać się np. o jedną pozycje w menu, albo wykonać tylko raz daną akcję dla pojedynczego wciśniętego przycisku. Dlaczego? ponieważ nasza gra wykonuje się około 60 razy na sekundę i dla każdego obiegu nasze ciągłe trzymanie palca na klawiszu to jakby klikanie go co każdy przebieg pętli gry, nawet szybkie kliknięcie to kilkanaście pętli co się równa kilkunastą wywołań akcji w naszych if-ach.
Żeby temu zaradzić stosujemy drugą zmienną przechowującą stan poprzedniego stanu przed naszym nowym, będziemy je porównywać w naszych if-ach:

Dla zobrazowania, nasz utworzone wcześniej tekst będziemy przesuwać o 20 pikseli w prawo dla każdego kliknięcia klawiszem SPACJI. Zaczniemy od dodania instrukcji:


keybStateOld = keybState;

przed instrukcją:


keybState = Keyboard.GetState();


Nie dajemy jej za, ponieważ nie miałoby to sensu, gdyż oba obiekty przechowujące stan klawiszy posiadały by ten sam stan klawiszy.

 dodajemy do zmiennych klasy gry obiekt:
Vector2 pozycjaTXT;

ustawiamy jego wartość początkową w metodzie LoadContent() np. na taką:

pozycjaTXT = new Vector2(0, 100);

teraz dodajemy if-a odpowiedzialnego za przemieszczanie naszego tekstu


            if (keybState.IsKeyDown(Keys.Space) && keybStateOld.IsKeyUp(Keys.Space))
            {
                pozycjaTXT += new Vector2(20, 0);
            }


na koniec zmieniamy w instrukcji rysującej nasz tekst:
new Vector2(100, 200) 
na
pozycjaTXT

Całość wygląda tak:



i sprawdzamy efekt naszego kodu. Po kliknięciu SPACJI nasz tekst powinien przemieszczać się o 20 pikseli w prawo. To na razie wszystko o przyciskach, ale analizując kod możecie zobaczyć kilka rzeczy:
-zwracanie wartości logicznej(prawda/fałsz) przez stan klawiatury.
- musimy sami zadeklarować czy chcemy sprawdzić czy przycisk jest naciśnięty, czy może wolimy sprawdzić czy przycisk jest zwolniony odwołując się do odpowiednich funkcji obiektu KeyboarState
--------------------------------------------------------------------------------------------------
Wróćmy jednak do animacji, na którą teraz przyszła kolej.
Mamy 2 podstawowe możliwości wyświetlania animacji:
- za pomocą pojedynczych obrazów, przedstawiających pojedyncze klatki
- lub za pomocą tablicy takich klatek na jednym obrazku, zwaną spritem
My zajmiemy się tą drugą możliwością. Do tego będzie oczywiście potrzebny nam taki sprite, ja użyję sprite od lf-a 2(Little Fighters 2):

Jest to w sumie lekko modyfikowany sprit, Wy jeśli chcecie możecie pobrać inny obraz postaci z LF2 lub wsiąść ten. Nie ma to różnicy ponieważ kwadratu są tutaj tej samej wielkości w każdej z postaci.

 Oczywiście nasz obraz należy dodać do Content i wczytaj w metodzie LoadContent() do utworzonego dla niego obiektu Texture2D:

Teraz utworzymy nową funkcję tworzącą miernik, dzięki któremu będziemy mogli odmierzać obieg pętli:
-po pierwszw dodajmy zmienną:


int TIMER;

do zmiennych klasy, a następnie napiszmy poniższa funkcję:
 Mamy już timer, teraz zaczniemy pisać kod do obsługi animowania klatek:
-dodajemy kilka zmiennych do naszej klasy:


        public Rectangle sourceRectangle;  // położenie aktualnej klatki
        int blokX; // rząd X naszych animacji w sprite
        int blokY; // rząd Y naszych animacji w sprite
        int wielkośćKlatek; // szerokość i wysokość pojedyńczej klatki
        int PozycjaX; //pozycja animacji na osi X
        int PozycjaY; // pozycja animacji na osi Y

w metodzie ContentLoad() dodajemy:


            wielkośćKlatek = 80;
            PozycjaX = 50;
            PozycjaY = 100;

Wygląda to tak:



teraz przejdziemy do metody Update() i dodamy do niej następujące instrukcje konfigurujące naszą aktualną klatkę:


            if (TIMER % 10 == 0) // zmiana animacji 6 razy na sekunde
            {
                if (blokX < 7)
                {
                    blokX++;
                }
                else
                {
                    blokX = 4;
                }

                blokY = 0;
               
            }
            // Utworzenie nowego kwadratu aktualnej klatki
            sourceRectangle = new Rectangle(blokX * wielkośćKlatek, blokY * wielkośćKlatek, wielkośćKlatek, wielkośćKlatek);

oraz pozwalająca przesuwać naszą animacje na 4 strony:

            if (keybState.IsKeyDown(Keys.Up)) PozycjaY--;
            if (keybState.IsKeyDown(Keys.Down)) PozycjaY++;
            if (keybState.IsKeyDown(Keys.Left)) PozycjaX--;
            if (keybState.IsKeyDown(Keys.Right)) PozycjaX++;


teraz w metodzie Draw() dodajemy:


            Rectangle pozycja = new Rectangle(PozycjaX, PozycjaY, wielkośćKlatek, wielkośćKlatek);
            spriteBatch.Draw(firen, pozycja, sourceRectangle, Color.White);

Skorzystałem tutaj z funkcji Draw ale pobierającej jeden parametr więcej(nasz sourceRectangle), który jest kwadratem zawierającym wycinek fragmentu obrazka.
 Nasz kod wygląda tak:

Możemy przetestować nasza aplikację i zobaczyć co się dzieje:)
Jak widzicie postać ma bardzo szybkie ruchy... zastanówcie się dlaczego?

Dla ułatwienia dodam że brakuje pewnej rzeczy i umieszczę cały kod mojej aplikacji oczywiście już w pełni działający(jeśli chcecie z niej skorzystać skopiujcie tylko wnętrze klasy gry, bez namespace i bibliotek.)


 
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Tutek
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;


        SpriteFont font; // nasza czcionka

        KeyboardState keybState; // stan klawiszy
        KeyboardState keybStateOld; // stan klawiszy

        Vector2 pozycjaTXT; // pozycja naszego tekstu na ekranie

        Texture2D firen; // nasz sprit z postacią
        int TIMER; // zmienna odpowiedzialna za animacje

        public Rectangle sourceRectangle;  // położenie aktualnej klatki
        int blokX; // rząd X naszych animacji w sprite
        int blokY; // rząd Y naszych animacji w sprite
        int wielkośćKlatek; // szerokość i wysokość pojedyńczej klatki
        int PozycjaX; //pozycja animacji na osi X
        int PozycjaY; // pozycja animacji na osi Y
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }


        protected override void Initialize()
        {   

            base.Initialize();
        }


        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);           

            font = Content.Load<SpriteFont>("font"); // Wczytujemy czcionkę
            pozycjaTXT = new Vector2(0, 100); // początkową pozycji tekstu
            firen = Content.Load<Texture2D>("firen0copysg6"); // wczytanie sprita

            wielkośćKlatek = 80;
            PozycjaX = 50;
            PozycjaY = 100;
        }


        protected override void UnloadContent()
        {
   
        }


        protected override void Update(GameTime gameTime)
        {

            keybStateOld = keybState;
            keybState = Keyboard.GetState();
            if (keybState.IsKeyDown(Keys.Escape)) Exit(); // Wyśjcie po nacisnieciu Esc
            if (keybState.IsKeyDown(Keys.Space) && keybStateOld.IsKeyUp(Keys.Space))
            {
                pozycjaTXT += new Vector2(20, 0);
            }
            UpTime();
            //------------------ANIMACJA------------------------------
            if (TIMER % 10 == 0) // 6 razy na sekunde
            {
                if (blokX < 7)
                {
                    blokX++;
                }
                else
                {
                    blokX = 4;
                }

                blokY = 0;
                
            }
            // Utworzenie nowego kwadratu aktualnej klatki
            sourceRectangle = new Rectangle(blokX * wielkośćKlatek, blokY * wielkośćKlatek, wielkośćKlatek, wielkośćKlatek);

            //---------------------STEROWANIE--------------------------
            if (keybState.IsKeyDown(Keys.Up)) PozycjaY--;
            if (keybState.IsKeyDown(Keys.Down)) PozycjaY++;
            if (keybState.IsKeyDown(Keys.Left)) PozycjaX--;
            if (keybState.IsKeyDown(Keys.Right)) PozycjaX++;
            base.Update(gameTime);
        }
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            spriteBatch.Begin();
            spriteBatch.DrawString(font, "Test pisania po ekranie", pozycjaTXT, Color.White);
            //-----------------------------ANIMACJA------------------------------
            Rectangle pozycja = new Rectangle(PozycjaX, PozycjaY, wielkośćKlatek, wielkośćKlatek);
            spriteBatch.Draw(firen, pozycja, sourceRectangle, Color.White);     
            //-------------------------------------------------------------------
            spriteBatch.End();
            base.Draw(gameTime);
        }

        // powiększa czas lub go resetuje
        public void UpTime()
        {
            if (TIMER < 60)
            {
                TIMER++;
            }
            else
            {
                TIMER = 0;
            }

        }

    }
}



Dzięki wszystkim za uwagę,
Silver

PS. Rozwiązaniem powyższej zagadki jest po prostu brak wywołania funkcji UpTime()