Jak Uniezależnić Ruch Od Ilości Klatek?
Wstęp
Liczba klatek na sekundę w grach to zawsze bardzo gorący temat. Warto jednak zauważyć, że duża ich liczba nie zawsze musi mieć pozytywny wpływ na rozgrywkę. W tym artykule omówimy, jak to wygląda z technicznego punktu widzenia i pokażemy, jak uczynić nasze funkcjonalności, niezależnymi od klatek.
Klatka
W trakcie rozgrywki gra zbiera bardzo dużo danych, które są interpretowane przez naszą jednostkę CPU1 oraz GPU2.
Przykładowa klatka z gry3
To, co widzimy na powyższym zrzucie ekranu, to stan gry, który został wyrenderowany na podstawie zebranych danych. Jest to odzwierciedlenie stanu animacji, pozycji gracza w przestrzeni, stanu interfejsu użytkownika itd. w bieżącym momencie gry. W nomenklaturze branżowej taki stan nazywamy klatką.
Płynność gry zależy od tego, jak często klatka ta będzie aktualizowana i ile zmian ta aktualizacja przyniesie. W celu ocenienia płynności gry będziemy wyliczać, jak dużo klatek nasz sprzęt jest w stanie wyrenderować w przeciągu jednej sekundy, stąd nazwa jednostki - FPS, czyli Klatki na Sekundę (Frames Per Second)
Czas pomiędzy klatkami
Ponieważ jesteśmy w stanie wyrenderować ograniczoną liczbę klatek na sekundę, możemy również wyznaczyć maksymalny czas, jaki sprzęt gracza ma na pełny render jednej z nich, tak aby końcowo liczba ta pozostała stała. Dla 30 FPS czas ten wynosi 33 ms, a dla 60 FPS – 17 ms. Jeśli dla którejkolwiek wybranej klatki wymagania te nie zostaną spełnione, może dojść do spadku lub skoku klatkarza podczas rozgrywki.
| Docelowy klatkarz | Czas w ms | Czas w sekundach |
|---|---|---|
| 30 | 33 | 0,0(3)4 |
| 60 | 17 | 0,01(6) |
| 120 | 8.33 | 0,008(3) |
Update
Zaprezentujemy działanie tego mechanizmu na przykładzie silnika Unity5. Logika byłaby podobna również w innych technologiach.
W silniku, gdy renderowana jest nowa klatka, wywoływana jest specjalna metoda Update. Jest ona dostępna w każdej klasie dziedziczącej po MonoBehaviour
1
2
3
4
5
6
7
public class Sample : MonoBehaviour
{
public void Update()
{
//ten kod będzie wykonywany za każdym razem, gdy zostanie wyrenderowana nowa klatka
}
}
Fakt, że metoda ta jest wykonywana w każdej klatce, oznacza, że częstość jej wywoływana zależna jest od mocy sprzętu. Gdy gra działa w 30FPS, ta metoda będzie wywoływana 30 razy na sekundę, ale gdy liczba klatek na sekundę wzrośnie, np. do 200, to częstotliwość wywołań wzrośnie proporcjonalnie.
Dla przykładu weźmy następujący kod.
1
2
3
4
5
6
7
public void Update()
{
if(Input.GetKey(KeyCode.D)) //Czy gracz trzyma wciśnięty klawisz 'D'?
{
transform.position += Vector3.right; //Przesuń obiekt o wektor (1,0,0)
}
}
Efekt jego działania będzie różny, w zależności od liczby klatek na sekundę, jaką może osiągnąć nasz sprzęt
Przykład takiego ruchu, gdy gra działa z szybkością około 1000 FPS
Uniezależnienie się od klatek
To, co chcemy zrobić, to stworzyć sytuację, w której niezależnie od liczby klatek, nasza postać będzie się przemieszczać o tę samą odległość w tym samym czasie. Prędkość naszego gracza jest równa Vector3.right czyli za każdym razem, gdy wywoła się Update, postać przesunie się o jedną jednostkę w prawo. Przy 60 FPS pokonamy 60 jednostek w ciągu sekundy, dla 30 FPS - 30 jednostek itd. Przeróbmy to taki sposób, aby pokonywać jedną jednostkę w ciągu sekundy.
Dystans po sekundzie wyliczymy zatem w następujący sposób
Liczba klatek na sekundę \(*\) Prędkość co klatkę
Następnie musimy zredukować prędkość, którą pokonujemy w każdej klatce do takiego poziomu, aby suma dystansów pokonanych we wszystkich klatach w ciągu sekundy wyniosła 1. Redukcja ta będzie zależna do ilości klatek. Im większy klatkarz, tym bardziej musimy spowolnić naszą postać, gdyż aktualizacja stanu będzie wykonywana częściej.
Liczba klatek na sekundę \(*\) (Prędkość co klatkę \(*\) Procent prędkości co klatkę) \(= 1\)
Do obliczeń załóżmy, że Procent prędkości co klatkę \(= x\)
30 FPS
\[30 * 1 * x = 1\] \[1 * x = \frac{1}{30}\] \[x = \frac{1}{30}\] \[x = 0,03(3)\]60 FPS
\[60 * 1 * x = 1\] \[1 * x = \frac{1}{60}\] \[x = \frac{1}{60}\] \[x = 0,01(6)\]Zatem, jeśli pomnożymy prędkość postaci w danej klatce przez czas, który upływa między klatkami, przy danej liczbie klatek na sekundę, staniemy się niezależni od klatek. Potrzebujemy zatem wartości czasu, który upłynął od wyrenderowania ostatniej klatki. Silnik przechowuje tę liczbę w zmiennej Time.deltaTime.
1
2
3
4
5
6
7
public void Update()
{
if(Input.GetKey(KeyCode.D))
{
transform.position += Vector3.right * Time.deltaTime;
}
}
Time.deltaTime przechowuje czas między klatkami, które zostały wyrenderowane przed aktualnie renderowaną klatką. Podczas jej używania zawsze jesteśmy o jedną klatkę do tyłu, co w niektórych przypadkach może być niewystarczająco precyzyjne.
Przykład ruchu, gdy gra działa z szybkością około 1000 FPS po użyciu Time.deltaTime.
Teraz, jeśli chcemy dodatkowo przyspieszyć lub spowolnić naszą postać, możemy dodać do tego równania dodatkową zmienną
1
2
3
4
5
6
7
8
9
float speed = 10f;
public void Update()
{
if(Input.GetKey(KeyCode.D))
{
transform.position += Vector3.right * Time.deltaTime * speed;
}
}
Podsumowanie
- Klatka to wyrenderowany stan naszej gry w danym momencie rozgrywki
- W silniku Unity, Update, jest metodą, która jest wykonywana za każdym razem, gdy renderowana jest nowa klatka
- Częstotliwość wykonywania metody Update jest od tego, jak dużo klatek nasz sprzęt może wyrenderować w ciągu sekundy.
- Jeśli nasz kod jest zależny od klatkarza, będzie się zachowywał inaczej w zależności od wydajności sprzętu.
- Time.deltaTime, jest zmienną przechowującą czas, jaki upłynął od wyrenderowania ostatniej klatki.
- Mnożenie liczby przez Time.deltaTime powoduje zmianę jego przyrostu z „co klatkę” na „co sekundę”.
- Zawsze powinniśmy chcieć, aby nasz kod był niezależny od liczby klatek na sekundę, z jaką działa gra
- Mnożenie przez wyżej wymienioną zmienną ma sens tylko wtedy, gdy kod jest wywoływany co klatkę. W innych przypadkach niepotrzebnie zmniejszy wartość liczby, nie dając żadnych korzyści w zamian.
1
2
3
4
5
6
7
8
9
10
11
12
public class Sample : MonoBehaviour
{
float speed = 10f;
public void Update()
{
if(Input.GetKey(KeyCode.D))
{
transform.position += Vector3.right * Time.deltaTime * speed;
}
}
}
Przypisy
Central Processing Unit ↩︎
Graphics Processing Unit ↩︎
https://www.metacritic.com/game/crash-bandicoot-4-its-about-time/ ↩︎
https://pl.wikipedia.org/wiki/Ułamek_dziesiętny_nieskończony ↩︎

Comments powered by Disqus.