[powrót]
Implementacja animacji jest prosta: wystarczy nieustająco modyfikować
stan animacji (zmienne w rodzaju przesunięcie_obiektu
lub kąt_obrotu_obiektu
) i nieustająco przerysowywać ekran.
Używając gluta:
Zazwyczaj należy robić w idle
(funkcji zarejestrowanej
przez glutIdleFunc
) instrukcje jak
przesunięcie_obiektu += 0.5; kąt_obrotu_obiektu += 0.5; glutPostRedisplay();
Wywołujemy glutPostRedisplay()
aby wymusić
wywołanie display
w najbliższym czasie. display
używa zmiennych jak przesunięcie_obiektu
i
kąt_obrotu_obiektu
aby narysować konkretką klatkę animacji.
W prostych przypadkach można też po prostu wykonać
te instrukcje pod koniec każdego display
.
Używając SDLa: jeśli sami piszemy pętlę programu, mamy kod w rodzaju
while ( !done ) { while ( SDL_PollEvent( &event ) ) { ... obsłuż zdarzenie event ... } }Zmienianie stanu animacji możemy zrobić w każdym przebiegu pętli, np.
while ( !done ) { while ( SDL_PollEvent( &event ) ) { ... obsłuż zdarzenie event ... } /* Zmień stan animacji */ przesunięcie_obiektu += 0.5; kąt_obrotu_obiektu += 0.5; /* Narysuj nową klatkę animacji */ display(); }Ponieważ wiemy że w każdym kroku pętli modyfikujemy stan animacji, możemy explicite w każdym kroku pętli wywołać
display()
(zamiast czekać na zdarzenie SDL_VIDEOEXPOSE
).
Alternatywnie, jeżeli lubimy czekać na SDL_VIDEOEXPOSE
,
to możemy zażądać wywołania SDL_VIDEOEXPOSE
w najbliższym
czasie przez wywołanie
{ SDL_Event event; event.type = SDL_VIDEOEXPOSE; SDL_PushEvent(&event); }Powyższy kod to odpowiednik
glutPostRedidplay()
z gluta.
Oczywiście powyższe kawałki kodu to tylko przykłady najprostszego i najbardziej typowego podejścia. Te przykłądy będą wymagać odpowiednich modyfikacji w różnych przypadkach, np. nie zawsze animacja działa w czasie całego programu: 1. użytkownik rzuca piłeczką 2. piłeczka leci, robimy animację 3. piłeczka upada, koniec animacji.
Problem polega na tym że tak jest źle. A dokładniej: problem polega na tym że animacja działa źle na prawie wszystkich komputerach, za wyjątkiem komputera na którym testowaliście program. Szybkość działania OpenGLa jest drastycznie różna na różnych komputerach — różne karty graficzne, różna implementacja OpenGLa itd. Na szybkim komputerze instrukcje
przesunięcie_obiektu += 0.5; kąt_obrotu_obiektu += 0.5;
mogą być wywoływane 100 razy na sekundę, na wolnym komputerze tylko
10 razy na sekundę. Wszystko przez to że szybki komputer szybko wykonuje
procedurę display
(która zazwyczaj jest najbardziej czasożerną
częścią programu). Czyli pętla programu działa szybciej,
i procedura w stylu idle
jest wywoływana znacznie częściej.
W rezultacie instrukcja przesunięcie_obiektu += 0.5;
jest wywoływana na szybszym komputerze
10 razy częściej na sekundę, więc odpowiedni
obiekt na ekranie porusza się szybciej.
Efekt: jeżeli dostosowaliście stałą o jaką w każdym kroku
zmieniacie przesunięcie_obiektu
(0.5
w naszych
przykładach) do swojego komputera, a potem uruchomicie ten sam program
na 10 razy szybszym komputerze, to stwierdzicie że obiekt porusza
się 10 razy szybciej. Lub 10 razy wolniej na 10 razy bardziej wolnym
komputerze.
Jeden sposób obejścia tego problemu to napisanie pętli programu w stylu
while (!done) { delay(10); ... zmień stan animacji ... ... obsłuż zdarzenia, czyli nasze "while ( SDL_PollEvent( &event ) ) ..." ... ... narysuj kolejną klatkę animacji, czyli nasze "display()" ... }
Gdzie funkcja delay
po prostu zawiesza program na 10
milisekund. To jest dobre rozwiązanie jeżeli wszystkie instrukcje
zabierają niezauważalnie mało czasu w porównaniu z delay(10)
.
Wtedy każdy krok pętli wykonuje się co 10 milisekund, czyli nasza
instrukcja przesunięcie_obiektu += 0.5;
wykonuje się na każdym
komputerze około 100 razy na sekundę, więc obiekt w ciągu sekundy
zawsze przesuwa się o 100 * 0.5 = 50 jednostek.
To było dobre rozwiązanie w starych dobrych czasach kiedy wszyscy byliśmy jeszcze w szkole podstawowej, mieliśmy nasze PC-ty z DOSem i programowaliśmy gry tekstowe, gdzie największym osiągnięciem animacyjnym było "przesuń literkę "D" symbolizującą smoka na środek ekranu, żeby mogła zjeść literkę "@"".
Nasz problem polega na tym że display
to ciąg wywołań OpenGLa
które zajmują dość sporo czasu. Musielibyśmy zrobić delay(X)
z naprawdę dużym parametrem X
żeby czas display
zabrał
"niezauważalnie mało czasu w porównaniu z delay(X)
".
Na przykład delay(100)
. Tylko że delay(100)
oznacza
że program zasypia na 1/10 sekundy. Więc nasz program działa na każdym
komputerze (nawet bardzo szybkim) z marną szybkością maksymalnie 10 klatek
na sekundę (frames per second, FPS).
Ujmując problem bardziej intuicyjnie: my nie możemy sobie pozwolić na zawieszenie programu na X milisekund: nasza gra 3D naprawdę potrzebuje tyle czasu ile tylko procesor jest w stanie jej dać. Rozwiązania które niepotrzebnie zawieszają nasz program są niedopuszczalne.
Podsumowując, chcemy mieć animację która
Jeżeli komputer jest szybki, to animacja bedzie bardziej "gładka". Na wolnym komputerze będzie bardziej "kanciasta". Na każdym komputerze użytkownik zobaczy że obiekt w ciągu sekundy przesunął się o 50 jednostek, ale na szybkim komputerze będzie to pokazane za pomocą 100 klatek animacji a na wolnym komputerze za pomocą jedynie 10 klatek.
Czyli musimy pozwolić na wywoływanie display
tak szybko
jak to tylko możliwe. Chcąc mieć gładką animację musimy też
zmieniać stan animacji tak często jak to jest możliwe.
Stąd np. rozwiązanie z wrzuceniem instrukcji przesunięcie_obiektu += 0.5;
do środką procedury timer
zarejestrowanej przez glutTimerFunc
lub SDL_AddTimer
nie jest dobre — na szybkim komputerze
powodowałoby że animacja jest bardziej kanciasta niż potrzeba.
Rozwiązanie: na początku idle
oblicz ilość czasu jaki upłynął
od ostatniego wywołania idle
. Przeskaluj wszystkie zmiany
współczynników przez tą ilość. Przykład:
/* Statyczna zmienna w programie */ Uint32 last_idle_time; ... /* Gdzieś w main() ... */ /* Zainicjuj last_idle_time */ last_idle_time = SDL_GetTicks(); /* Pętla zdarzeń */ while ( !done ) { while ( SDL_PollEvent( &event ) ) { ... obsłuż zdarzenie event ... } /* Zmień stan animacji */ { Uint32 time_now = SDL_GetTicks(); przesunięcie_obiektu += 50.0 * (time_now - last_idle_time) / 1000.0; last_idle_time = time_now; } /* Narysuj nową klatkę animacji */ display(); }
SDL_GetTicks podaje czas w milisekundach, stąd
(last_idle_time - time_now) / 1000.0
to ilość sekund
(zmiennoprzecinkowa, więc być może np. 0.5 sekundy) jaka upłynęła
od ostatniej zmiany. Stąd wiem że przesunięcie_obiektu
zmieni się o 50 jednostek w ciągu 1 sekundy — na każdym komputerze.
Kilka uwag:
last_idle_time
zazwyczaj należy włożyć do środka funkcji idle
.
Zamiast SDL_GetTicks
trzeba użyć innej funkcji
która mierzy upływ czasu z dużą dokładnością (tzn. co najmniej
do milisekund).
Za pomocą getimeofday można napisać funkcję która zwraca czas z dokładnością do milisekund:
unsigned int get_ticks() { struct timeval now; gettimeofday(&now, NULL); return (now.tv_sec) * 1000 + (now.tv_usec) / 1000; }
Taki czas się "przewija" co około 49 dni, dla prostoty możemy
to zignorować. Moglibyśmy też nieco ulepszyć get_ticks
,
tak żeby działało dobrze o ile tylko nasz program będzie działał
krócej niż 49 dni, i będzie zwracać czas w milisekundach od uruchomienia
programu:
struct timeval start; unsigned int get_ticks_improved() { struct timeval now; gettimeofday(&now, NULL); return (now.tv_sec - start.tv_sec) * 1000 + (now.tv_usec - start.tv_usec) / 1000; } /* ... i gdzieś na początku programu zainicjuj start: */ gettimeofday(&start, NULL);
Mamy też clock.
Mierzy czas w jednostkach CLOCKS_PER_SEC
. Samo
CLOCKS_PER_SEC
jest dość duże pod Linuxem, ale to jeszcze
nie oznacza że zegar ma dobrą precyzję...
zdarza się że ma precyzję
np. tylko 100 razy na sekundę, a to zbyt mało (jeżeli
nasz program będzie produkował więcej niż 100 klatek na sekundę,
co jest całkiem możliwe,
time_now - last_idle_time
będzie wynosiło zazwyczaj zero...).
Pod Windowsem jest GetTickCount
, QueryPerformanceCounter
i clock
też jest.
QueryPerformanceCounter
ma generalnie najlepszą dokładność.
W praktyce, dla prostoty będzie wystarczające używać
GetTickCount
— zwraca czas w milisekundach.
Czyli działa tak jak pierwsza wersja get_ticks
dla Unixów
podana wyzej (jeżeli nam na tym zależy, to możemy użyć analogicznej
sztuczki ze zmienną start
jak dla Unixów żeby ulepszyć
działanie tej funkcji).
Nie jestem pewien jak jest z clock
pod Windowsem,
chyba CLOCKS_PER_SEC
wynosi 1000 i clock
jest
w praktyce równowazne z GetTickCount
. Czyli clock
też powinno być OK.
Sama implementacja SDL_GetTicks
w SDLu wykorzystuje
mniej więcej pomysły rzucone powyżej (gettimeofday, QueryPerformanceCounter,
GetTickCount).
Języki programowania inne niż C mogą oferować inne, przenośne funkcje do pomiaru czasu. Np. w Pythonie
timeNow = datetime.datetime.now();
Zwracam uwagę że do time-based animation wszystkie zmienne do animacji zawsze muszą być floatami. Nie można tutaj wykręcić się liczbami całkowitymi.
Kiedy mamy dodawanie, np.
przesunięcie_obiektu += coś_tam;to poprawa jest prosta: musimy przeskalować
coś_tam
:
przesunięcie_obiektu += coś_tam * ilość_czasu_jaki_upłynął;
Co jeśli w każdym kroku chcieliśmy mnożyć ? Np.
przesunięcie_obiektu *= coś_tam;Aby zamienić to na time-based animation nie możemy teraz po prostu pomnożyć
coś_tam
przez
ilość_czasu_jaki_upłynął
. Przecież jeżeli jeden komputer
jest dwa razy szybszy od drugiego, to wykona dwa razy mnożenie
przez coś_tam
w czasie kiedy tamten pomnożył przez
coś_tam
tylko raz. Musimy to zrównoważyć przez
spierwiastkowanie (czyli podniesienie do potęgi 1/2) na szybszym
komputerze. Czyli musi być potęgowanie z wykładnikiem proporcjonalnym
do czasu jaki upłynął:
przesunięcie_obiektu *= Power(coś_tam, ilość_czasu_jaki_upłynął)
Niektórzy programiści mierzą czas jaki zajęło ostatnie wywołanie
funkcji display
zamiast czasu jaki upłynął od
ostatniego wywołania idle
. Jest to trochę mniej precyzyjne
(ponieważ zakładamy że cała reszta (obsługa zdarzeń programu,
wywołania idle
itd.) nigdy nie zajmie znaczącej ilości
czasu), więc lepiej nie używajcie tego.
Jedyna zaletą takiego pomiaru
to że można go użyć aby zmierzyć szybkość działania funkcji
display
i wypisać użytkownikowi "jakie byłoby FramesPerSecond
gdyby program wykonywał jedynie wywołania display
",
tym samym mierząc szybkość renderowania w oderwaniu od szybkości
innych operacji (np. szybkości sztucznej inteligencji potworków
w grze).
time_based_example.c. Po uruchomieniu zobaczycie że żółty prostokąt przesuwa się o długość białej kreski w czasie dokładnie 1 sekundy. Bez względu na to na jak szybkim komputerze go uruchomimy. Można sprawdzać z zegarkiem.