Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych Systemy wirtualnej rzeczywistości Laboratorium Sterowanie i wykrywanie kolizji Wstęp: W czwartej części przedstawione zostaną mechanizmy obsługi urządzeń wejściowych w XNA. Zrealizowane zostanie sterowanie czołgiem. Zostaną również omówione i zaimplementowane techniki wykrywania kolizji gracza z terenem i przeszkodami. 1. Sterowanie czołgiem Pisanie obsługi sterowania zaczniemy od obracania korpusu i wieżyczki. Przechodzimy do metody UpDate klasy Gracz. Przede wszystkim musimy pobrać informacje o stanie klawiatury. Obiektem reprezentującym klawiaturę jest Keyboard, a informacje o aktualnym jej stanie zwraca metoda GetState w formie obiektu typu KeyboardState: KeyboardState klawiatura = Keyboard.GetState(); Teraz możemy sprawdzić czy został naciśnięty dany klawisz i zmienić odpowiednio rotacje korpusu i wieżyczki. Ponieważ metoda UpDate może być wywoływana z różną częstotliwością prędkość obracania uzależniamy od czasu jaki miną od poprzedniego zaktualizowania. Z pomocą przychodzi nam parametr wejściowy metody UpDate. Jest to zmienna typu GameTime, która zawiera wiele przydatnych informacji na temat czasu gry. Nas interesuje czas od ostatniego wywołania metody UpDate w milisekundach. Dodatkowo regulujemy prędkość obrotu mnożąc czas razy współczynnik prędkości. // korpus if (klawiatura.iskeydown(keys.a)) rotacjakorpusu += gametime.elapsedgametime.milliseconds * 0.0016f; if (klawiatura.iskeydown(keys.d)) rotacjakorpusu -= gametime.elapsedgametime.milliseconds * 0.0016f; 1
// wiezyczka if (klawiatura.iskeydown(keys.left)) rotacjawiezyczki += gametime.elapsedgametime.milliseconds * 0.004f; if (klawiatura.iskeydown(keys.right)) rotacjawiezyczki -= gametime.elapsedgametime.milliseconds * 0.004f; Jeśli teraz skompilujemy naszą grę będziemy już mogli kontrolować obrót korpusu i wieżyczki czołgu. Przyszła kolej na poruszanie się do przodu i do tyłu. Ponieważ w dalszej części dodamy obsługę kolizji z innymi obiektami na scenie dla uproszczenia umieścimy kod poruszania się czołgu w metodzie UpDate klasy głównej, a nie klasy Gracz. Zaczynamy od pobrania stanu klawiatury: KeyboardState klawiatura = Keyboard.GetState(); Następnie sprawdzamy czy został wciśnięty odpowiedni klawisz: if (klawiatura.iskeydown(keys.w) klawiatura.iskeydown(keys.s)) Zmianę pozycji czołgu uzyskamy dodając do wektora pozycji odpowiedni wektor przesunięcia. Aby go wyznaczyć musimy najpierw ustalić jego kierunek. Chcielibyśmy aby nasz czołg poruszał się do przodu lub do tyły. Obliczamy zatem macierz obrotu korpusu, a następnie wykorzystujemy ją do obrócenia wektora [0.0, 0.0, -1.0] (wektor ten reprezentuje przód czołgu bez uwzględnienia rotacji, jest dostępny jako stała Vector3.Forward). Efektem będzie wektor znormalizowany (o długości 1) skierowany w stronę, w którą zwrócony jest czołg. Vector3 kierunek = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(gracz.Rotacja)); Skoro wiemy już po jakiej linii będzie poruszał się gracz wystarczy przesunąć go wzdłuż tej linii w przód lub w tył o odpowiednią ilość jednostek. Ponownie wykorzystamy czas, który minął od ostatniej aktualizacji oraz pewien współczynnik szybkości. Nasz odwrócony wektor mnożymy razy otrzymany skalar i aktualizujemy pozycję gracza. kierunek *= gametime.elapsedgametime.milliseconds * 0.008f; Vector3 nowapozycja = gracz.pozycja; if (klawiatura.iskeydown(keys.w)) 2
nowapozycja += kierunek; if (klawiatura.iskeydown(keys.s)) nowapozycja -= kierunek; gracz.pozycja = nowapozycja; 2. Śledzenie czołgu Chcielibyśmy żeby nasza kamera śledziła czołg gdy będziemy nim poruszać. Dlatego po obliczeniu nowej pozycji gracza uaktualniamy również cel kamery: kamera.cel = nowapozycja; 3. Kolizje Zagadnienie kolizji w grach komputerowych jest bardzo szerokie i zostanie przedstawione jedynie w podstawowym zakresie. Jest wiele metod wykrywania kolizji miedzy obiektami ale wszystkie sprowadzają się do określenia czy dane dwa obiekty się przenikają (np. gracz nie może przejść przez ścianę). Dokładne algorytmy sprawdzania kolizji bazują na analizie wzajemnego położenia poszczególnych trójkątów siatek badanych obiektów (w przypadku bardzo dynamicznych symulacji stosuje się również analizę trajektorii obiektów). Takie dokładne sprawdzanie wymaga oczywiście stosunkowo dużych nakładów obliczeniowych. Należy pamiętać, że z reguły te obliczenia wykonuje się w każdej klatce symulacji. Zakładając, że na scenie jest wiele skomplikowanych obiektów, a sprawdzenie kolizji należy przeprowadzić dla każdej możliwej pary tych obiektów, wymagane jest stosowanie technik optymalizacji. Jedną z nich jest uproszczenie skomplikowanych siatek do postaci prostych brył (np. sześcian, kula), dla których obliczenie kolizji jest proste i szybkie. Dopiero jak nastąpi kolizja dla brył uproszczonych stosuje się dokładniejsze algorytmy w celu weryfikacji. Właśnie takie uproszczone sprawdzanie wykorzystamy w tworzonej aplikacji. XNA posiada podstawowe mechanizmy sprawdzania kolizji opisaną wyżej metodą. Siatkę można opisać sześcianem lub kulą, a klasy reprezentujące te bryły to odpowiednio BoundingBox i BoundingSphere. 3.1 Ograniczenie poruszania sie do terenu Najpierw zajmiemy się ograniczeniem obszaru poruszania się do wygenerowanego terenu. Nasz teren jest kwadratem zatem idealnym sposobem reprezentacji przestrzeni, po której może poruszać się gracz, będzie prostopadłościan. Dodajemy więc zmienną typu BoundingBox do klasy Teren: BoundingBox szescian; 3
Inicjujemy ją w konstruktorze: szescian = new BoundingBox(new Vector3(-rozmiar, -rozmiar, -rozmiar), new Vector3(rozmiar, rozmiar, rozmiar)); Na koniec udostępnimy ją poprzez właściwość: public BoundingBox Szescian get return szescian; Teraz potrzebna nam jeszcze bryła opisująca czołg. Jego kształt jest bardziej nieregularny i możemy nim obracać więc do jego reprezentacji użyjemy kuli. Przechodzimy do klasy gracza i deklarujemy zmienną: BoundingSphere sfera; Podobnie jak w przypadku terenu inicjujemy ją w konstruktorze. Każda siatka w modelu ma już swoją kulę stworzoną na podstawie wierzchołków. Do reprezentacji naszego czołgu wykorzystamy sferę korpusu i odpowiednio ją przeskalujemy: sfera = new BoundingSphere(); sfera = model.meshes["korpus"].boundingsphere; sfera = sfera.transform(matrix.createscale(2.0f * rozmiar)); Na koniec udostępniamy ją poprzez właściwość: public BoundingSphere Sfera get return sfera; Pozostało jeszcze dodać sprawdzanie czy gracz próbuje wyjechać poza teren. Zmodyfikujemy zatem metodę UpDate klasy głównej. Po wyznaczeniu kierunku i obliczeniu nowej pozycji czołgu ale przed jej aktualizacją sprawdzimy czy nowe położenie gracza nie wykracza poza dozwolony obszar. Pobieramy sferę opisującą czołg. Przesuwamy ją w miejsce nowego położenia gracza poprzez transformację translacji. Pobieramy również sześcian opisujący teren. 4
bool kolizja = false; BoundingSphere sfera = gracz.sfera; sfera = sfera.transform(matrix.createtranslation(nowapozycja)); BoundingBox szescian = teren.szescian; Do sprawdzenia wzajemnej korelacji pomiędzy dwiema bryłami wykorzystujemy metodę Contains. Metoda ta zwraca zmienną typu ContainmentType, która określa czy dana bryła zawiera się, przecina lub jest poza inną bryłą. W naszym przypadku interesuje nas aby bryła czołgu zawierała się w bryle terenu. Jeśli nie, następuje kolizja. if (szescian.contains(sfera)!= ContainmentType.Contains) kolizja = true; Jeśli nie wystąpiła kolizja aktualizujemy położenie gracza: if (!kolizja) gracz.pozycja = nowapozycja; kamera.cel = nowapozycja; 3.2 Kolizje z przeszkodami Gracz nie może już wyjechać poza teren. Dodamy teraz przeszkody w postaci prostych skrzynek. Tworzymy nowy komponent i nadajemy mu nazwę Przeszkoda. Zmieniamy dziedziczenie na DrawableGameComponent. Siatkę skrzynki będziemy przechowywać, podobnie jak siatkę czołgu, w zmiennej typu Model. Ponadto potrzebujemy również informacji o pozycji w scenie i rozmiarze, oraz bryłę opisującą, którą będzie sześcian. Deklarujemy więc odpowiednie zmienne: Vector3 pozycja; float rozmiar; Model model; BoundingBox szescian; W konstruktorze inicjujemy zmienne: public Przeszkoda(Game game, Model model, Vector3 pozycja, float rozmiar) : base(game) this.model = model; this.pozycja = pozycja; this.rozmiar = rozmiar; 5
szescian = new BoundingBox(new Vector3(-rozmiar + pozycja.x, pozycja.y, -rozmiar + pozycja.z), new Vector3(rozmiar + pozycja.x, pozycja.y + 2 * rozmiar, rozmiar + pozycja.z)); Rysowanie przeszkody realizujemy analogicznie do rysowania czołgu: public override void Draw(GameTime gametime) GraphicsDevice urzadzenie = (GraphicsDevice)Game.Services.GetService(typeof(GraphicsDevice)); Kamera kamera = (Kamera)Game.Services.GetService(typeof(Kamera)); Matrix[] transformacje = new Matrix[model.Bones.Count]; model.copyabsolutebonetransformsto(transformacje); foreach (ModelMesh mesh in model.meshes) foreach (BasicEffect effect in mesh.effects) effect.enabledefaultlighting(); effect.projection = kamera.projekcja; effect.view = kamera.widok; effect.world = transformacje[mesh.parentbone.index] * Matrix.CreateScale(rozmiar) * Matrix.CreateTranslation(pozycja); mesh.draw(); base.draw(gametime); Na koniec udostępniamy zmienną szescian poprzez właściwość: public BoundingBox Szescian get return szescian; Stworzymy teraz dwie przeszkody. Przechodzimy do klasy głównej gry. Dodajemy do projektu plik przeszkoda.fbx. Deklarujemy zmienną typu Model oraz dwie zmienne typu Przeszkoda. Model przeszkoda; Przeszkoda przeszkoda1; Przeszkoda przeszkoda2; 6
Następnie w metodzie LoadContent wczytujemy model przeszkody z dodanego właśnie pliku: przeszkoda = Content.Load<Model>("przeszkoda"); Do listy komponentów dodajemy dwie przeszkody: przeszkoda1 = new Przeszkoda(this, przeszkoda, new Vector3(-6.0f, 0.0f, -3.0f), 0.9f); Components.Add(przeszkoda1); przeszkoda2 = new Przeszkoda(this, przeszkoda, new Vector3(2.5f, 0.0f, 2.5f), 1.2f); Components.Add(przeszkoda2); Pozostało jeszcze obsłużyć kolizje czołgu z przeszkodą. W metodzie UpDate dodamy sprawdzenie czy gracz najechał na którąś z przeszkód. Tym razem zależy nam aby czołg znajdował się poza bryłą przeszkody. Zatem kolizja wystąpi kiedy bryła gracza przetnie lub zawrze się w bryle przeszkody. szescian = przeszkoda1.szescian; if (szescian.contains(sfera)!= ContainmentType.Disjoint) kolizja = true; szescian = przeszkoda2.szescian; if (szescian.contains(sfera)!= ContainmentType.Disjoint) kolizja = true; Rys. 1 prezentuje przykładowy zrzut ekranu z gotowej aplikacji. Rys. 1 - Przykładowy zrzut ekranu z gotowej aplikacji 7
4. Zadanie dodatkowe Wykorzystując poznane mechanizmy należy dodać możliwość wystrzelenia z lufy pocisku. Do instrukcji został dołączony plik kula.fbx, który zawiera odpowiedni model 3D. Po naciśnięciu przez gracza odpowiedniego przycisku powinien nastąpić wystrzał. Dodatkowo należy sprawdzić kolizje pocisku z przeszkodami i w przypadku trafienia w przeszkodę należy ją usunąć i losowo wygenerować nową (uwzględniając położenie czołgu nowa przeszkoda nie powinna przenikać ani czołgu ani drugiej przeszkody). 8