Krok po kroku pierwsza gra w XNA Udostępnij na: Facebook

Autor: Piotr Olejarz

Opublikowano: 2010-12-20

Z poprzedniego artykułu dowiedziałeś się, czym jest XNA, jakie daje Ci możliwości i jak wygląda struktura tworzonego w nim projektu. Natomiast ten artykuł pokaże Ci, w jaki sposób podejść do tematu projektowania gry. Dowiesz się, jak tworzyć animacje 2D i przemieszczać je po ekranie, a także jak obsługiwać grę za pomocą ekranu dotykowego.  

Pobierz kod gry
AstroSurvival

Wprowadzenie

Rozpoczynając proces tworzenia gry, pierwsze, co musisz zrobić, to określić jej rodzaj. Dzięki temu zarysujesz ogólny kierunek dalszych prac. Czy twoja gra będzie platformówką, RPG-egiem, strategią, a może jakąś ich mieszanką? Na potrzeby tego artykułu skupimy się na najprostszym typie, a mianowicie na tzw. strzelance 2D. Zadaniem gracza będzie unikanie nadlatujących meteorytów. Aby ułatwić mu nieco zadanie, wyposażymy go w możliwość ich niszczenia wystrzeliwanymi ze statku kosmicznego gracza pociskami. Poniższy szkic przedstawia ogólną koncepcję gry.

                

Gra kończy się w momencie, gdy któraś z asteroid dotknie statku gracza.

Powyższy szkic pozwala nam zidentyfikować obiekty, jakie będzie zawierał nasz projekt. W poprzednim artykule wspomniałem, że grę komputerową można zdefiniować jako symulację pewnego wykreowanego przez nas świata. Obiektowe języki programowania narodziły się wraz z rozwojem symulacji, ponieważ pozwalały w łatwy sposób odtworzyć zachowanie danego obiektu i jego otoczenia w postaci kodu zrozumiałego zarówno dla człowieka, jak i dla maszyny. Wszystkie obiekty, jakie istnieją w świecie naszej gry, przedstawione są na wcześniej wspomnianym szkicu. Następnym więc naszym zadaniem jest ich prawidłowe opisanie.

Aby logika naszej gry mogła wychwycić warunek jej zakończenia, musi „wiedzieć”, w jakim miejscu znajdują się obiekty. W tym celu każdy z elementów musimy wyposażyć w atrybut mówiący o ich aktualnym położeniu. XNA dostarcza gotową strukturę, która umożliwia przechowywanie współrzędnych w dwuwymiarowej przestrzeni. Vector2 możesz interpretować na dwa sposoby. Jeden z nich to wspomniany wcześniej punkt w przestrzeni. Drugi sposób to interpretować Vector2 jako wektor i wykorzystywać wbudowane metody, które pozwalają np. na obliczenie jego długości lub normalizację. Pamiętaj także, że możesz swobodnie dodawać, odejmować i mnożyć przez skalar każdy obiekt typu Vector2.

Mając już informacje o położeniu danego obiektu, musimy jeszcze wyposażyć go w metody, które umożliwią  mu poruszanie się po ekranie. W przypadku asteroid sytuacja jest dość prosta. Musimy tylko zadbać o to, aby przemieszczały się one z góry na dół ekranu, odbijając się od bocznych krawędzi. Statek musimy natomiast wyposażyć w metody pozwalające na sterowanie nim przez gracza i zadbać, aby nie mógł on opuścić widocznego na ekranie obszaru. Trochę odrębny problem stanowi sposób, w jaki rozwiążemy kwestię wystrzeliwania pocisków. Na początku przyjmijmy założenie, że gracz może oddać  kolejny strzał tylko wtedy, gdy poprzedni trafił w asteroidę lub opuścił pole widzenia gracza. Jak sam później zobaczysz, ten trik uprości nam trochę logikę gry, a tym samym odpowiedzialny za nią kod. Wszelkie informacje na temat stanu pocisku umieścimy w obiekcie gracza, a następnie wyposażymy go w dodatkowe metody pozwalające na ich odczytanie. Postępując w ten sposób, sprawimy, że kod stanie się trochę bardziej czytelny. Pozostał nam jeszcze tylko jeden problem do rozwiązania. W jaki sposób gracz będzie sterował swoim statkiem kosmicznym? Pytanie to być może wydaje Ci się proste, ale pamiętaj, że nasza gra będzie działać na smartphonie, a nie zwykłym domowym komputerze wyposażonym w klawiaturę i myszkę. Urządzenia pracujące pod kontrolą Windows Phone 7 mogą być wyposażone w fizyczną klawiaturę. Wszystko zależy od producenta danego urządzenia. Jeśli chcesz, aby twoja aplikacja znalazła szerokie grono odbiorców, powinieneś umożliwić jej działanie na każdym smartphonie. Obsługa fizycznej klawiatury może być tylko dodatkowym atutem. Możesz być natomiast pewien, że każde urządzenie z WP7 posiada dotykowy ekran i nasza gra będzie obsługiwana właśnie za jego pomocą. Ekran urządzenia zostanie podzielony na dwie części. Górna będzie zawierać naszą grę, dolna zaś będzie wyświetlać odpowiednie kontrolki pozwalające na sterowanie pojazdem gracza. Każde dotknięcie ekranu będzie rejestrowane i odpowiednio analizowane w celu sprawdzenia, którą z kontrolek wcisnął gracz. Opisany pomysł możesz zobaczyć na poniższym szkicu koncepcyjnym:

       

Nasza gra będzie wymagała, aby gracz trzymał swój telefon pionowo. Tryb poziomy nie będzie wspierany.

Od pomysłu do kodu

Prace nad kodem naszej gry zaczniemy od klasy reprezentującej obiekt gracza. Poniższy rysunek przedstawia klasę gracza o nazwie Rocket:

  

Zawarte w niej pola oraz metody opisują obiekt, którym steruje gracz.

Atrybut texture typu Texture2D przechowuje teksturę ze specjalnie przygotowanym plikiem graficznym zawierającym animację przedstawiającą pojazd kosmiczny gracza. (Więcej na temat animacji będzie w dalszej części artykułu). Z wcześniej wspomnianym atrybutem powiązane są jeszcze dwa inne – currentFrame,którego zadaniem jest pamiętanie aktualnej klatki animacji, oraz stała FRAMES, określająca ilość klatek animacji w pliku graficznym. Parametr position określa aktualną pozycję statku. Podczas wstępnej fazy prac nad naszą grą założyliśmy, że wszystkie informacje na temat pocisku będą przechowywane w klasie gracza. W tym celu w klasie Rocket umieścimy strukturę Missle i atrybut bullet. Oto jak wygląda wspomniana struktura:

Jak widzisz, Missle składa się z dwóch publicznie dostępnych atrybutów. Pierwszy z nich określa aktualne położenie pocisku, drugi pozwala na określenie, czy pocisk został już wystrzelony przez gracza. Atrybut  przyjmie wartość false, jeśli graczowi uda się trafić asteroidę lub jeśli pocisk wyleci poza górną krawędź ekranu.

Przyjrzyjmy się teraz dokładniej metodom klasy Rocket. Konstruktor tej klasy za atrybut wywołania przyjmuje obiekt typu Texture2D, który jest następnie przypisywany do pola texture. Jak zapewne wiesz, w C# obiekty klas przekazywane są przez referencję. To, co trafia do konstruktora, to w efekcie referencja na obiekt  stworzony w głównej klasie gry. Jej wartość będzie nam potrzebna podczas wywołania metody odpowiedzialnej za rysowanie grafiki na ekranie smartphona. Metoda GetPosition() zwraca aktualną pozycję statku gracza, która będzie nam potrzebna podczas wykrywania kolizji. Wszystkie metody z rodziny  Move sprawiają, że pozycja gracza zmienia się o ustaloną liczbę pikseli w danym kierunku. Podczas ich wywołania, które następuje wówczas,  gdy gracz dotknie odpowiedniego miejsca na ekranie, odbywa się sprawdzenie, czy obiekt gracza nie przekroczy granicy obszaru widocznego ekranu. Wartości graniczne są tak ustawione, żeby brały pod uwagę wielkość tekstury reprezentującej statek gracza. Metody GetFireState() i GetMisslePosition()umożliwiają dotarcie do informacji zgromadzonych w strukturze Missle. Jeśli gracz zechce wystrzelić pocisk, to w obiekcie zostanie wywołana metoda Fire(). Jej zadanie polega na zasygnalizowaniu wystrzału (bullet.isShooting = true), a także ustawieniu pozycji początkowej w ten sposób, aby pocisk sprawiał wrażenie wystrzelonego z dziobowej wyrzutni statku. Metoda UpdateMisslePos() zmienia pozycję pocisku w taki sposób, by pocisk leciał w linii prostej od pozycji, z której został wystrzelony, do górnej krawędzi ekranu. Po jej osiągnięciu następuje jego dezaktywacja i gracz może oddać kolejny strzał. Jeśli pocisk podczas swojej „podróży” w górę ekranu zderzył się z jakimś asteroidem, to w obiekcie gracza wywoływana jest metoda MissleHited(), która natychmiast dezaktywuje pocisk.

Animacja

Statycznie poruszające się po ekranie grafiki asteroid i statku kosmicznego możemy zastąpić prostymi animacjami. Dzięki temu nasza gra zyska trochę na dynamice. 

Efekt animacji można osiągnąć poprzez odpowiednio szybkie wyświetlenie serii grafik, przedstawiających kolejne fazy ruchu animowanego obiektu. Zacznijmy więc od stworzenia animacji statku kosmicznego gracza.

W tym celu utworzymy plik graficzny zawierający kolejne klatki animacji:

 

Powyższa grafika będzie odpowiadała za animację strumienia ognia wydobywającego się z dyszy statku. Podczas tworzenia tego typu plików pamiętaj, aby zachować równe odstępy pomiędzy kolejnymi klatkami. Swoje prace zapisuj w formacie .png, dzięki czemu będziesz mógł łatwo uzyskać efekt przeźroczystości tła przy użyciu odpowiedniego edytora graficznego (np. Paint.net). W poprzednim artykule była mowa o udogodnieniach, jakie dostarcza programiście XNA. Jedną z takich funkcji był mechanizm Content Pipeline. Aby umożliwić mu poprawne działanie, wszystkie elementy zewnętrzne gry muszą znaleźć się w folderze Content twojego projektu. Procedura dodawania zewnętrznego zasobu wygląda następująco: w Visual Studio, w oknie Solution Explorer, kliknij wspomniany folder prawym przyciskiem myszy. Z menu, które się pojawi, wybierz Add →ExistingItem i odszukaj odpowiedni element.

Samo stworzenie odpowiedniego pliku graficznego to jednak nie wszystko. Aby uzyskać zamierzony efekt ruchu, musimy stworzyć zestaw metod, które odpowiednio odczytają i wyświetlą poszczególne klatki animacji. W klasie Rocket istnieją dwie metody odpowiedzialne za poprawne wyświetlenie odpowiedniej grafiki.  Najważniejsza z nich to metoda Draw. Parametrem jej wywołania jest obiekt typu SpriteBatch, który jest odpowiedzialny za rysowanie przekazywanych mu grafik. Procedura rysowania odbywa się w następujący sposób:

  1. Nasza animacja zawiera sześć klatek. Jeśli podzielimy długość całej grafiki przez ich liczbę, to otrzymamy długość pojedynczej klatki. Dzięki temu będziemy mogli odwołać się do kolejnych klatek poprzez proste pomnożenie indeksu przez wcześniej otrzymaną wartość.
  2. Aby narysować cokolwiek na ekranie smartphona, musimy wywołać metodę Draw z obiektu typu SpriteBatch. Istnieje kilka wersji metody Draw, różniących się między sobą parametrami wywołania (metoda Draw  jest przeciążona). Jedna z nich pozwala na rysowanie wybranego fragmentu tekstury na określonym obszarze ekranu. Oba te parametry są definiowane przy użyciu struktury Rectangle, która określa prostokątny obszar poprzez wskazanie położenia jego lewej górnej krawędzi, a także szerokości i długości. Poniższy kod odpowiada za zdefiniowanie obszaru źródłowego:
Rectangle sourceRectangle = new Rectangle(frame * currentFrame, 0, frame, texture.Height);

Zmienna currentFrame jest indeksem służącym do poruszania się po pliku graficznym. Jej wartość zwiększa się o jeden przy każdym wywołaniu metody Update (currentFrame = ++currentFrame % FRAMES, co zapewnia, że nie „wypadniemy” poza obszar naszego pliku). Jako że cały czas poruszamy się wzdłuż osi X, współrzędna Y pozostaje nie zmieniona. Dwa ostatnie parametry określają rozmiar pojedynczej klatki animacji.

  1. Określamy obszar, w którym ma się wyświetlić nasza wyodrębniona klatka animacji:
Rectangle destinationRectangle = new Rectangle((int)position.X, (int)position.Y, frame, texture.Height);

Dwa pierwsze parametry określają teraz aktualne położenia statku gracza.

  1. Wywołujemy metodę Draw  i przekazujemy jej odpowiednie wartości:
spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, Color.White);

Pierwszy z nich to nasz plik z animacją, ostatni natomiast określa odcień wyświetlanej grafiki. Przekazanie wartości Color.White sprawi, że tekstura zachowa swoje oryginalne kolory.

W taki oto sposób stworzyliśmy podstawowy mechanizm pozwalający na wyświetlanie prostej animacji. Nasze kolejne zadanie to zbudowanie klasy opisującej asteroidę, a następnie połączenie wszystkiego w całość.

Od pomysłu do kodu – ciąg dalszy

Kolejny etap prac nad naszą grą to stworzenie klasy opisującej asteroidy:

Klasa Meteor zawiera, podobny do klasy Rocket, zestaw metod i atrybutów. Podczas jej opisywania skupimy się więc tylko na miejscach, w których różni się ona od klasy Rocket.

Pierwszym, nowym parametrem jest count. Każda asteroida będzie poruszała się samodzielnie, co oznacza, że będziemy musieli wyposażyć ją w odpowiednie metody, które pozwolą jej na swobodne przemieszczanie się po ekranie. W poprzedniej implementacji metoda Update zwiększała tylko indeks klatki animacji. W klasie Meteor, oprócz wspomnianej funkcji, jej zadaniem jest także kontrola położenia asteroidy. Parametr count odpowiada za to, jak często zawarty w niej kod zostanie wykonany. Dzięki temu spowalniamy animację i ruch asteroidy względem innych obiektów występujących w grze. Parametr velocity określa wektor przyspieszenia. Jego kierunek jest zmieniany, jeśli asteroida zderzy się z którąś z bocznych ścian.

Konstruktor klasy Meteor posiada trzy parametry. Pierwszy z nich, tak jak poprzednio, inicjuje atrybut texture. Parametr seed to wartość ziarna generatora pseudolosowego. Standardowy konstruktor Random podczas tworzenia obiektu za wartość ziarna przyjmuje aktualny czas systemowy. Nie ma problemu, jeśli w danym czasie tworzymy pojedynczy obiekt tego typu. Sytuacja komplikuje się, jeśli – tak jak w naszym przypadku – chcemy stworzyć cały ich zbiór. Gdyby nie ręczne ustawienie wartości ziarna, to każdy generator w obiektach typu Meteor generowałby ten sam ciąg wartości. Kolejnym nowym parametrem jest bornT,który określa opóźnienie, z jakim asteroida pojawi się po uruchomieniu gry. Aby włączyć dany obiekt do gry, musimy wywołać metodę Resurrect(int gameTime) z wartością aktualnego czasu gry. W chwili jej wywołania następuje sprawdzenie, czy czas, w którym dana asteroida miała pojawić się na ekranie, jest mniejszy bądź równy od aktualnego czasu gry. Jeśli warunek jest spełniony, to asteroida zostanie umieszczona poza widocznym obszarem ekranu. Następnie losowana jest wartość wektora przyśpieszenia. Umieszczając nowy obiekt poza widocznym ekranem, sprawimy, że zamiast pojawiać się znikąd, asteroida zacznie powoli wyłaniać się znad górnej krawędzi. Metoda IsAlive zwraca wartość true, jeśli dana asteroida nie została jeszcze trafiona pociskiem. W przypadku wystąpienia takiego trafienia, wywoływana jest metoda Kill z wartością aktualnego czasu gry, która ustawia czas, po jakim asteroid zostanie przywrócony do gry.

Ostatnia prosta

Na tym etapie zakończyliśmy tworzenie niezbędnych elementów gry. Od teraz zajmiemy się jej logiką. Dowiedziałeś się już z poprzedniego artykułu, jak wygląda struktura klasy Microsoft.Xna.Framework.Game. Na potrzeby naszej gry ta podstawowa struktura zostanie rozszerzona o kilka niezbędnych atrybutów, które będą przechowywać obiekty wcześniej zdefiniowanych klas ( playeri enemies[] ). Potrzebne nam będą również zewnętrzne tekstury przechowywane w obiektach typu Texture2D. Dla metody wykrywającej kolizje zdefiniujemy strukturę CollisionInfo.

Podczas opracowywania koncepcji gry założyliśmy, że rozgrywka będzie się odbywać wyłącznie w pionowej orientacji smartphona. Standardowo, XNA uruchamia twoje gry w orientacji poziomej (800x480). Aby to zmienić, w konstruktorze klasy Game1 umieszczamy poniższy kod:

graphics.PreferredBackBufferWidth = 480;

graphics.PreferredBackBufferHeight = 800;

Dzięki funkcji autoskalowania XNA dopasuje wielkość wyświetlanego obrazu do wielkości wyświetlacza. Obraz zostanie na stałe obrócony do pozycji pionowej (480x800).

Każda aplikacja uruchomiona w systemie Windows Phone 7 standardowo nie wykorzystuje całej powierzchni ekranu. Część górnej przestrzeni przeznaczona jest na pasek statusu. Pisząc grę na urządzenie typu smartphone o dość ograniczonej wielkości ekranu chcielibyśmy mieć jak najwięcej wolnych pikseli do zagospodarowania,  i dlatego ukryjemy pasek stanu, wstawiając następujący kod w konstruktorze klasy:

 

graphics.IsFullScreen = true;

Zaraz po konstruktorze wywoływana jest metoda Initialize. Jej jedynym zadaniem, oprócz standardowego zaincjalizowania komponentów klasy Game, z której dziedziczymy, będzie także powołanie tablicy obiektów typu Meteor. O wiele więcej pracy czeka nas w metodzie LoadContent, gdzie załadujemy wszystkie potrzebne nam zewnętrzne zasoby. Oto przykład kodu odpowiedzialnego za załadowanie pliku graficznego o nazwie rocket.png, znajdującego się w folderze Content:   

rocket = this.Content.Load<Texture2D>("rocket");

W metodzie tej tworzymy także obiekt gracza playeri obiekty w tablicy enemies. Z kolei w metodzie UnloadContent zwalniamy wszystkie załadowane zasoby poprzez wywołanie metody Dispose.

Cała logika naszej gry będzie znajdować się w metodzie Update. Jest to także miejsce, w którym będą obsługiwane zdarzenie generowane przez gracza. Ich głównym źródłem będzie dolna część dotykowego ekranu, która będzie służyć za wirtualny gamepad. Jego zasada działania będzie dość prosta: spośród wszystkich miejsc których dotknął gracz, ma wychwycić te, które znajdują się w pewnym określonym obszarze ekranu.  Każdy obszar będzie miał przypisaną określoną funkcję, np. dotknięcie pewnego obszaru spowoduje wystrzelenie pocisku. Zgodnie z naszym wstępnym projektem użytkownik gry na swoim wirtualnym

gamepadzie znajdzie dpad-a i jeden przycisk. Poniżej możesz zobaczyć, jak będzie wyglądał ten element interfejsu wraz z zaznaczonymi obszarami, które będą sprawdzane w poszukiwaniu punktów dotkniętych przez gracza.

              

Zielone okręgi oznaczają obserwowane przez nas strefy. Dotknięcie którejś z nich przez gracza sprawi, że zostanie wywołana odpowiednia metoda w obiekcie player. Urządzenia, na których działa Windows Phone 7, muszą mieć multidotykowe ekrany. Oznacza to, że w danej chwili być może będziemy musieli obsłużyć więcej niż jeden punkt. W tym celu w metodzie Update musimy umieścić poniższy kod:

TouchCollection touchPlaces = TouchPanel.GetState();

TouchPanel to statyczna klasa, która dostarcza kilku użytecznych informacji na temat ekranu dotykowego, w jaki jest wyposażony smartphone. Najbardziej użyteczną z nich jest informacja na temat miejsc ekranu dotkniętych przez użytkownika, udostępniana w postaci kolekcji obiektów typu TouchLocation. Każdy taki obiekt opisuje pojedyncze dotknięcie ekranu za pomocą następujących parametrów:

  • State, który przyjmuje jedną z trzech wartości: Pressed, Moved, Released.  Jeśli użytkownik dotknie i przytrzyma ekran, to State w kolejnych wywołaniach metody Update będzie przechodzić następującą zmianę wartości: Pressed -> Moved -> Released. Wartość Released pojawi się, kiedy użytkownik przestanie przyciskać ekran. Istnieje jeszcze jedna możliwa ścieżka wartości: Pressed -> Released. Odpowiada ona zdarzeniu, w którym to użytkownik szybko „stuka” w ekran;
  • Position zawiera współrzędne miejsca, które dotknął użytkownik;
  • Pressure określa, jak mocno użytkownik nacisnął ekran (wartość z zakresu od 0 do 1);
  • Id jest identyfikatorem pozwalającym na śledzenie źródła dotyku (palca użytkownika).

Za każdym  razem, gdy będzie wywoływana metoda Update, będziemy przeszukiwać kolekcję TouchCollectionw poszukiwaniu punktów, które należą do określonych obszarów. Obszary te są zdefiniowane za pomocą równań okręgów. Jeśli dany punkt spełnia któreś z równań, to wywoływana jest odpowiednia metoda, np. MoveL która spowoduje przesunięcie się pojazdu gracza w lewo. Dotknięcia, które wpływają na ruch statku kosmicznego, będą miały wartość atrybutu State równą Moved, dzięki czemu gracz może kontynuować poruszanie się w określonym kierunku, trzymając palec w tym samym miejscu (lub nawet nieznacznie go przesuwając, ważne jest tylko to aby nie opuścił wyznaczonej strefy). Aby gracz nie wystrzelił pocisku przypadkowym dotykiem, wszystkie dotknięcia z obszaru odpowiedzialnego za wystrzelenie pocisku będą musiały mieć wartość State równą Pressed.

Wykrywanie kolizji to podstawowe zagadnienie każdej gry. Tworzenie takiego mechanizmu może być niekiedy dość czasochłonnym zajęciem. Gra, którą tu omawiamy, jest na tyle prosta, że zbudowanie odpowiedniej metody nie byłoby zbyt trudne – dzięki XNA jest jednak jeszcze prostsze.

Przy okazji omawiania procesu tworzenia animacji wspomniałem o strukturze Rectangle. Jej głównym przeznaczeniem jest definiowanie prostokątnego obszaru. Na tak zdefiniowanym obszarze możemy wykonywać pewne operacje. Jedną z nich jest sprawdzenie czy dwa obszary przecinają się. Metoda DetectCollisionwykorzystuje tę własność struktury Rectangle w procesie wykrywania kolizji. Dla każdego obiektu w grze tworzona jest wcześniej wspomniana struktura. Następnie sprawdzamy, czy statek gracza zderzył się z jakąś asteroidą przy użyciu metody Intersects. Tak samo postępujemy w przypadku wystrzelonego pocisku.

Metoda DetectCollision zwraca strukturę CollisionInfo zawierającą wszystkie informacje o wykrytych kolizjach. Jeśli statek gracza zderzył się z asteroidą, to pole RocketCollided przyjmuje wartość true. Jeśli natomiast wystrzelony pocisk trafił w którąś z asteroid, to pole MissleCollided przyjmuje wartość true, a pole Target wskazuje na cel, który został trafiony. Wszystkie te informacje posłużą metodzie Update do określenia stanu gry (czy zakończyć grę, czy zdezaktywować pocisk itp.).

Ostatnia metoda to metoda Draw. Jej jedynym zadaniem jest wyświetlenie wszystkich elementów na ekranie urządzenia. Cały proces rysowania zaczyna się od wyczyszczenia zawartości ekranu. Następnie obiekt typu   SpriteBatch jest inicjalizowany (spriteBatch.Begin()), a my możemy zacząć dodawać do niego elementy wymagające wyświetlenia (spriteBatch.Draw()). Pamiętaj, że kolejność dodawania elementów ma znaczenie. Te dodane najwcześniej będą wyświetlane pod tymi dodanymi później. Ostatni etap to opróżnienie obiektu SpriteBatch z już wyświetlonej zawartości (spriteBatch.End()).                    

A oto jak prezentuje się efekt końcowy naszej pracy:

Podsumowanie

Jak sam widzisz, napisanie prostej gry w XNA nie jest specjalnie trudnym przedsięwzięciem. Środowisko to wyposażono w wiele gotowych rozwiązań, które wyręczają programistę w niektórych żmudnych i czasochłonnych czynnościach.

Zachęcam Cię, czytelniku do dalszych prac nad kodem gry. Jest wiele rzeczy, które mógłbyś dodać, aby przekształcić tę prezentującą podstawowe możliwości XNA aplikację w naprawdę ciekawą grę. Pamiętaj, że przez ostatnie lata, od czasu pojawienia się XNA, środowisko to zyskało sobie sporą społeczność developerów, co znacznie ułatwia Ci zdobywanie nowych informacji oraz szukanie odpowiedzi na dręczące Cię pytania.