Wybrane aplikacje systemów mobilnych. Windows Phone 7 XNA
1. Wstęp do XNA XNA Game Studio 4.0 to środowisko programistyczne, które pozwala na używanie Visual Studio do tworzenia gier na telefony z systemem Windows Phone, konsoli Xbox 360 i komputerów z systemem Windows. XNA Game Studio zawiera XNA Framework, który jest zbiorem bibliotek przeznaczonych do zarządzania rozwojem gier opartych na Microsoft. NET Framework. Zbiór ten zawiera przegląd dokumentacji technologii, samouczki i materiały informacyjne związane z XNA Game Studio. 1.1 Cykl życia gry. Środowisko XNA jest odpowiedzialne za zarządzanie cyklem życia oprogramowania poprzez wywoływanie metod odziedziczonych po klasie Game tak jak przedstawiono to na rysunku 1. Przedstawiony cykl życia jest podstawą programowania gier, nie tylko w środowisku XNA. Rysunek 1 Kolejność wywoływania metod obiektu dziedziczącego po klasie Game
Metoda Initialize jest wywoływana na starcie gry aby przeprowadzić inicjalizację obiektu. Następnie wołana jest metoda LoadContent, w której obiekt powinien załadować wszystkie zasoby zewnętrzne, z których będzie korzystał. Metody Update i Draw tworzą tzw. pętlę gry i są wywoływane wielokrotnie, wiele razy na sekundę w zależności od obciążenia generowanego przez grę. Metoda Update powinna zawierać logikę aplikacji, na którą składa przykładowo składa się: Zebranie danych wejściowych od użytkownika Aktualizacji fizyki i sztucznej inteligencji używając zebranych danych wejściowych Aktualizacja animacji obiektów Aktualizacja stanu i położenia kamery Metoda Draw musi zawierać kod odpowiedzialny za rysowanie obiektów, ustawianie shader'ów, itd. 1.2 Zarządzanie zasobami Gra, tak jak każda aplikacja, składa się z zasobów i logiki. Jak już zostało wspomniane wcześniej, logika gry aktualizowana jest w metodzie Update, jednak zarządzanie zasobami odbywa się osobno. 1.3 Używanie klasy ContentManager XNA Framework udostępnia klasę ContentManager, która jest częścią przestrzeni nazw Microsoft.Xna.Framework.Content. Instancja klasy ContentManager pozwala programiście zdefiniować główny folder zawierający wymagane przez grę zasoby takie jak grafiki, dźwięki, modele, jak również pozwala na wczytanie ich do aplikacji.
Klasa Game zawiera referencję do instancji klasy ContentManager we właściwości Content. Przydatną praktyką może być nadpisywanie wyżej wymienionej właściwości innym obiektem, co umożliwia proste przełączanie się między przygotowanymi wcześniej zestawami zasobów. Technika ta może być przykładowo wykorzystana w grze, w której mamy wiele różnych plansz. Dla każdej planszy możemy przygotować różniące się nieco zestawy zasobów, w szczególności grafik. Następnie możemy stworzyć więcej obiektów klasy ContentManager, po czym po prostej podmianie właściwości Content, w prosty sposób możemy wybrać, który zestaw zasobów chcemy w danej chwili wykorzystać. 1.4 Shadery Do dyspozycji programistów będą oddane następujące predefiniowane shadery: BasicEffect (stary, dobrze znany, oferujący podstawowe efekty) DualTextureEffect (jak sama nazwa wskazuje, mieszanie tekstur) AlphaTestEffect (billboarding) SkinnedEffect (animacje) EnviromentMapEffect (mapa środowiska na obiekcie, odbicia) Custom Shadery nie będą dostępne, przy czym Microsoft nie wyklucza ich wprowadzenia w przyszłości. Dostępne rozdzielczości dla gier: 480 800 360 600 240 400
1.5 Szkielet aplikacji Wiemy już co to XNA i do czego służy, przejdźmy więc teraz do stworzenia pustego projektu i zapoznania się z założeniami technologii. Po utworzeniu nowego projektu mamy już gotowy kod startowy dla naszej aplikacji Jak widać na obrazku obok nowe rozwiązanie składa się z dwóch projektów. Pierwszy z nich zawiera kod naszej gry i jest nazwany tak jak tylko wybraliśmy podczas tworzenia rozwiązania. Drugi projekt jest natomiast miejscem dla zasobów naszej aplikacji takich jak: grafiki, modele, dźwięki i wideo. Jest to korzeń z którego korzysta wspomniany wcześniej ContentManager. Rysunek 2 Szkielet rozwiązania W głównym projekcie, można zauważyć trzy pliki graficzne. Plik Background.png jest to obraz, który pojawi się na stronie głównej telefonu, kiedy aplikacja jest do niej przypięta. Miniaturka GameThumbnail.png będzie wyświetlana na liście wszystkich aplikacji zainstalowanych w telefonie po prawej stronie od ekranu głównego. Ikona Game.ico jest ikoną wykorzystywaną wyłącznie dla gier na Windowsa, i nie jest używana dla Windows Phone i Xboxa.
1.6 Zawartość klasy Game1 Game1.cs jest plikiem w którym zaczynamy budować naszą grę. Przyjrzyjmy się teraz bliżej funkcjom w nim zawartym. W konstruktorze tworzymy GraphicsDeviceManager z którego będziemy mogli pobrać parametry ekranu. 1.6.1 Metoda Initialize Metoda inicjalizująca jest miejscem dla ustawienia zmiennych wykorzystywanych w naszej grze przed jej uruchomieniem. 1.6.2 Metoda LoadContent LoadContent jest metodą przeznaczona do ładowania zasobów z Content Project. Typowym zastosowaniem jest ładowanie obrazów, dźwięków, modeli 3D.
SpriteBatch, który jest domyślnie inicjalizowany w metodzie LoadContent(), jest wykorzystywany do wyświetlania grafik 2D. 1.6.3 Metoda UnloadContent Metoda UnloadContent() jest wykorzystywana jeżeli ładujemy jakieś zasoby poza metodą LoadContent(). Z reguły funkcja ta nie jest wykorzystywana. 1.6.4 Metoda Update Update() jest metodą nieustannie wywoływaną poprzez główną pętle gry. W tej metodzie zarządzamy obiektami naszej gry, aktualizujemy ich pozycje, stan itp. Obsługuje się tutaj także interakcje z użytkownikiem. Jeśli przetwarzanie tej metody trwa więcej niż 1/30s. metoda ta jest dalej wykonywana zamiast metody Draw(). Kod zawarty w tej metodzie po utworzeniu projektu zawiera obsługę przycisku back. Jeśli przycisk ten zostanie wciśnięty aplikacja zostanie zamknięta.
1.6.5 Metoda Draw Draw jest metodą, która podobnie jak Update(), jest wywoływana cały czas przez główną pętlę. W tej metodzie wywołujemy metod odpowiedzialne za rysowanie naszych obiektów np. sprite'y i modele 3D. 2. Zadania do wykonania Należy pobrać ze strony przedmiotu wcześniej przygotowany szkielet aplikacji i otworzyć go w Visual Studio. Projekt zawiera gotową klasę kamery (Camera.cs). Użytkownik na starcie ma możliwość intuicyjnego poruszania kamerą. Istnieje także możliwość oddalenia/przybliżenia kamery za pomocą podwójnego tapnięcia w prawym/lewym górnym rogu ekranu. Projekt zawiera również fabrykę (klasy w folderze Primitives projektu) pozwalającą łatwo tworzyć graficzne prymitywy na scenie. 2.1 Zadanie 1 Zadanie 1 polega na stworzeniu i wyświetleniu na scenie sześcianu w kolorze czerwonym oraz jego obrót. W celu dodania sześcianu na scenie skorzystamy z wcześniej przygotowanej klasy CubePrimitive. Dadajemy zmienną cube na początku programu:
Następnie musimy stworzyć nową instancję CubePrimitive i przypisać ją do zmiennej cube. Dokonujemy tego w metodzie LoadContent(). Teraz wystarczy już tylko w metodzie Draw() wywołać wyrysowanie naszego sześcianu: W tej chwili mamy nieruchomo stojący sześcian na środku naszej sceny, wprawmy go trochę w ruch :) Aby obrócić obiekt należy zmienić jego macierz świata. XNA ułatwia nam tworzenie macierzy udostępniając klasę Matrix. Skorzystamy z metody statycznej tej klasy CreateFromYawPitchRoll(yaw, pitch, roll). Parametry: yaw, pitch, roll okręślają obrót w poszczególnych osiach świata. Zmodyfikowany kod w metodzie Draw(), umożliwiający obrót bryły, powinien wyglądać tak:
Ostatnim krokiem będzie dodanie interakcji użytkownika poprzez dotykowy ekran Windows Phone 7. Gracz będzie posiadał możliwość zmiany koloru bryły poprzez podwójne tapnięcie w ekran urządzenia. W celu uzyskania takiego efektu musimy zmodyfikować metodę HandleInput(). Modyfikujemy sekcję odpowiedzialną za obsługę podwójnego tapnięcia (GestureType.DoubleTap):
2.2 Zadanie 2 Zadanie 2 polega na załadowaniu modelu żaglowca do przygotowanej wcześniej sceny. W celu dodania interakcji z użytkownikiem, zaimplementujemy również ruch wczytanego modelu. Projekt zawiera już plik modelu, po którego zaznaczeniu możemy zobaczyć ustawienia mechanizmu ContentPipeline dotyczące parametrów przetwarzania modelu. Skalę ustawiamy na 0.01 oraz zaznaczamy zmianę rozmiaru tekstur do potęg dwójki, co jest wymaganym zabiegiem. Inne dostępne opcje to m.in. generowanie mipmap (wcześniejsze przygotowanie kilku wersji teksturu o różnym poziomie szczegółów), ustawienie głównego koloru. W projekcie jest również klasa Ship, która reprezentuje logiczną część modelu, odpowiada za Rysunek 3 Ustawienia importu modelu jego stan, ruch, rysowanie, ładowanie i wszystkie najważniejsze akcje z cyklu życia zaprezentowanego we wstępie. Na początek przyjrzyjmy się jej polom i właściwościom. Rysunek 4 Pola i właściwości klasy Ship Inicjalizację pól i załadowanie modelu umieśćmy w metodzie LoadContent(), która
powinna wyglądać następująco: Rysunek 5 Ładowanie żaglowca Jak widać, ładowanie modelu w XNA Framework jest trywialne i sprowadza się do jednej linii kodu. Oś statku zainicjowaliśmy w lewo, aby zgadzała się z kierunkiem wskazywanym przez dziób statku po załadowaniu modelu. Teraz należy dodać metodę odpowiedzialną za wyświetlenie statku, którą będziemy wywoływać z głównej klasy programu. Nazwijmy ją Draw(): Rysunek 6 Wyświetlanie modelu
Podstawowy sposób wyświetlenia modelu zajmuje co prawda więcej niż jedną linijkę, ale również nie jest skomplikowany. Nasz model posiada wiele kości, czyli niezależnych obiektów 3D posiadające również swoje własne siatki (mesh), które tworzą hierarchię. Na początku pobieramy listę macierzy przekształceń kości, natomiast potem wyświetlamy każdą siatkę. To z pozoru niewygodne rozwiązanie okazuje się praktyczne, jeżeli przykładowo chcemy, aby żagle były jaśniejsze, a kadłub ciemniejszy. Wtedy wystarczy zmienić parametry efektu dla wyświetlania danej siatki aby uzyskać pożądany efekt. W naszym przykładzie każda siatka jest wyświetlana w ten sam sposób, używając domyślnego ogólnego oświetlenia sceny (bez użytych konkretnie zdefiniowanych źródeł światła), widoku i projekcji kamery, które prześlemy jako parametry metody. Właściwość World efektu określa macierz przekształceń świata dla naszego statku, którą tworzymy modyfikując bazową translację kości o obrót wokół osi Y oraz aktualne przesunięcie obiektu w świecie (Position). Do pełni funkcjonalnego statku brakuje jeszcze tylko ruchu. W tym celu utworzymy metodę Update(), którą będziemy wywoływać z głównej klasy programu. W najprostszej formie wygląda ona tak: Rysunek 7 Logika statku Przedstawiona metoda realizuje ruch w znormalizowanej osi statku Axis z szybkością Speed. W głównej klasie wykonujemy następujące kroki: Definiujemy statek: Ship Galleon; Dodajemy do Initialize: Galleon = new Ship(); Dodajemy do metody Draw linijkę: Galleon.Draw(camera.viewMatrix, camera.projectionmatrix);
Dodajemy do LoadContent: Galleon.LoadContent(Content); Dodajemy do Update: Galleon.Update(gameTime); Po dodaniu sterowania w HandleInput, statek będzie obracał się wokół osi Y oraz wzdłóż osi X. Zadanie do wykonania: zmodyfikować metodę Update statku tak, żeby oś statku obracała się razem z nim, przez co będzie się on poruszał w kierunku wskazywanym przez dziób, a nie tylko wzdłuż jednej osi X. 2.3 Zadanie 3 Zadanie trzecie polega na wprowadzenie wektora wiatru i zmodyfikowaniu ruchu żaglowca tak, aby jego szybkość była zależna od aktualnego kąta między osią statku i kierunkiem wiatru. Dopełnieniem efektu będzie przechylenie się statku w momencie poruszania się bokiem do kierunku wiatru. Wskazówki: Przy ruchu względem wiatru pomocny będzie iloczyn skalarny wektorów (dot product), który przyjmuje maksymalną wartość dodatnią kiedy wektory mają te same kierunki i zwroty, wartość 0 kiedy przecinają się pod kątem prostym i minimalną wartość (ujemną), kiedy mają te same kierunki i przeciwne zwroty.
Odpowiednio zmodyfikowany wynik iloczynu skalarnego można przyjąć jako szybkość. Przechylanie żaglowca można zrealizować używając składowej Y iloczynu wektorowego wektora wiatru z osią statku w celu określenia kierunku wychylenia (odpowiednio do znaku przyjmowanej przez w/w składową). W połączeniu z iloczynem skalarnym tych wektorów można obliczyć kąt wychylenia (TiltAngle). Po obliczeniu kąta TiltAngle należy dodać stosowny obrót do iloczynu w miejscu obliczania macierzy World podczas rysowania statku (pomocna może być metoda Matrix.CreateFromAxisAngle(oś_obrotu, kąt)).