Tworzenie gry w C# z użyciem silnika Ogre - cz.2
10.09.2010 - Mateusz Osowski
![]() ![]()
[Część 7] W poprzedniej części cyklu osadziliśmy stworzoną w Blenderze postać w świecie gry i daliśmy jej możliwość chodzenia. Kluczowym elementem silnika gry, zwłaszcza RPG jest sterowanie zdarzeniami, które zachodzą w świecie gry. W tym artykule poznasz jeden ze sposobów zarządzania nimi i uzyskasz dostęp do listy obiektów widzianych przez postać. Uniezależnienie prędkości gry od FPSZanim przejdziemy do implementowania zdarzeń, zwróćmy uwagę na niedoskonałości poprzedniej wersji silnika. Jedną z nich jest zależność prędkości gry od ilości wyświetlanych na sekundę klatek . Użyliśmy synchronizacji pionowej, aby ograniczyć prędkość renderowania klatek do 60. Nie jest to najlepsze rozwiązanie - bowiem zakładamy, że częstotliwość odświeżania ekranu gracza jest równa 60Hz. W przypadku wyższej częstotliwości gra potoczy się szybciej. W przypadku słabszego sprzętu, gdy nie będzie on w stanie wyświetlić 60 klatek tempo gry odpowiednio zmaleje. Istnieje pewne proste i często stosowane rozwiązanie tego problemu. Polega ono na skalowaniu wszelkiego ruchu przez zmienny współczynnik bazujący na aktualnym FPS. Jest ono poprawne, lecz w przypadku gęstych, dużych zmian FPS Newton nie będzie w stanie poprawnie przeprowadzić poprawnej symulacji, w efekcie czego, przykładowo, ciała mogą przypadkowo wybijać się w powietrze. Zastosujemy kompromisowe rozwiązanie pozwalające uniknąć błędów numerycznych, polegające na kontroli częstotliwości aktualizowania stanu gry. Dostosujemy fizykę i logikę gry do stałego, z góry znanego FPS i będziemy dbać o to, by ilość wywołań funkcji aktualizacyjnych była adekwatna do czasu, który upłynął. Inaczej mówiąc, oddzielimy klatki graficzne od klatek logiki i fizyki. Przejdź do klasy Engine i dopisz następujące pola.
Pole Idea rozwiązania opiera się na założeniu, że czas przetwarzania logiki i fizyki jest o wiele krótszy od czasu renderowania grafiki. Sumując czas renderowania kolejnych klatek, w końcu uzbieramy go tyle, ile wynosi krok czasowy do którego przystosowaliśmy grę. W przypadku, gdy podczas jednej klatki upłynie go więcej niż ustalona długość kroku, będziemy mogli wykonać kilka kroków mechaniki gry.
Gruntowne zmiany zachodzą w metodzie
W linijce 3 pobieramy aktualny czas mierzony w milisekundach i dzielimy go przez 1000 by uzyskać czas w sekundach. Następnie obliczamy długość klatki aktualizując wartość
Pozbądźmy się powiązań z konretną wartością FPS w metodzie
Musimy także dostosować do zmian obrót postaci w pętli głównej, w pliku Program.cs:
W przypadku obrotu musimy dokonać skalowania, ponieważ wykonujemy je niezależnie od aktualizacji fizyki i mechaniki. W przyszłości dobrze będzie wprowadzić klasę kontrolera postaci przystosowaną do stałego FPS aktualizowaną wraz z silnikiem fizycznym i mechaniką gry, odpowiadającą za sterowanie dowolną postacią.
Od tej pory aplikacja powinna zachowywać się prawidłowo po wyłączeniu synchronizacji pionowej. Możesz już badać wydajność aplikacji monitorując FPS za pomocą narzędzi takich jak Fraps bez dokonywania zmian w kodzie. ![]() Aktualny kod źródłowyO zdarzeniach słów paręZapewne zdarzyło Ci się grając w grę, zastanawiać nad tym, jak to jest, że elementy świata ze sobą współgrają i są od siebie zależne. W zależności od rodzaju gry, jest to różnie rozwiązane. Jedną z ciekawszych koncepcji stanowi rozwiązanie sterowane zdarzeniami. Jak to jest zrobione? Jako przykładem, posłużymy się prostą grę platformową, np. Metal Slug znaną z automatów do gier. Akcja gry przebiega liniowo - bohater porusza się w kierunku jednej z krawędzi ekranu niszcząc po drodze swoich wrogów za pomocą różnorakiej broni. Przykładowym i oczywistym zdarzeniem występującym w tej grze jest pojawienie się przeciwnika. Z pojęciem zdarzenia wiąże się naturalne pojęcie wyzwalacza - obiektu, który wyzwala, tj. tworzy nowe zdarzenia. W grze Metal Slug przeciwnicy pojawiają się, gdy bohater posunie się odpowiednio daleko do przodu, lub też, w przypadku gdy przeciwnicy atakują oddziałami, po pokonaniu ostatniego z reprezentantów oddziału. Te sytuacje są zdarzeniami i mogą być jednocześnie wyzwalaczami kolejnych zdarzeń, tworząc tym samym łańcuchy. Zdarzenia w szczególności mogą modyfikować stan gry. Przykładowo, w Metal Slug pokonanie piątego oddziału wrogów w pewnym miejscu może wyzwolić zdarzenie, które informuje grę o czystości strefy, co pozwoli graczowi posunąć się dalej. Jak widać, koncepcja nie jest skomplikowana, a dostarcza nam wiele możliwości. Zdarzenia pozwalają sterować mechaniką gry i modelować konkretny świat, w którym toczy się rozgrywka. Definicje zdarzeń gry w przypadku gier pisanych w językach takich jak C++ zazwyczaj umieszcza się w skryptach, które są wykonywane poprzez silnik skryptowy, np. Lua. Ma to na celu między innymi oddzielenie rzadko zmienianej części gry - jej silnika - od opisu jej świata, który często ulega poprawkom, co pozwala na oszczędzenie czasu związanego z kompilacją projektu. Ponadto język skryptowy jest nierzadko istotnie prostszy od C++, tak więc znika potrzeba zatrudniania dodatkowych programistów C++. W związku z tym, że nasz silnik gry jest pisany w języku C# czas kompilacji jest bardzo krótki, a sam kod nieskomplikowany. Nie istnieje więc potrzeba użycia zewnętrznego silnika skryptowego. Ponadto kod zarządzany oferuje nam znacznie wyższą wydajność od silników języków takich jak Lua.
Wyzwalacz obszarowyW niemalże każdej grze warto mieć możliwość wywoływania jakiejś akcji, w momencie gdy postać znajdzie się w pewnym obszarze. W tym celu zaimplementujemy w silniku gry wyzwalacz obszarowy. Czego oczekujemy od takiego wyzwalacza?
Na początku zajmiemy się pierwszymi dwoma punktami. Ponieważ chcielibyśmy mieć możliwość badania kolizji, najlepiej będzie, jeśli skorzystamy z funkcjonalności dostarczanej przez silnik fizyczny. W poprzednim artykule tworzyliśmy ciała fizyczne nadając im określoną przez obiekt klasy Collision geometrię. Podobnie postąpimy i tym razem. Newton oferuje możliwość wygenerowania geometrii wielu różnych prostych kształtów (prymitywów), takich jak prostopadłościany, sfery, walce i kapsuły. Pozwala nam także wygenerować obiekt kolizji z siatki wczytanej przez Ogre. Za pomocą prymitywów nie uzyskamy jednak dowolnego obszaru, zaś użycie siatek zmuszałoby nas do modelowania każdego obszaru w Blenderze. Zastosujemy więc ogólniejsze rozwiązanie - damy naszemu obiektowi możliwość utworzenia obiektu kolizji za pomocą połączenia kilku prymitywów. Dzięki temu w przyszłości będzie można utworzyć edytor map, w którym obszary będzie się wygodnie komponowało za pomocą myszki.
Utwórz nowy plik o nazwie TriggerVolume.cs i zaimportuj do niego przestrzenie nazw Mogre i MogreNewt. Klasa TriggerVolume powinna dziedziczyć z utworzonej w poprzedniej części artykułu klasy GameObject .
Potrzebować będziemy ciała i listy obiektów kolizji z których zostanie utworzony ostateczny kształt:
Utworzymy dwie metody: BeginShapeBuild() i EndShapeBuild() . Pomiędzy ich wywołaniami będzie można za pomocą specjalnych metod dokładać nowe prymitywy do wyzwalacza.
Metoda nie jest skomplikowna, po prostu tworzymy obiekt listy.
W 3 linijce budujemy nowy obiekt kolizji na podstawie obiektów z listy części. Ponieważ konstruktor wymaga tablicy obiektów typu Collision, konwertujemy naszą listę do postaci tablicy. Następnie, w linijce 7 tworzymy ciało o zerowej masie i bezwładności. Zerowa masa nakazuje silnikowi traktować ciało jako obiekt statyczny, zatem nie będzie on podlegać działaniu sił i grawitacji. Korzystaliśmy już z tego tworząc ciało planszy w klasie Level . Zauważmy, że nie wiążemy z ciałem żadnego obiektu typu Node, obszar bowiem nie będzie posiadał reprezentacji graficznej. Mimo to, będziemy mogli go zobaczyć przy pomocy debuggera Newtona. W linijce 9 wiążemy z ciałem obiekt typu TriggerVolume . Pole UserData jest bardzo przydatne, ponieważ pozwala dostać się do obiektu korzystającego z ciała poprzez referencję.
Dodamy jeszcze metodę dodającą prostopadłościan do listy kształtów:
Pierwszy i drugi parametr określają odpowiednio przesunięcie i obrót prostopadłościanu względem środka ciała. Trzeci parametr to rozmiar. Konstruktor obiektu kolizji wygląda podobnie do konstruktora kapsuły, który wykorzystaliśmy tworząc obiekt kolizji postaci. Ciało utworzone w metodzie EndShapeBuild() będzie się znajdowało w pozycji (0,0,0), zatem potrzebujemy metody przestawiającej ciało w dowolne miejsce:
W związku z tym, że klasa TriggerVolume dziedziczy z GameObject , potrzebujemy przeciążyć metodę Update() . Pozostawimy ją w tym momencie pustą.
Możemy przetestować działanie naszego obiektu dodając jeden do świata gry. Przejdź do klasy Program i utwórz obiekt TriggerVolume . Pamiętaj jednak, aby zrobić to po inicjacji silnika:
Obiekt triggerVolume posiadać będzie jedną część - prostopadłośćian o wymiarach 2mx2mx2m. Ustawiamy go w pozycji (4,1,0), w miejscu schodów.
Możesz uruchomić aplikację i zobaczyć efekty.
![]() Aktualny kod źródłowy
(4 ocen) |
Copyright © 2008-2010 Wrocławski Portal Informatyczny
design: rafalpolito.com