Tworzenie gry w C# z użyciem silnika Ogre - cz.6
12.05.2011 - Mateusz Osowski
![]() ![]()
W tym artykule poruszymy bardzo ważną kwestię - modelowanie stanów postaci. Dotychczas posługiwaliśmy się prostym automatem skończonym, lecz w świetle przyszłej implementacji sztucznej inteligencji ręczne zarządzanie przejściami staje się niewygodne. Spróbujemy nowego spojrzenia na ten problem i poszukamy bardziej skalowalnego rozwiązania. W ramach polepszania modelu stanów dodamy też upłynnianie animacji.
[Część 7]
Mieszacz animacji Dotychczas nasze oczy męczone były przez natychmiastowo zmieniające się animacje. Temu fatalnemu efektowi jesteśmy jednak w stanie zaradzić. Ogre umożliwia nadawanie animacjom określonych wag i odtwarzanie wielu animacji w jednej chwili. Nie poprzestaniemy jednak na upłynnianiu przejść między animacjami. Zauważmy, że w pewnych sytuacjach może zaistnieć potrzeba użycia dwóch animacji jednocześnie z pełnymi wagami, lecz na różnych częściach szkieletu. Załóżmy na przykład, że chcemy w trakcie biegu rozpocząć zamach mieczem, aby równocześnie z dobiegnięciem do celu zadać cios. Chcielibyśmy więc jednocześnie używać animacji biegu i od określonego miejsca w czasie użyć dodatkowo animacji machnięcia. Programiści Ogre wprowadzili dość niedawno funkcje rozwiązujące nasz problem - maski animacji. Pozostaje je dobrze wykorzystać w naszym silniku gry.Chcemy, by nasz system animacji posiadał możliwość odtwarzania różnych animacji na różnych częściach ciała, jednocześnie zapewniając płynne przejścia między stanami. Podzielimy więc szkielet postaci na dwie części: ![]()
Plan działania prezentuje się następująco:
Pisanie zaczniemy od najmniejszej z nich.
Pole StateName pozwoli nam odczytać stan animacji bieżącego obiektu do pola State . Animowany obiekt będzie zapamiętany w polu Entity . Ponadto zachowamy referencję do grupy, do której należeć będzie animacja.
Konstruktor będzie od nas wymagać podania nazwy animacji.
Metoda ta będzie wywoływana w momencie przypisania animowanego obiektu, lub też po jego zmianie w upłynniaczu.
Zanim napiszemy kolejną metodę, utwórzmy klasę
Potrzebna nam będzie lista animacji należąca do grupy ( Anims ), aktualna animacja docelowa (Target ), referencja do AnimBlender , w którym będziemy przechowywać parametry upłynniania i animacji. Istotnym elementem jest też lista nazw kości, które należą do grupy (BlendMask ).
Konstruktor wypełnia tylko niezbędne pola.
Dodając animację do grupy podajemy jej nazwę (ustaloną przy eksportowaniu szkieletu). Wypełniamy też pole referencyjne animacji AffectedGroup . Posiadając tę
informację, możemy wrócić do klasy Anim i dopisać do niej ostatnią metodę:
Będzie ona mogła być wywołana jedynie po zainicjowaniu pól Entity i AffectedGroup . Tworzymy maskę mieszania, jeżeli nie istnieje. Maska pozwala ustalić wpływ animacji na każdą z kości z osobna. Maska zostaje zainicjowana wartościami 0 (zerowy wpływ). W pętli (6) nanosimy wagę 1 (100% wpływ) na wszystkie kości z maski grupy, do której należy animacja.
Dokończmy klasę
Update() jest kluczową metodą naszego mieszacza. Podczas upłynniania animacji ważne jest, by wagi wszystkich aktywnych animacji sumowały się do jedynki. Jeżeli bowiem sumowałyby się do wartości mniejszej lub większej, przekształcenie nakładane przez animacje byłoby odpowiednio niekompletne lub nadmiarowe. Z tego powodu zapamiętujemy sumę wag "odprowadzonych" z aktywnych, nie będących docelowymi animacji. Pilnujemy, by wagi nie były ujemne (11).
Wpompowujemy wcześniej odprowadzone wagi do wagi animacji docelowej pilnując przy tym, by nie przekroczyła ona 1.
Teraz zrobimy klasę główną -
Potrzebować będziemy słowników na grupy ( Groups ), wszystkie animacje wszystkich grup (AllAnims ) i zestawy animacji (AnimSests ). Oprócz tego potrzebujemy
listę powiązań animacji (Links ). Pole DefaultAnimSetName zawierać będzie nazwę zestawu animacji, którego wagi przyjmą wartość 1 zaraz po przypisaniu obiektu Entity do upłynniacza. Jest to konieczne ze względu na założenie poczynione wcześniej - suma aktywnych wag w każdej z grup ma się sumować do 1. BlendSpeed określać będzie jak szybko odprowadzane będą wagi z aktywnych animacji.
Konstruktor tworzy potrzebne kolekcje.
Dodając grupę będziemy musieli określić jej nazwę oraz podać listę nazw kości, które ma ta grupa objąć. Słowo kluczowe params przerobi listę argumentów na zwyczajną tablicę.
Te metody uproszczą zapis rejestrowania nowych animacji i ich zbiorów w kolekcjach.
Rejestrując animacje, które chcemy ze sobą zsynchronizować podajemy jedynie ich nazwy. Metoda dodaje do zbioru powiązań listę samych referencji.
Bardzo ważna metoda. To ona łączy mieszacz z animowanym obiektem. Ustawienie w szkielecie trybu mieszania na ANIMBLEND_CUMULATIVE sprawi, że niezależne animacje w różnych grupach nie będą sobie przeszkadzać. Pierwsza pętla pobiera referncje na Ogrowy obiekt AnimationState i ustala maski. Ostatnia pętla inicjuje domyślną animację wagami 1.
Ta metoda włącza i ustawia daną animację jako docelową w swojej grupie.
Podobnie jak poprzednia metoda, lecz ustawia cały zestaw animacji jako cel (przypomnijmy, że zestaw animacji to zbiór animacji pokrywający wszystkie grupy animacji).
Za pomocą tej metody dowiemy się, jak zaawansowane jest odtwarzanie danej animacji, w skali od 0 do 1.
Metoda o znaczeniu podobnym do poprzedniej. W przypadku zestawów animacji uznajemy, że pierwsza animacja w zestawie będzie tą pozwalającą określić zaawansowanie odtwarzania. Przykładowo, dla zestawu animacji uderzania podczas biegu jako pierwszą będziemy musieli podać animację uderzania należącą do górnej grupy, dopiero potem animację biegu. Dzięki temu ustaleniu upraszczamy kod - unikniamy konieczności szukania niezapętlonej animacji w zestawie.
Ta metoda jest bardzo przydatna - szybko zresetuje cały zestaw animacji.
Ta, już ostatnia metoda aktualizuje mieszanie w każdej grupie i synchroznizuje pozycje w czasie powiązanych animacji. Uznajemy, że pierwsza z powiązanych animacji narzuca swoją pozycję. Musimy teraz skonfigurować przykładowy mieszacz dla naszego modelu postaci. Korzystamy z szybkości kompilacji C# i zamiast pisać, parsować pliki konfiguracyjne stworzymy klasę konfiguracyjną:
Tworząc maski grup ważne jest, by nie przeoczyć żadnej z kości i uważać, by żadna z nich nie wystąpiła w dwóch grupach jednocześnie.
Rejestrujemy grupy i nazwy animacji, których oczekiwać będziemy od szkieletu animowanej postaci. Ze względu na podział szkieletu na grupy tworzymy wersje animacji dla każdej z grup.
Przydzielamy animacje do odpowiednich grup.
W tej chwili interesują nas trzy zestawy. Nie korzystamy jeszcze z możliwości użycia jednej animacji w wielu zestawach
Zsynchronizujemy ze sobą animacje dla nóg i dla torsu i ustalamy domyślną animację - spoczynek.
Odwiedźmy teraz klasę
Dopisujemy pole i inicjujemy je po stworzeniu obiektu Entity .
W metodzie Update() dodajemy aktualizację mieszacza.
Wszystkie stany zmieniamy na wzór:
Porównania czasu poprawiamy wg. wzoru:
Resetowanie animacji odbywać się będzie przez metodę:
Jak widać, obsługa animacji z punktu widzenia klasy postaci stała się bardzo prosta. Animacje przełączają się płynnie - cel został osiągnięty. Rozwiązanie zdaje się sprawdzać.
![]() Nowy model i szkielet![]() Aktualny kod źródłowy![]() (2 ocen) |
Copyright © 2008-2010 Wrocławski Portal Informatyczny
design: rafalpolito.com