12. Čas

Programovaciu sekciu uzavrieme jednou z najneintuitívnejších(*) vlastností interaktívnych aplikácií: Chápanie času.
(*) 381 bodov v Scrabble

Pohyblivý obraz, ktorý nie je interaktívny – film, video – je rozdelený na veľa po sebe idúcich jednotlivých obrázkov – políčok (frameov), ktoré keď sa prehrávajú rýchlo za sebou, vidíme to ako pohyb:

Sekvencia frejmov z filmu Sintel

V animovanom filme sú tieto frejmy už hotové (vyrenderované) a prehrávajú sa konštantnou rýchlosťou, najčastejšie 24, 25 alebo 30 snímkov za sekundu. Toto číslo sa volá aj FPS (frames per second).

To znamená, že vo videu je medzi jedným a nasledujúcim frejmom konštantný časový odstup. Napríklad pri 25 FPS je to 40milisekúnd.

Na rozdiel od animovaného filmu, v Unity sa frejmy musia renderovať za behu a prehrávajú sa premenlivou rýchlosťou, podľa toho ako rýchlo ich počítač stíha renderovať. Niektorý frejm sa vyrenderuje za 15milisekúnd, niektorý za 70milisekúnd a podobne. Časové odstupy medzi nimi teda nie sú konštantné.

A čo je horšie – nevieme ani dopredu koľko bude ktorý frejm trvať, lebo to závisí od počítača, na ktorom naša aplikácia beží. Od jeho výkonu, od toho, čo práve používateľ robí, čo ešte zamestnáva procesor na pozadí a od veľa iných faktorov.

A prečo nás to trápi?

Najlepšie si to ukážeme na príklad. V našom projekte máme otáčajúce sa červené kvádre. Tak takto to vyzerá, keď počas behu aplikácie zamestnáme procesor:

Kvádre sa otáčajú pomalšie. A to asi nechceme, že? Rýchlosť pohybu v hre, animácii či v interaktívnej aplikácii by nemala závisieť od toho, aký rýchly je procesor. Tak prečo sa nám to tu deje?

Pozrime sa na kód, ktorý nám otáča kvádrami. Je v skripte Rotator:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rotator : MonoBehaviour
{
    public float speedY;

    // Start is called before the first frame update
    void Start()
    {
        if (speedY == 0)
        {
            speedY = Random.Range(-2.0f, 2.0f);
        }            
    }

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(0, speedY, 0);
    }
}

Príkaz transform.Rotate(0, speedY, 0) nám zabezpečuje otáčanie.

V každom jednom Update nám pootočí game objekt o hodnotu speedY (napríklad o 0.5f ). A Update prebehne v každom jednom frejme, to už vieme. Koľkokrát za sekundu ale stihne počítač vykonať tento Update?

Počítač, ktorý nie je zamestnaný ničím stihne vykonať Update možno aj 300-krát za sekundu. Tým pádom sa za sekundu game objekt otočí celkovo o 300 * 0.5 = 150 stupňov.

Keď ale počítaču zamestnáme procesor, klesne FPS z 300 napríklad na 40. Takto zamestnaný počítač stihne za sekundu vykonať Update už len 40-krát a dosiahne tak celkové otočenie 40 * 0.5 = 20 stupňov.

Za takú istú dlhú sekundu sa teda kocka niekedy otočí celkovo o 150 stupňov a inokedy len o 20 stupňov. Preto sa niekedy kocka otáčala rýchlejšie a inokedy pomalšie.

Čo robiť, ak chceme, aby sa kocka otáčala vždy rovnakou rýchlosťou. Napríklad aby sa za sekundu otočila celkovo o 180 stupňov, bez ohľadu na to, aké FPS počítač stíha vykresľovať?

Nastupuje Time.deltaTime

Unity nám na toto dáva riešenie v podobe pomocnej triedy Time, ktorá nám ponúka viaceré užitočné nástroje na prácu s časom.

Najdôležitejším je pre nás vlastnosť Time.deltaTime. V nej je uložená hodnota, koľko času uplynulo od predošlého vykonania frejmu. Teda vlastne koľko času trval minulý Update. Táto hodnota sa udáva v sekundách.

Skúsme si túto hodnotu len tak pre skúšku vypisovať do konzoly v rámci Rotatora:

    void Update()
    {
        Debug.Log(Time.deltaTime);
        transform.Rotate(0, speedY, 0);
    }

Vidíme, že tieto hodnoty sa menia, nie sú konštantné. Niekedy trval Update 0.0039968 sekundy, inokedy až 0.0084032 sekundy.

Ak by sme chceli, aby za 1 sekundu urobila kocka celkové otočenie o 180 stupňov, tak nám stačí v každom jednom Update otočiť kocku o takéto číslo: 180 * Time.deltaTime.

Skúška správnosti? Predstavime si, že sa nám stalo niečo katastrofálne s počítačom a spomalil sa až tak, že vykreslenie 1 frejmu trvalo 1 sekundu. Vtedy bude v Time.deltaTime hodnota 1. Výraz 180 * Time.deltaTime sa potom rovná 180 * 1, čiže 180. A to presne chceme, za 1 sekundu otáčku o 180 stupňov.

Ak by bol počítač trošku rýchlejší a trvá mu prvý frejm 0.4 sekundy a druhý frejm mu trvá 0.6 sekundy, tak v prvom frejme sa kocka pootočí o 180 * 0.4 = 72 stupňov. V druhom frejme sa pootočí o 180 * 0.6 = 108 stupňov. V súčte sa teda otočí o 72 + 108 = 180 stupňov.

A takto nech máme za 1 sekundu hocikoľko hocijako dlhých frejmov, vždy po 1 sekunde bude kocka otočená o 180 stupňov.

Vyskúšajme:

    void Update()
    {
        transform.Rotate(0, 180 * Time.deltaTime, 0);
    }

Čo sa stane teraz ak si počas chodu aplikácie zamestnám procesor niečím iným:

Namiesto spomalenia je badateľné len drobné trhnutie (a potom ešte jeden väčší skok, ale to je preto že toto video na stránke nie je dokonalá slučka). Tomu sa nedá vyhnúť, s tým treba žiť. Hocijako dobre naprogramovaná aplikácia sa občas sekne. Ale okrem toho nie je v takto naprogramovanej rotácii možné rozoznať kedy beží aplikácia na 300 FPS a kedy na 40 FPS.

Rovnako to funguje pri hocijakom inom pohybe, nie len pri rotácii. Určíme si aký celkový pokrok chceme dosiahnuť za 1 sekundu a toto číslo násobíme krát Time.deltaTime.

Ale prišli sme o reguláciu rýchlosti cez parameter speedY. Keby sme ho teraz menili, nijak sa nezmení rýchlosť otáčania. Vždy to bude 180 stupňov za sekundu.

Jednoduchá pomoc, pridáme do výrazu 180 * Time.deltaTime naspať aj speedY:

    void Update()
    {
        transform.Rotate(0, 180 * speedY * Time.deltaTime, 0);
    }

S takto upraveným skriptom už môžem na kocke reguláciou parametra Speed Y v inšpektore meniť rýchlosť otáčania. Ak napríklad nastavím Speed Y na hodnotu 2, bude to znamenať, že za sekundu urobí kocka otáčku o 180 * speedY = 360 stupňov.

Konštanta 180 mi teda určuje základnú rýchlosť – koľko stupňov sa otočí kocka za sekundu bez speedY. A vlastnosťou speedY násobíme túto základnú rýchlosť.

Čo nám ešte Time ponúka?

Trieda Time má veľa rôznych vlastností, niektoré sú dosť exotické, ale niektoré majú častejšie využitie.

Napríklad vlastnosť Time.timeScale nám umožňuje meniť rýchlosť plynutia času. Ak túto vlastnosť nastavíme napríklad na 0.5f, dianie v hre pobeží polovičnou rýchosťou. Ak ju nastavíme na 2, dianie pobeží dvojnásobnou rýchlosťou. A keď ju nastavíme na 0, dianie v hre sa zastaví úplne. Ovládneme Matrix.

Toto je užitočné keď chceme napríklad otvoriť v hre nejaké menu a nechceme, aby sa počas toho hra na pozadí pohybovala ďalej. Alebo chceme pauznúť hru a dať postavičkám nové inštrukcie.

Dostaneme sa k tomu v budúcnosti, keď budeme pracovať s GUI.

A tu je kompletná dokumentácia k triede Time, keby ste mali záujem o hlbšie štúdium.

Zhrnutie

Chod interaktívnej aplikácie je rozdelený na jednotlivé snímky, frejmy. Podobne ako film je rozdelené na jednotlivé políčka. Ale u filmu sa políčka vykresľujú rovnako rýchlo, film má konštantné FPS.

Na rozdiel od filmu tieto frejmy v aplikácii trvajú každý inak dlho. Aplikácia má premenlivé FPS.

Preto potrebujeme pohyb v metóde Update násobiť hodnotou Time.deltaTime, ktorá zabezpečí, aby sa pohyb vykonával rovnako rýchlo bez ohľadu na FPS.

Hotový projekt po tejto lekcii: Programovanie_12.zip