10 najczęstszych błędów popełnianych przez twórców Unity

Opublikowany: 2022-03-11

Unity to świetne i proste narzędzie do tworzenia na wielu platformach. Jego zasady są łatwe do zrozumienia i możesz intuicyjnie zacząć tworzyć swoje produkty. Jeśli jednak nie weźmiesz pod uwagę pewnych rzeczy, spowolnią one Twoje postępy, gdy przejdziesz do następnego poziomu, ponieważ przechodzisz z początkowej fazy prototypu lub zbliżasz się do ostatecznego wydania. W tym artykule znajdziesz porady, jak przezwyciężyć najczęstsze problemy i jak uniknąć fundamentalnych błędów w nowych lub istniejących projektach. Należy pamiętać, że perspektywa tego artykułu koncentruje się bardziej na tworzeniu aplikacji 3D, ale wszystko, co wspomniano, dotyczy również tworzenia aplikacji 2D.

Unity to świetne i proste narzędzie do tworzenia na wielu platformach.
Ćwierkać

Powszechny błąd jedności nr 1: niedoszacowanie fazy planowania projektu

W przypadku każdego projektu ważne jest, aby ustalić kilka rzeczy, zanim zacznie się projektowanie aplikacji i część programistyczna projektu. W dzisiejszych czasach, kiedy marketing produktu jest ważną częścią całego procesu, ważne jest również, aby mieć jasne wyobrażenie o tym, jaki będzie model biznesowy wdrożonej aplikacji. Musisz być pewien, na jakie platformy będziesz wypuszczać produkt i jakie platformy są w Twoim planie. Konieczne jest również określenie minimalnych specyfikacji obsługiwanych urządzeń (czy będziesz obsługiwać starsze urządzenia z niższej półki, czy tylko nowsze modele?), aby mieć wyobrażenie, na jaką wydajność i grafikę możesz sobie pozwolić. Ten fakt ma wpływ na każdy temat w tym artykule.

Z bardziej technicznego punktu widzenia, powinno być konieczne zaplanowanie z wyprzedzeniem całego przepływu pracy tworzenia zasobów i modeli podczas udostępniania ich programiście, ze szczególnym uwzględnieniem procesu iteracji, gdy modele będą wymagały dalszych zmian i udoskonaleń. Powinieneś mieć jasne wyobrażenie o pożądanej liczbie klatek na sekundę i budżecie wierzchołków, aby artysta 3D wiedział, w jakiej maksymalnej rozdzielczości muszą być modele i ile zmian LOD musi wykonać. Należy również określić, w jaki sposób ujednolicić wszystkie pomiary, aby uzyskać spójną skalę i proces importu w całej aplikacji.

Sposób zaprojektowania poziomów ma kluczowe znaczenie dla przyszłej pracy, ponieważ podział poziomu ma duży wpływ na wydajność. Projektując nowe poziomy, zawsze musisz mieć na uwadze problemy z wydajnością. Nie idź z nierealistycznymi wizjami. Zawsze ważne jest, aby zadać sobie pytanie „czy można to rozsądnie osiągnąć?” Jeśli nie, nie powinieneś marnować cennych zasobów na coś, co jest trudne do osiągnięcia (oczywiście w przypadku, gdy nie jest częścią Twojej strategii biznesowej, aby mieć to jako główną przewagę konkurencyjną).

Powszechny błąd jedności nr 2: praca z niezoptymalizowanymi modelami

Bardzo ważne jest, aby wszystkie modele były dobrze przygotowane, aby móc używać ich w scenach bez dalszych modyfikacji. Jest kilka rzeczy, które dobry model powinien spełniać.

Ważne jest, aby prawidłowo ustawić wagę. Czasami nie jest możliwe prawidłowe ustawienie tego w oprogramowaniu do modelowania 3D z powodu różnych jednostek używanych przez te aplikacje. Aby wszystko było w porządku, ustaw współczynnik skali w ustawieniach importu modeli (pozostaw 0.01 dla 3dsMax i Modo, ustaw 1.0 dla Maya) i zauważ, że czasami będziesz musiał ponownie zaimportować obiekty po zmianie ustawienia skali. Te ustawienia powinny zapewnić, że możesz używać tylko podstawowej skali 1,1,1 w swoich scenach, aby uzyskać spójne zachowanie i brak problemów fizycznych. Dynamiczne grupowanie będzie również bardziej prawdopodobne, że będzie działać poprawnie. Ta zasada powinna być również stosowana do każdego podobiektu w modelu, a nie tylko głównego. Kiedy potrzebujesz dostosować wymiary obiektu, rób to w odniesieniu do innych obiektów w aplikacji do modelowania 3D, a nie w Unity. Możesz jednak poeksperymentować ze skalowaniem w Unity, aby znaleźć odpowiednie wartości, ale dla ostatecznej aplikacji i spójnego przepływu pracy dobrze jest mieć wszystko dobrze przygotowane przed zaimportowaniem do Unity.

Jeśli chodzi o funkcjonalność obiektów i ich dynamiczne części - dobrze podziel swoje modele. Im mniej podobiektów, tym lepiej. Oddziel części obiektu na wypadek, gdyby były potrzebne, na przykład do dynamicznego przesuwania lub obracania, do celów animacji lub innych interakcji. Każdy obiekt i jego podobiekty powinny mieć swój trzpień odpowiednio wyrównany i obrócony w odniesieniu do jego głównej funkcji. Główny obiekt powinien mieć oś Z skierowaną do przodu, a oś obrotu powinna znajdować się na dole obiektu, aby lepiej umieścić go na scenie. Używaj jak najmniej materiałów na przedmiotach (więcej na ten temat poniżej).

Wszystkie aktywa powinny mieć nazwy własne, które z łatwością opiszą ich rodzaj i funkcjonalność. Zachowaj tę spójność we wszystkich swoich projektach.

Powszechny błąd jedności nr 3: budowanie współzależnej architektury kodu

Prototypowanie i implementacja funkcjonalności w Unity jest dość prosta. Możesz łatwo przeciągać i upuszczać wszelkie odniesienia do innych obiektów, adresować każdy pojedynczy obiekt w scenie i uzyskiwać dostęp do każdego posiadanego komponentu. Jednak może to być również potencjalnie niebezpieczne. Oprócz zauważalnych problemów z wydajnością (znalezienie obiektu w hierarchii i dostęp do komponentów ma swoje obciążenie), istnieje również duże niebezpieczeństwo, że części kodu będą całkowicie od siebie zależne. Lub bycie zależnym od innych systemów i skryptów unikalnych dla Twojej aplikacji, a nawet od aktualnej sceny lub aktualnego scenariusza. Spróbuj przyjąć bardziej modułowe podejście i stworzyć części wielokrotnego użytku, które można wykorzystać w innych częściach aplikacji, a nawet współdzielić w całym portfolio aplikacji. Zbuduj swoją platformę i biblioteki na bazie Unity API w taki sam sposób, w jaki budujesz swoją bazę wiedzy.

Istnieje wiele różnych podejść, aby to zapewnić. Dobrym punktem wyjścia jest sam system komponentów Unity. Komplikacje mogą się pojawić, gdy poszczególne komponenty muszą komunikować się z innymi systemami aplikacji. W tym celu możesz użyć interfejsów, aby części systemu były bardziej abstrakcyjne i wielokrotnego użytku. Alternatywnie można użyć podejścia opartego na zdarzeniach do reagowania na określone zdarzenia spoza zakresu, tworząc system przesyłania wiadomości lub rejestrując się bezpośrednio w częściach innego systemu jako odbiorniki. Właściwym podejściem będzie próba oddzielenia właściwości gameObject od logiki programu (przynajmniej coś w rodzaju zasady model-kontroler), ponieważ trudno jest określić, które obiekty modyfikują jego właściwości transformacji, takie jak położenie i obrót. Powinno to leżeć wyłącznie w gestii administratora.

Postaraj się, aby wszystko było dobrze udokumentowane. Traktuj to zawsze tak, jakbyś wracał do swojego kodu po długim czasie i musisz szybko zrozumieć, co dokładnie robi ta część kodu. Ponieważ w rzeczywistości dość często po pewnym czasie dojdziesz do niektórych części swojej aplikacji i jest to zbędna przeszkoda w szybkim wchodzeniu w problem. Ale nie przesadzaj. Czasami wystarczy odpowiednia nazwa klasy, metody lub właściwości.

Powszechny błąd jedności nr 4: marnowanie wydajności

Najnowsza linia produktów telefonów komórkowych, konsol czy komputerów stacjonarnych nigdy nie będzie tak zaawansowana, by nie trzeba było dbać o wydajność. Optymalizacje wydajności są zawsze potrzebne i stanowią podstawę do zmiany wyglądu Twojej gry lub aplikacji w porównaniu z innymi dostępnymi na rynku. Ponieważ, gdy oszczędzasz wydajność w jednej części, możesz to wykorzystać do dopracowania innych części swojej aplikacji.

Istnieje wiele obszarów optymalizacji. Cały artykuł byłby potrzebny tylko po to, by zarysować powierzchnię na ten temat. Przynajmniej spróbuję podzielić tę dziedzinę na kilka podstawowych obszarów.

Aktualizuj pętle

Nie używaj elementów wymagających dużej wydajności w pętlach aktualizacji, zamiast tego używaj buforowania. Typowym przykładem jest dostęp do komponentów lub innych obiektów w scenie lub intensywne obliczenia w twoich skryptach. Jeśli to możliwe, przechowuj wszystko w pamięci podręcznej w metodach Awake() lub zmień swoją architekturę na podejście oparte na zdarzeniach, aby uruchamiać rzeczy dokładnie wtedy, gdy są potrzebne.

Instancje

W przypadku obiektów, które są tworzone dość często (na przykład pociski w grze FPS), utwórz ich wstępnie zainicjowaną pulę i po prostu wybierz już zainicjowany, gdy jest potrzebny, i aktywuj go. Następnie zamiast niszczyć go, gdy nie jest już potrzebny, dezaktywuj go i odłóż do basenu.

Wykonanie

Użyj technik okluzji lub technik LOD, aby ograniczyć renderowane części sceny. Spróbuj użyć zoptymalizowanych modeli, aby móc kontrolować liczbę wierzchołków w scenie. Pamiętaj, że liczba wierzchołków to nie tylko liczba wierzchołków w samym modelu, ale mają na nią wpływ inne rzeczy, takie jak normalne (twarde krawędzie), współrzędne UV (szwy UV) i kolory wierzchołków. Ponadto wiele dynamicznych świateł w scenie znacząco wpłynie na ogólną wydajność, więc staraj się upiec wszystko z wyprzedzeniem, gdy tylko jest to możliwe.

Narysuj połączenia

Postaraj się zmniejszyć liczbę wywołań remisowych. W Unity można osiągnąć redukcję wywołań rysowania, używając statycznego przetwarzania wsadowego dla nieruchomych obiektów i dynamicznego przetwarzania wsadowego dla ruchomych. Jednak najpierw musisz przygotować sceny i modele (obiekty wsadowe muszą mieć te same materiały), a grupowanie obiektów dynamicznych działa tylko w przypadku modeli o niskiej rozdzielczości. Alternatywnie możesz połączyć siatki za pomocą skryptu w jeden ( Mesh.CombineMeshes ) zamiast używać grupowania, ale musisz uważać, aby nie tworzyć zbyt dużych obiektów, które nie mogą wykorzystać cullingu frustum widoku na niektórych platformach. Ogólnie rzecz biorąc, kluczem jest użycie jak najmniejszej ilości materiałów i udostępnianie ich na całej scenie. Czasami będziesz musiał tworzyć atlasy z tekstur, aby móc dzielić jeden materiał między różne obiekty. Dobrą wskazówką jest również użycie wyższej rozdzielczości tekstur lightmap scen (nie generowanej rozdzielczości, ale rozdzielczości wyjściowej tekstur), aby zmniejszyć ich liczbę podczas pieczenia światła w większych środowiskach.

Problemy z przerysowaniem

Nie używaj przezroczystych tekstur, gdy nie jest to konieczne, ponieważ spowoduje to problemy z szybkością wypełniania. Można go używać do skomplikowanej i bardziej odległej geometrii, takiej jak drzewa lub krzewy. Kiedy musisz go użyć, wolą shadery w wersji alfa zamiast shaderów z testowaniem alfa lub zamiast shaderów wycinanych dla platform mobilnych. Aby ogólnie zidentyfikować te problemy, spróbuj obniżyć rozdzielczość aplikacji. Jeśli to pomoże, możliwe, że masz problemy z szybkością wypełniania lub potrzebujesz bardziej zoptymalizować shadery. W przeciwnym razie może to być większy problem z pamięcią.

Shadery

Zoptymalizuj swoje shadery, aby uzyskać lepszą wydajność. Zmniejsz liczbę przejść, używaj zmiennych z mniejszą precyzją, zastąp skomplikowane obliczenia matematyczne wstępnie wygenerowanymi teksturami wyszukiwania.

Zawsze używaj profilera do określania wąskich gardeł. To świetne narzędzie. Do renderowania możesz również użyć niesamowitego Debugera ramek, który pomoże ci dowiedzieć się, jak ogólnie działają rzeczy podczas rozkładania procesów renderowania za jego pomocą.

Powszechny błąd jedności nr 5: ignorowanie problemów ze zbieraniem śmieci

Trzeba zdać sobie sprawę, że pomimo tego, że sam Garbage Collector (GC) pomaga nam być naprawdę wydajnym i skupionym na ważnych rzeczach w programowaniu, jest kilka rzeczy, o których powinniśmy być wyraźnie świadomi. Korzystanie z GC nie jest darmowe. Ogólnie rzecz biorąc, powinniśmy unikać niepotrzebnych alokacji pamięci, aby zapobiec zbyt częstemu uruchamianiu się GC, a tym samym psuciu wydajności przez skoki liczby klatek na sekundę. W idealnym przypadku w każdej klatce nie powinno być żadnych nowych alokacji pamięci regularnie. Jak jednak możemy osiągnąć ten cel? Tak naprawdę zależy to od architektury aplikacji, ale istnieją pewne zasady, które mogą pomóc:

  • Unikaj niepotrzebnych alokacji w pętlach aktualizacji.
  • Użyj struktur dla prostych kontenerów właściwości, ponieważ nie są one przydzielone na stercie.
  • Spróbuj wstępnie przydzielić tablice, listy lub inne kolekcje obiektów, zamiast tworzyć je w pętlach aktualizacji.
  • Unikaj używania monofonicznych rzeczy problematycznych (na przykład wyrażeń LINQ lub pętli foreach), ponieważ Unity używa starszej, nie idealnie zoptymalizowanej wersji Mono (w momencie pisania jest to zmodyfikowana wersja 2.6, z aktualizacją na mapie drogowej).
  • Buforuj łańcuchy w metodach Awake() lub w zdarzeniach.
  • Jeśli konieczna jest aktualizacja właściwości ciągu w pętli aktualizacji, użyj obiektu StringBuilder zamiast ciągu.
  • Użyj profilera, aby zidentyfikować potencjalne problemy.

Powszechny błąd jedności nr 6: Optymalizacja wykorzystania pamięci i przestrzeni kosmicznej na koniec

Należy zwracać uwagę na jak najmniejsze zużycie pamięci i miejsca przez aplikację od początku projektu, gdyż trudniej jest to zrobić, gdy optymalizacja zostanie pozostawiona w fazie przedpremierowej. Na urządzeniach mobilnych jest to jeszcze ważniejsze, ponieważ brakuje nam tam zasobów. Ponadto, przekraczając rozmiar instalacji 100MB, możemy stracić znaczną część naszych klientów. Wynika to z limitu 100 MB na pobieranie przez sieć komórkową, a także z przyczyn psychologicznych. Zawsze lepiej jest, gdy Twoja aplikacja nie marnuje cennych zasobów telefonicznych klientów, a klienci będą bardziej skłonni pobrać lub kupić Twoją aplikację, gdy jej rozmiar jest mniejszy.

Aby znaleźć narzędzia do usuwania zasobów, możesz użyć dziennika edytora, w którym możesz zobaczyć (po każdej nowej kompilacji) rozmiar zasobów podzielonych na osobne kategorie, takie jak audio, tekstury i biblioteki DLL. Dla lepszej orientacji w Unity Asset Store dostępne są rozszerzenia edytorów, które zapewnią Ci szczegółowe podsumowanie z przywoływanymi zasobami i plikami w Twoim systemie plików. Rzeczywiste zużycie pamięci można również zobaczyć w profilerze, ale zaleca się przetestowanie go po połączeniu w celu kompilacji na platformie docelowej, ponieważ istnieje wiele niespójności podczas testowania w edytorze lub na czymkolwiek innym niż platforma docelowa.

Największymi konsumentami pamięci są często tekstury. Najlepiej używać skompresowanych tekstur, ponieważ zajmują znacznie mniej miejsca i pamięci. Ustaw wszystkie tekstury do kwadratu, najlepiej, aby długość obu stron była potęgą dwójki (POT), ale pamiętaj, że Unity może również automatycznie skalować tekstury NPOT do POT. Tekstury mogą być kompresowane w formie POT. Atlas teksturuje razem, aby wypełnić całą teksturę. Czasami możesz nawet użyć kanału alfa tekstur, aby uzyskać dodatkowe informacje dla swoich shaderów, aby zaoszczędzić dodatkowe miejsce i wydajność. I oczywiście staraj się w miarę możliwości ponownie używać tekstur w swoich scenach i używaj powtarzających się tekstur, gdy jest to możliwe, aby zachować dobry wygląd. W przypadku słabszych urządzeń możesz obniżyć rozdzielczość tekstur w ustawieniach jakości. Używaj skompresowanego formatu audio dla dłuższych klipów audio, takich jak muzyka w tle.

Kiedy masz do czynienia z różnymi platformami, rozdzielczościami lub lokalizacjami, możesz użyć pakietów zasobów, aby używać różnych zestawów tekstur dla różnych urządzeń lub użytkowników. Te pakiety zasobów można dynamicznie ładować z Internetu po zainstalowaniu aplikacji. W ten sposób możesz przekroczyć limit 100 MB, pobierając zasoby w trakcie gry.

Powszechny błąd jedności nr 7: Powszechne błędy w fizyce

Czasami, przesuwając obiekty w scenie, nie zdajemy sobie sprawy, że obiekt ma na sobie zderzacz i że zmiana jego pozycji zmusi silnik do ponownego przeliczenia całego fizycznego świata. W takim przypadku powinieneś dodać do niego komponent Rigidbody (możesz ustawić go jako niekinematyczny, jeśli nie chcesz, aby zaangażowane były siły zewnętrzne).

Aby zmodyfikować pozycję obiektu z umieszczonym na nim Rigidbody , zawsze ustawiaj Rigidbody.position , gdy nowa pozycja nie jest następująca po poprzedniej, lub Rigidbody.MovePosition , gdy jest to ruch ciągły, który uwzględnia również interpolację. Podczas jego modyfikacji stosuj operacje zawsze w FixedUpdate , a nie w funkcjach Update . Zapewni to spójne zachowania fizyczne.

Jeśli to możliwe, używaj prymitywnych zderzaczy na gameObjects, takich jak kula, pudełko lub cylinder, a nie zderzacza siatki. Możesz skomponować swój ostateczny zderzacz z więcej niż jednego z tych zderzaczy. Fizyka może stanowić wąskie gardło wydajności aplikacji ze względu na obciążenie procesora, a kolizje między prymitywnymi zderzaczami są znacznie szybsze do obliczenia. Możesz także dostosować ustawienie Fixed Timestep w Menedżerze czasu, aby zmniejszyć częstotliwość stałych aktualizacji fizyki, gdy dokładność interakcji fizyki nie jest tak potrzebna.

Powszechny błąd jedności nr 8: ręczne testowanie wszystkich funkcji

Czasami może pojawić się tendencja do ręcznego testowania funkcjonalności, eksperymentując w trybie gry, ponieważ jest to całkiem zabawne i masz wszystko pod bezpośrednią kontrolą. Ale ten fajny czynnik może dość szybko się zmniejszyć. Im bardziej złożona staje się aplikacja, tym bardziej żmudne zadania programista musi powtarzać i zastanawiać się, aby upewnić się, że aplikacja zachowuje się zgodnie z pierwotnym zamierzeniem. Z łatwością może stać się najgorszą częścią całego procesu rozwoju, ze względu na swój powtarzalny i pasywny charakter. Ponadto, ponieważ ręczne powtarzanie scenariuszy testowych nie jest tak zabawne, więc istnieje większa szansa, że ​​niektóre błędy przejdą przez cały proces testowania.

Unity ma świetne narzędzia do testowania, które to automatyzują. Dzięki odpowiedniemu projektowi architektury i kodu możesz używać testów jednostkowych do testowania izolowanej funkcjonalności, a nawet testów integracyjnych do testowania bardziej złożonych scenariuszy. Możesz znacznie ograniczyć podejście „ próbuj i sprawdź ”, w którym rejestrujesz rzeczywiste dane i porównujesz je z pożądanym stanem.

Testowanie ręczne jest bez wątpienia krytyczną częścią rozwoju. Ale jego ilość można zmniejszyć, a cały proces może być solidniejszy i szybszy. Gdy nie ma możliwości zautomatyzowania tego, przygotuj sceny testowe, aby móc jak najszybciej zająć się problemem, który próbujesz rozwiązać. Najlepiej kilka klatek po naciśnięciu przycisku odtwarzania. Zaimplementuj skróty lub kody, aby ustawić żądany stan do testowania. Ponadto odizoluj sytuację testową, aby mieć pewność, co powoduje problem. Każda niepotrzebna sekunda w trybie odtwarzania, gdy testowanie jest gromadzone, i im większe początkowe nastawienie związane z testowaniem problemu, tym większe prawdopodobieństwo, że w ogóle go nie przetestujesz i będziesz mieć nadzieję, że wszystko działa dobrze. Ale prawdopodobnie nie będzie.

Powszechny błąd Unity nr 9: myślenie, że wtyczki Unity Asset Store rozwiążą wszystkie Twoje problemy

Zaufaj mi; nie będą. Pracując z niektórymi klientami, czasami spotykałem się z tendencją lub reliktami z przeszłości, polegającym na używaniu wtyczek do sklepu z zasobami dla każdego drobiazgu. Nie chodzi mi o to, że w Unity Asset Store nie ma przydatnych rozszerzeń Unity. Jest ich wiele, a czasem nawet trudno zdecydować, którą wybrać. Ale w przypadku każdego projektu ważne jest zachowanie spójności, która może zostać zniszczona przez nierozsądne użycie różnych elementów, które nie pasują do siebie.

Z drugiej strony, dla funkcjonalności, których wdrożenie zajęłoby Ci dużo czasu, zawsze przydatne jest korzystanie ze sprawdzonych produktów z Unity Asset Store, co pozwala zaoszczędzić ogromną ilość czasu na programowaniu. Jednak wybieraj ostrożnie, używaj sprawdzonych, które nie wprowadzą wielu niekontrolowanych i dziwnych błędów do Twojego produktu końcowego. Pięciogwiazdkowe recenzje to dobry początek.

Jeśli pożądana funkcjonalność nie jest trudna do wdrożenia, po prostu dodaj ją do stale rosnących bibliotek osobistych (lub firmowych), które można później ponownie wykorzystać we wszystkich projektach. W ten sposób jednocześnie poprawiasz swoją wiedzę i zestaw narzędzi.

Powszechny błąd nr 10 w Unity: brak potrzeby rozszerzania podstawowych funkcji Unity

Czasami może się wydawać, że środowisko Unity Editor jest w zupełności wystarczające do podstawowego testowania gier i projektowania poziomów, a jego rozszerzanie to strata czasu. Ale uwierz mi, tak nie jest. Ogromny potencjał rozwojowy Unity wynika z umiejętności dostosowania go do konkretnych problemów, które należy rozwiązać w różnych projektach. Może to poprawić wrażenia użytkownika podczas pracy w Unity lub znacznie przyspieszyć cały proces tworzenia i projektowania poziomów. Szkoda byłoby nie używać wbudowanych funkcji, takich jak wbudowane lub niestandardowe szuflady właściwości, szuflady dekoratorów, niestandardowe ustawienia inspektora komponentów, a nawet nie budować całych wtyczek z własnymi oknami edytora.

Wniosek

Mam nadzieję, że te tematy będą dla Ciebie przydatne, gdy będziesz dalej rozwijać swoje projekty w Unity. Jest wiele rzeczy, które są specyficzne dla projektu, więc nie można ich zastosować, ale zawsze warto mieć na uwadze kilka podstawowych zasad podczas rozwiązywania trudniejszych i bardziej szczegółowych problemów. Możesz mieć różne opinie lub procedury dotyczące rozwiązywania tych problemów w swoich projektach. Najważniejsze jest, aby Twoje idiomy były spójne w całym projekcie, aby każdy w Twoim zespole mógł jasno zrozumieć, w jaki sposób dana domena powinna zostać poprawnie rozwiązana.


Dalsza lektura na blogu Toptal Engineering:

  • Rozwój SI w Unity: samouczek dotyczący maszyny skończonej