Pracuję nad własnym silnikiem gry i obecnie projektuję swoich menedżerów. Przeczytałem, że do zarządzania pamięcią użycie Init()
i CleanUp()
funkcje są lepsze niż używanie konstruktorów i destruktorów.
Szukałem przykładów kodu C ++, aby zobaczyć, jak działają te funkcje i jak mogę je zaimplementować w moim silniku. Jak działa Init()
i CleanUp()
działa i jak mogę je wdrożyć do mojego silnika?
Odpowiedzi:
To całkiem proste:
Zamiast mieć Konstruktora, który wykonuje konfigurację,
... niech twój konstruktor zrobi niewiele lub nic, i napisze metodę o nazwie
.init
lub.initialize
, która zrobiłaby to, co normalnie zrobiłby twój konstruktor.Więc teraz zamiast po prostu:
Możesz iść:
Zaletą jest to, że możesz teraz łatwiej używać systemów wstrzykiwania zależności / inwersji kontroli w swoich systemach.
Zamiast mówić
Można budować żołnierza, dać mu metodę zasilania, korzystny gdzie ręką mu broń, a następnie wezwać wszystkich pozostałych funkcji konstruktora.
Więc teraz zamiast podklasować wrogów, w których jeden żołnierz ma pistolet, a drugi ma karabin, a drugi ma strzelbę, a to jedyna różnica, możesz po prostu powiedzieć:
To samo dotyczy zniszczenia. Jeśli masz specjalne potrzeby (usuwanie detektorów zdarzeń, usuwanie instancji z tablic / dowolnych struktur, z którymi pracujesz itp.), Wówczas ręcznie je wywołujesz, abyś dokładnie wiedział, kiedy i gdzie w programie się dzieje.
EDYTOWAĆ
Jak zauważył Kryotan poniżej, odpowiada to na „How” oryginalnego postu , ale tak naprawdę nie działa dobrze na „dlaczego”.
Jak zapewne widać w powyższej odpowiedzi, różnica między:
i pisanie
mając po prostu większą funkcję konstruktora.
Trzeba argumentować za obiektami, które mają 15 lub 20 warunków wstępnych, co spowodowałoby, że konstruktor byłby bardzo, bardzo trudny w obsłudze, a także ułatwiłby widzenie i zapamiętywanie, wyciągając te rzeczy do interfejsu , dzięki czemu można zobaczyć, jak działa tworzenie instancji, o jeden poziom wyżej.
Opcjonalna konfiguracja obiektów jest naturalnym rozszerzeniem tego; opcjonalnie ustawiając wartości w interfejsie, przed uruchomieniem obiektu.
JS ma kilka świetnych skrótów do tego pomysłu, które wydają się nie na miejscu w silniejszych językach typu C.
To powiedziawszy, istnieje szansa, że jeśli masz do czynienia z listą argumentów, która jest długa w twoim konstruktorze, twój obiekt jest zbyt duży i robi zbyt wiele, jak jest. Ponownie, jest to kwestia preferencji osobistych i istnieją daleko idące wyjątki, ale jeśli przenosisz 20 przedmiotów do obiektu, są duże szanse, że możesz znaleźć sposób, aby ten obiekt zrobił mniej, tworząc mniejsze obiekty .
Bardziej stosownym powodem i jednym z powszechnie stosowanych jest to, że inicjalizacja obiektu opiera się na danych asynchronicznych, których obecnie nie masz.
Wiesz, że potrzebujesz obiektu, więc i tak go utworzysz, ale aby poprawnie działać, potrzebuje danych z serwera lub z innego pliku, który teraz musi załadować.
Znowu, czy przekazujesz potrzebne dane do gigantycznej inicjacji, czy budujesz interfejs, nie jest tak naprawdę ważny dla tej koncepcji, tak bardzo jak dla interfejsu twojego obiektu i projektu twojego systemu ...
Ale jeśli chodzi o budowę obiektu, możesz zrobić coś takiego:
async_loader
może otrzymać nazwę pliku, nazwę zasobu lub cokolwiek innego, załadować ten zasób - może ładuje pliki dźwiękowe lub dane obrazu, a może ładuje zapisane statystyki postaci ...... a następnie zasiliłby te dane z powrotem
obj_w_async_dependencies.init(result);
.Ten rodzaj dynamiki znajduje się często w aplikacjach internetowych.
Niekoniecznie w konstrukcji obiektu, dla aplikacji wyższego poziomu: na przykład galerie mogą od razu się załadować i zainicjować, a następnie wyświetlać zdjęcia podczas przesyłania strumieniowego - to nie jest tak naprawdę inicjalizacja asynchroniczna, ale tam, gdzie jest ona widziana częściej, w bibliotekach JavaScript.
Jeden moduł może zależeć od drugiego, dlatego inicjalizacja tego modułu może zostać odroczona do czasu zakończenia ładowania osób zależnych.
Jeśli chodzi o przypadki tego specyficzne dla gry, rozważ rzeczywistą
Game
klasę.Dlaczego nie możemy wywołać
.start
lub.run
w konstruktorze?Zasoby muszą zostać załadowane - reszta wszystkiego została właściwie zdefiniowana i dobrze jest przejść, ale jeśli spróbujemy uruchomić grę bez połączenia z bazą danych lub bez tekstur, modeli, dźwięków lub poziomów, nie będzie szczególnie interesująca gra ...
... więc jaka jest różnica między tym, co widzimy w typowym
Game
, z wyjątkiem tego, że nadajemy jego metodzie „kontynuuj” nazwę, która jest bardziej interesująca niż.init
(lub odwrotnie, jeszcze bardziej rozbijać inicjalizację, aby oddzielić ładowanie, konfigurowanie rzeczy, które zostały załadowane i uruchamianie programu, gdy wszystko zostało skonfigurowane).źródło
.init
, może nie, ale prawdopodobnie. Ergo, ważna sprawa.Cokolwiek czytasz, że napisane jest, że Init i CleanUp jest lepsze, powinno również powiedzieć ci, dlaczego. Artykuły, które nie uzasadniają swoich roszczeń, nie są warte czytania.
Posiadanie osobnych funkcji inicjalizacji i zamykania może ułatwić konfigurowanie i niszczenie systemów, ponieważ można wybrać kolejność ich wywoływania, podczas gdy konstruktory są wywoływane dokładnie podczas tworzenia obiektu, a destruktory wywoływane po zniszczeniu obiektu. Kiedy masz złożone zależności między 2 obiektami, często potrzebujesz, aby oba istniały, zanim same się skonfigurują - ale często jest to oznaką złego projektu w innym miejscu.
Niektóre języki nie mają destruktorów, na których można polegać, ponieważ liczenie referencji i odśmiecanie utrudniają ustalenie, kiedy obiekt zostanie zniszczony. W tych językach prawie zawsze potrzebujesz metody zamykania / czyszczenia, a niektórzy lubią dodawać metodę init dla symetrii.
źródło
Myślę, że najlepszym powodem jest: umożliwienie łączenia.
jeśli masz Init i CleanUp, możesz, po zabiciu obiektu, po prostu wywołać CleanUp i wcisnąć obiekt na stos obiektu tego samego typu: „pulę”.
Następnie, ilekroć potrzebujesz nowego obiektu, możesz usunąć jeden obiekt z puli LUB jeśli pula jest pusta - źle - musisz utworzyć nowy. Następnie wywołujesz Init na tym obiekcie.
Dobrą strategią jest wstępne wypełnienie puli, zanim rozpocznie się gra z „dobrą” liczbą obiektów, dzięki czemu nigdy nie będziesz musiał tworzyć żadnych obiektów w puli podczas gry.
Z drugiej strony, jeśli użyjesz „nowego” i po prostu przestaniesz odwoływać się do obiektu, który nie jest dla ciebie bezużyteczny, tworzysz śmieci, które należy kiedyś przypomnieć. To przypomnienie jest szczególnie złe w przypadku języków jednowątkowych, takich jak JavaScript, w których moduł czyszczący zatrzymuje cały kod, gdy ocenia, że musi przypomnieć sobie pamięć obiektów, które nie są już używane. Gra zawiesza się w ciągu kilku milisekund, a wrażenia z gry są zepsute.
- Zrozumiałeś już: - jeśli połączysz wszystkie swoje obiekty, nie nastąpi przypomnienie, a zatem nie będzie już przypadkowego spowolnienia.
Znacznie szybciej jest także wywołać init na obiekcie pochodzącym z puli niż przydzielić pamięć + init nowy obiekt.
Jednak poprawa prędkości ma mniejsze znaczenie, ponieważ dość często tworzenie obiektów nie stanowi wąskiego gardła w wydajności ... Z kilkoma wyjątkami, takimi jak szalone gry, silniki cząstek lub silnik fizyki wykorzystujący intensywnie wektory 2D / 3d do swoich obliczeń. Tutaj zarówno szybkość, jak i tworzenie śmieci są znacznie poprawione dzięki użyciu puli.
Rq: może nie być konieczne zastosowanie metody CleanUp dla twoich pulowanych obiektów, jeśli Init () resetuje wszystko.
Edycja: odpowiedź na ten post zmotywowała mnie do sfinalizowania małego artykułu, który napisałem o tworzeniu puli w Javascript .
Możesz go znaleźć tutaj, jeśli jesteś zainteresowany:
http://gamealchemist.wordpress.com/
źródło
Twoje pytanie jest odwrócone ... Historycznie rzecz biorąc, bardziej trafne pytanie to:
Dlaczego konstrukcja + inicjałowanie jest łączona , tzn. Dlaczego nie wykonujemy tych kroków osobno? Z pewnością jest to sprzeczne z SoC ?
W przypadku C ++ intencją RAII jest powiązanie pozyskiwania i uwalniania zasobów bezpośrednio z czasem życia obiektu, w nadziei, że zapewni to uwolnienie zasobów. Czy to? Częściowo. Jest w 100% spełniony w kontekście zmiennych opartych na stosie / automatycznych, gdzie opuszczenie powiązanego zakresu automatycznie wywołuje destruktory / zwalnia te zmienne (stąd kwalifikator
automatic
). Jednak w przypadku zmiennych sterty ten bardzo przydatny wzorzec niestety się psuje, ponieważ nadal musisz jawnie wywoływaćdelete
w celu uruchomienia destruktora, a jeśli zapomnisz to zrobić, nadal będziesz ugryziony przez to, co RAII próbuje rozwiązać; w kontekście zmiennych alokowanych na stercie C ++ zapewnia ograniczoną przewagę nad C (wdelete
porównaniu z Cfree()
) przy jednoczesnym połączeniu konstrukcji z inicjalizacją, co ma negatywny wpływ na:Zdecydowanie zaleca się zbudowanie systemu obiektowego do gier / symulacji w C, ponieważ rzuci on dużo światła na ograniczenia RAII i innych takich wzorców OO-centrycznych poprzez głębsze zrozumienie założeń C ++ i późniejszych klasycznych języków OO (pamiętaj, że C ++ zaczął jako system OO zbudowany w C).
źródło