rozwiązania dla gier realtime 14 maja 2014
Problemy 1 Utrzymanie płynności gry 2 Różny charakter obiektów gry (budynek, gracz, system osiągnięć) 3 Rózne akcje wykonywane na poszczególnych obiektach (animowanie, wybieranie przeciwnika) 4 Synchronizacja ze światem zewnętrznym (użytkownik, inne instancje gry) 5 Sprawiedliwe aktualizowanie wszystkich obiektów (niezależnie od obciążenia) 6 Utrzymanie kolejności aktualizowania obiektów
Najprostsze rozwiązania while (1) { foreach ( GameObject & go, gameobjects ) go. update (); } Zależne od sprzętu Zależne od obciążenia Brak podziału wykonywanych akcji według częstotliwości
Najprostsze rozwiązania while (1) { wait ( lastupdate + INTERVAL - time ()); lastupdate = time (); } foreach ( GameObject & go, gameobjects ) go. update (); Zależne od obciążenia Brak podziału wykonywanych akcji według częstotliwości
Zmienny czas wywołania while (1) { foreach ( GameObject & go, gameobjects ) go. update ( time () - lastupdate ); lastupdate = time (); } Niezależne od obciążenia lub sprzętu 100% wykorzystanie procesora Konieczność implementacji obsługi czasu wewnątrz update() Dobre przy liczeniu fizyki i animacji - brak lagów
Wiele niezależnych pętli gry while (1) { wait ( lastupdate1 + INTERVAL - time ()); lastupdate1 = time (); foreach ( GameObject & go, gameobjects ) go. update11 (); foreach ( GameObject & go, gameobjects ) go. update12 (); if ( lastupdate2 + INTERVAL2 > time ()){ lastupdate2 = time (); } } foreach ( GameObject & go, gameobjects ) go. update2 (); Ponownie - zależność od obciążenia Możliwość rzadszego wykonywania ciężkich akcji
Rozwiązanie Ale w końcu mamy Qt i sensowne timery.
Rozwiązanie Ale w końcu mamy Qt i sensowne timery. Niezależność od sprzętu Pasuje do kodu sterowanego zdarzeniami Częściowa niezależność od obciążenia Możliwość updateowania różnych rzeczy co różny czas Dodatkowe nakłady na timery (wywoływane systemowo) Możliwość wykonywania ciężkich działań gdy procesor jest wolny (QtConcurrent) Brak kontroli nad kolejnością wykonywania działań
Predykcja zachowań Co zrobić kiedy obiektów gry jest zbyt dużo i nie nadążamy z ich aktualizacją? Udawać że aktualizujemy Koniecznie aktualizować grafikę Aktualizować fizykę (a przynajmniej część, np bez detekcji kolizji) Pomijać część informacji o zmianach, być może nie są już potrzebne (małe bufory na zdarzenia jednego typu)
Update() Do tej pory skupialiśmy się na samej strukturze pętli gry. Trzeba jednak zintegrować ją z odpowiednią implementacją obiektów. Możliwe podejścia:
Update() Do tej pory skupialiśmy się na samej strukturze pętli gry. Trzeba jednak zintegrować ją z odpowiednią implementacją obiektów. Możliwe podejścia: Obiekty zawierają tylko dane, aktualizuje je główna funkcja w pętli gry. Każda klasa opisująca jakiś obiekt zawiera osobną implementację update() Dziedziczenie funkcji update(). Być może wielodziedziczenie Strategie (Brainzzzz) Strategia na dekoratorach Animatory
Dziedziczenie update() 1 Problemy z wielodziedziczeniem 2 Możliwe nieintuicyjne dziedziczenia (budynek - statek matka) 3 Spora nadmiarowość kodu - niektóre akcje mogą mieć duże części wspólne 4 Długie ścieżki dziedziczenia - problemy w utrzymaniu kodu 5 Mnożenie liczby klas posiadających te same atrybuty przez niewielkie różnice w zachowaniu (żołnierz, żołnierz z super umiejętnościami)
Strategie - Brainzzzz Każdy obiekt posiada wskaźnik na swój mózg - obiekt opisujący działanie głównego obiektu 1 Możliwość oddzielenia opisu atrybutów od logiki 2 Znowu - dużo klas i wielodziedziczenie 3 Redundancja kodu 4 Możliwość zmiany zachowań obiektu w trakcie gry (uśpiony guardian, patrolujący, ścigający)
Dekoratory Sposób na zastąpienie dziedziczenia, można użyć zarówno na samym obiekcie jak i na jego mózgu.
Wady i zalety 1 Możliwość tworzenia dowolnych kombinacji obiekt - zachowanie 2 Brak dziedziczenia 3 Mała redundancja kodu 4 Brak sposobu na łatwe podzielenie akcji według okresu wykonywania 5 Możliwość pomyłki przy kolejności inicjowania dekoratora (złe zagnieżdżenie)
Animatory - pochodzenie Niektóre silniki, takie jak Irrlicht wykorzystują Animatory, rozwiązanie pozwalające szybko dodawać logikę typowych zachowań do dowolnego obiektu gry.
Animatory - pochodzenie Niektóre silniki, takie jak Irrlicht wykorzystują Animatory, rozwiązanie pozwalające szybko dodawać logikę typowych zachowań do dowolnego obiektu gry. 1 Animowanie kolejnych klatek 2 Zatrzymywanie się przy zderzeniu 3 Wchodzenie na niewielkie podwyższenia 4 Grawitacja Dodawanie animatora jest realizowane przez wywołanie metody silnika samej gry, na węźle danego obiektu.
Animatory - podstawowe założenia 1 Obiekty gry jedynie przechowują dane - nie zawierają logiki zachowań 2 Animatory są częścią silnika, nie obiektów 3 Każdy animator może działać na potencjalnie dowolnym obiekcie gry 4 Każdy obiekt może być obsługiwany przez wiele animatorów 5 Animatory mogą mieć różny czas aktualizacji 6 Animatory mają łatwy dostęp do danych zawartych w silniku (Gameplay) 7 Każdy animator dba o kolejność aktualizowanych przez siebie obiektów
Animatory - architektura Każdy animator posiada częstotliwość aktualizacji, priorytet, listę aktualizowanych obiektów i wskaźnik do Gameplay Gameplay inicjalizuje wszystkie animatory Dla każdego występującego czasu aktualizacji tworzy osobną warstwę Warstwa składa się z posortowanych po priorytecie animatorów i timera o odpowiednim okresie aktualizacji Timer co określony czas wywołuje update() wszystkich animatorów należących do danej warstwy Update() animatora polega na wykonaniu tej samej akcji na wszystkich obiektach z listy, zawsze w tej samej kolejności Animatory mogą korzystać ze swoich wyników zapisywanych w mapie QString - QVariant
Animatory - zady i walety Relatywnie mało timerów Dowolne mieszanie obiektów z zachowaniami Dopuszczalne różne czasy aktualizacji Stała kolejność aktualizowania obiektów Bezpieczeństwo wywołania animatorów gwarantowane globalnie, nie osobno dla każdego obiektu Krótki kod dodający nowy obiekt o typowym zachowaniu Brak pewności czy obiekt posiada parametry wymagane przez dany animator Niewygodne wczytywanie obiektów - konieczność niezależnego pamiętania animatorów dla każdego obiektu Łatwość debugowania - można dowolny, mały fragment działania obiektów testować niezależnie Brak redundancji kodu
Skryptowanie - Po co? Czemu dobrze jest skryptować fragmenty logiki gry? Brak konieczności rekompilacji przy niewielkich zmianach Możliwość szybszej pracy przy szlifowaniu zachowań i sprawdzaniu różnych wariantów Mniejsze wymagania dot. osoby pracującej nad logiką Ułatwia pisania kodu łatwego w dostosowaniu do innych projektów
Skryptowanie -Jak to robić poprawnie Skryptować jedynie najwyższą warstwę logiki Skrypty powinny być możliwie proste Skomplikowane fragmenty trzymać w kodzie - są od razu sprawdzane przez kompilator, są szybsze Skryptowane powinny być fragmenty, które wynikają z mechaniki opisywalnej graczowi Przekazywać minimum danych, z maksymalną wartością
Skryptowanie -Bridge Pattern Pozwala na niezależne łączenie rodzajów obiektów ze sposobami implementacji. Przykład: Animatory, Warunki zwycięstwa, Menu HUD Przykład Inkscape: Narzędzia rysowania, konwertery, filtry
Skryptowanie w Qt QScriptEngine engine ; QObject * someobject = new MyObject ; QScriptValue objectvalue = engine. newqobject ( someobject ); engine. globalobject (). setproperty (" myobject ", objectvalue ); engine. globalobject (). setproperty (" foo ", 123) ; qdebug () << engine. evaluate (" foo * 2 + myobject. scale "). tonumber ();
Skryptowanie w Qt QScriptEngine engine ; QObject * someobject = new MyObject ; QScriptValue objectvalue = engine. newqobject ( someobject ); engine. globalobject (). setproperty (" myobject ", objectvalue ); engine. globalobject (). setproperty (" foo ", 123) ; qdebug () << engine. evaluate (" foo * 2 + myobject. scale "). tonumber (); Connect: QScriptEngine eng ; QLineEdit * edit = new QLineEdit (...) ; QScriptValue handler = eng. evaluate ( "( function ( text ) { print ( text was changed to, text ); })"); q ScriptConnect ( edit, SIGNAL ( textchanged ( const QString &)), QScriptValue (), handler );
Dziękuję za uwagę.