Unity3d QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity3d!

Dzisiejszy odcinek: Optymalizacja gry, część pierwsza: Skrypty

Uwaga! Jest to poradnik typu QuickTip. Zatem skupia się on na osiągnięciu założonego celu. Zatem zakładamy że użytkownik zna na tyle program Unity3d, aby samodzielnie wykonać najprostsze czynności, jak np. dodanie modelu kostki do sceny czy dodanie modelowi jakiegoś komponentu. Jeżeli brakuje Ci tej podstawowej wiedzy, zapraszam do tutoriala:
Unity Tutorial – Podstawy

Teoria

Optymalizacja gry, to bardzo obszerny temat. Część rzeczy, można opisać jednym zdaniem, ale inne wymagają kilku słów komentarza. Przez to, że sposobów optymalizacji są setki, całość dzielę na części i będę podawał po kilka metod na część. Gdybym chciał zgromadzić całość w jednym miejscu, pewnie wiele rzeczy bym pominął, a tak zawsze mogę dopisać kolejną część.

Spis treści, poradników o optymalizacji

  • QuickTip 31: Optymalizacja gry, part I: Skrypty

Metody optymalizacji

Object Pooling

Jedna z lepszych metod optymalizacji. Używanie funkcji Instantinate i Destroy jest dość zasobożerne. Jeżeli tej parki mocno nadużywamy, spadki na prędkości mogą być ogromne. W takim przypadku z pomocą przychodzi ObjectPooling. Czyli zamiast, w kółko tworzyć i usuwać nowe instancję jakiegoś obiektu, mamy pewien zbiór (basen od pool) obiektów, którymi w kółko operujemy. Taką metodę można wykorzystać np. przy toworzeniu pocisków, lub gdy przeciwnicy są generowani losowo – efekt jej stosowania można zaobserwować w niektórych grach, gdzie gracze wpadają do jaskini pełnej zwłok NPCów. Twórcy zwyczajnie chowają gdzieś ciało, do ponownego wykorzystania, zamiast je całkowicie usuwać z gry i potem generować model od nowa.

Idę stojącą za tą metodą, pozwoliłem sobie zilustrować obrazkiem:

Object Pooling
Object Pooling

Zamiast usuwać ostatni pocisk i tworzyć nowy przy broni, ten ostatni chowamy, przenosimy na początek i wystrzeliwujemy. Zaprogramowane takiego rozwiązania jest bardziej skomplikowane i przyniesie więcej linijek kodu. Ale w ogólnym rozrachunku komputer mocno sobie odsapnie. Oczywiście są sytuację, kiedy ta metoda się nie sprawdzi. Np. gdy nasi przeciwnicy są nawet w dużej liczbie, ale wraz z progresem gry znikają i nie pojawiają się więcej.

Własne uproszczone metody

Unity udostępnia masę genialnych rozwiązań i funkcji. Przykładowo dostajemy do dyspozycji wiele gotowych kontrolerów postaci na różne platformy, które mocno chwalę. Jednak może się okazać, że gdy chcemy aby nasza postać tylko biegała w lewo i prawo, to optymalnie będzie napisać swój kontroler. Te gotowe skrypty są bardzo uniwersalne, więc mają wiele dodatkowego kodu, który cały czas się wykonuje. Napisanie swojego skryptu, który może i będzie gorszy jakościowo, ale wykonującego tylko wymagane funkcję, może nam zaoszczędzić kilka cykli procesora. Taka metoda, może być bardzo dobra w przypadku gier na urządzenia mobilne, gdzie każda linia kodu się liczy.

Ograniczenie funkcji

Czasami zdarza się, że mamy rozbudowane funkcję, które wykonujemy. Jednak, mogą zdarzyć się okoliczności, które sprawiają, że danej funkcji nie trzeba wykonać. Wtedy… nie wykonujemy jej. Przykładowo:

function Update () {

    if (Vector3.Distance(transform.position, target.position) > 100) {
        return;
    }
    // Tutaj kod, który ma się wykonać
}

Co nam to daje? Jeśli podstawowy warunek nie jest spełniony, olewamy resztę kodu. Im mniej kodu się wykonuje, tym lepiej dla nas.

Zapamiętuj referencję

W grach najczęściej mamy do czynienia z wieloma obiektami, które nawiązują ze sobą interakcję. Bardzo często zdarza się, że jakiś obiekt z innym nawiązuje reakcję bardzo często. Wtedy zamiast każdorazowo “szukać” tego obiektu, to zapisujemy sobie jednorazowo referencję do niego. To samo dotyczy odwoływania się do komponentów. Przykładowo zamiast:

float speed = 5.0f;
 
void Update () {
    transform.LookAt(GameObject.FindWithTag("Player").transform);
    transform.position += transform.forward * speed * Time.deltaTime;
}

Można zrobić tak:

float speed = 5.0f;
 
private Transform myTransform;
private Transform playerTransform;
 
void Start () {
    myTransform = transform;
    playerTransform = GameObject.FindWithTag("Player").transform;
}
 
void Update () {
    myTransform.LookAt(playerTransform);
    myTransform.position += myTransform.forward * speed * Time.deltaTime;
}

Rezygnują z każdorazowego wyszukiwania komponentu/obiektu, zyskujemy na pracy procesora.

Używaj niedynamicznych tablic

Jako dynamiczne tablice rozumiem wszystkie ArrayListy, Stosy, Hashmapy. Klasyczna tablica, jest cięższa w obsłudze, bo trzeba znać jej rozmiar, ale korzystanie ze zwykłej tablicy, jest szybsze – technicznie.

Unikaj kosztownych funkcji i obliczeń matematycznych

To jest dość proste, kosztowne funkcję, to np. rzucanie promienia (RayCast), staramy się go stosować jak najmniej i wykorzystywać tylko, gdy to konieczne. Np. po kliknięciu przycisku – sprawdzanym ifem, zamiast cały czas w funkcji Update.

Istnieje też wiele innych wbudowanych funkcji, które niby mają nam pomóc, jednak jeśli chodzi o optymalizację kodu, to tylko przeszkadzają. Przykładowo wektory mają funkcję normalizującą Normalize. Tyle, że korzystniej jest samodzielnie dokonać normalizacji.  Równie kosztowne są funkcję matematyczne jak Math.Pow. Jeśli potrzebujesz potęgi drugiego stopnia, lepiej wykonać x*x, niż Math.Pow(x, 2).

 

Nie rozdrabniaj się

Często stajemy przed dylematem, jeden skrypt czy wiele skryptów. Ogólnie powinniśmy się kierować logiką. Dzielić całość na moduły, które można ponownie wykorzystywać, ale tworzyć je możliwie duże. Komunikacja wewnątrz skryptu jest szybsza, niż pomiędzy skryptami. Jednak nie można tutaj popadać w paranoję i tworzyć np. jednego skryptu Player.cs w który wsadzamy wszystko co z nim związane. Lepiej będzie sobie rozbić to na np.: Inventory.cs, PlayerStats.cs itp. Te moduły będą się ze sobą komunikować rzadko. Więc nie ma potrzeby ich łączyć, a do tego gdy są niezależne można np. inwentarz szybko przenieść do innego projektu.