Jak zaimplementować funkcje w systemie encji?

31

Po zadaniu dwóch pytań na temat systemów bytów ( 1 , 2 ) i przeczytaniu kilku artykułów na ich temat, myślę, że rozumiem je znacznie lepiej niż wcześniej. Nadal mam pewne wątpliwości, głównie dotyczące budowy emitera cząstek, systemu wejściowego i kamery. Oczywiście nadal mam pewne problemy ze zrozumieniem systemów encji i mogą one dotyczyć całej innej gamy obiektów, ale wybrałem te trzy, ponieważ są to bardzo różne koncepcje, powinny obejmować dość szeroki obszar i pomóc mi zrozumieć systemy encji i jak radzę sobie z takimi problemami, jak się pojawiają.

Buduję silnik w JavaScript i zaimplementowałem większość podstawowych funkcji, w tym: obsługę danych wejściowych, elastyczny system animacji, emiter cząstek, klasy i funkcje matematyczne, obsługę scen, kamerę i rendering oraz całą masę innych rzeczy, które zazwyczaj obsługują silniki. Przeczytałem odpowiedź Byte56, która zainteresowała mnie przekształceniem silnika w system encji. Nadal pozostałby silnikiem gry HTML5, z podstawową filozofią scen, ale powinien wspierać dynamiczne tworzenie bytów z komponentów.


Problem, który mam teraz, polega na dopasowaniu mojej starej koncepcji silnika do tego nowego paradygmatu programowania. Oto niektóre definicje z poprzednich pytań, zaktualizowane:

  • Podmiot jest identyfikatorem. Nie ma żadnych danych, to nie jest obiekt, to prosty identyfikator reprezentujący indeks na liście scen wszystkich bytów (które faktycznie planuję zaimplementować jako macierz komponentów).

  • Komponent jest posiadaczem danych, ale z metodami, które mogą pracować na tych danych. Najlepszym przykładem jest Vector2Dkomponent „Pozycja”. Ma dane: xa y, ale także niektóre metody, które sprawiają, że działające na danych nieco łatwiejszej: add(), normalize(), i tak dalej.

  • System jest czymś, co może działać na zestawie podmiotów, które spełniają określone wymagania; zwykle jednostki muszą mieć określony zestaw komponentów, aby móc nimi operować. System jest częścią „logiczną”, „algorytmową”, a wszystkie funkcje dostarczane przez komponenty służą wyłącznie do łatwiejszego zarządzania danymi.


Aparat fotograficzny

Kamera ma Vector2Dwłaściwość położenia, właściwość obrotu i niektóre metody centrowania jej wokół punktu. Każda klatka jest podawana do renderera wraz ze sceną, a wszystkie obiekty są tłumaczone zgodnie z jego pozycją. Scena jest następnie renderowana.

Jak mogę reprezentować tego rodzaju obiekt w systemie encji? Czy kamera byłaby bytem, ​​komponentem lub kombinacją (zgodnie z moją odpowiedzią )?

Emiter cząstek

Problemem, jaki mam z moim emiterem cząstek, jest to, co powinno być. Jestem prawie pewien, że same cząstki nie powinny być bytami, ponieważ chcę wesprzeć ich ponad 10 000, i wierzę, że stworzenie tak wielu bytów byłoby dużym ciosem dla mojej wydajności.

Jak mogę reprezentować tego rodzaju obiekt w systemie encji?

Menedżer wprowadzania

Ostatnim, o którym chcę porozmawiać, jest sposób obsługi danych wejściowych. W mojej obecnej wersji silnika istnieje klasa o nazwie Input. Jest to moduł obsługi, który subskrybuje zdarzenia przeglądarki, takie jak naciśnięcia klawiszy i zmiany pozycji myszy, a także utrzymuje stan wewnętrzny. Następnie klasa gracza ma react()metodę, która akceptuje obiekt wejściowy jako argument. Zaletą tego jest to, że obiekt wejściowy można przekształcić do postaci szeregowej w .JSON, a następnie udostępnić przez sieć, co pozwala na płynne symulacje dla wielu graczy.

Jak to się przekłada na system encji?

jcora
źródło

Odpowiedzi:

26
  • Kamera: włączenie tego komponentu byłoby całkiem fajne. Po prostu miałbyisRenderingflaga i zakres głębokości, jak powiedział Sean. Oprócz „pola widzenia” (myślę, że można to nazwać skalą w 2D?) I strefy wyjściowej. Strefa wyjściowa może określać część okna gry, do której renderowana jest ta kamera. Nie miałby osobnej pozycji / rotacji, jak wspomniałeś. Utworzony element, który ma komponent kamery, użyłby komponentów położenia i obrotu tego elementu. Wtedy miałbyś system kamer, który szuka bytów, które mają kamerę, pozycję i elementy obrotu. System pobiera ten byt i rysuje wszystkie byty, które „widzi” ze swojego położenia, obrotu, głębokości widzenia i pola widzenia, do określonej części ekranu. Daje to wiele opcji symulacji wielu portów widoku, okien „widoku postaci”, lokalnego trybu dla wielu graczy,

  • Emiter cząstek: To też powinno być po prostu komponentem. Układ cząstek szukałby bytów, które mają pozycję, obrót i emiter cząstek. Emiter ma wszystkie właściwości potrzebne do odtworzenia obecnego emitera, nie jestem pewien, co to wszystko, takie rzeczy jak: szybkość, prędkość początkowa, czas rozpadu i tak dalej. Nie musisz wykonywać wielu przejść. Układ cząsteczkowy wie, które byty mają ten składnik. Wyobrażam sobie, że możesz ponownie wykorzystać sporo istniejącego kodu.

  • Dane wejściowe: Muszę powiedzieć, że przekształcenie tego w komponent ma największy sens, biorąc pod uwagę powyższe sugestie. Twójinput systemzostanie zaktualizowany w każdej ramce o bieżące zdarzenia wejściowe. Następnie, gdy przejdzie przez wszystkie swoje podmioty, które mają składnik wejściowy, zastosuje te zdarzenia. Składnik wejściowy miałby listę zdarzeń klawiatury i myszy, wszystkie powiązane wywołania zwrotne metod. Nie jestem do końca pewien, gdzie mogłyby istnieć wywołania zwrotne metody. Może jakaś klasa kontrolera wejściowego? Cokolwiek ma sens w przypadku późniejszej modyfikacji przez użytkowników Twojego silnika. Dałoby to jednak siłę do łatwego zastosowania kontroli wejścia do podmiotów kamery, podmiotów odtwarzacza lub czegokolwiek, czego potrzebujesz. Chcesz zsynchronizować ruch grupy bytów z klawiaturą? Po prostu podaj im wszystkie komponenty wejściowe, które odpowiadają tym samym wejściom, a system wejściowy zastosuje te zdarzenia przenoszenia do wszystkich komponentów, które o nie proszą.

Więc większość z tego jest tuż nad moją głową, więc prawdopodobnie nie będzie to miało sensu bez dalszych wyjaśnień. Po prostu daj mi znać, co nie jest jasne. Zasadniczo dałem ci dużo pracy :)

MichaelHouse
źródło
Kolejna świetna odpowiedź! Dzięki! Teraz moim jedynym problemem jest szybkie przechowywanie i wyszukiwanie jednostek, więc użytkownik może faktycznie zaimplementować pętlę / logikę gry ... Spróbuję to rozgryźć sam, ale najpierw muszę się dowiedzieć, jak JavaScript radzi sobie z tablicami, obiektami i niezdefiniowane wartości w pamięci, aby zgadnąć ... To będzie problem, ponieważ różne przeglądarki mogą go implementować inaczej.
jcora
Wydaje się to architektonicznie czyste, ale w jaki sposób system renderujący określa, że ​​aktywna kamera nie wykonuje iteracji przez wszystkie byty?
Tempo
@Pace Ponieważ chciałbym, aby aktywna kamera została znaleziona bardzo szybko, prawdopodobnie pozwoliłbym systemowi kamery zachować odniesienie do podmiotów, które mają aktywną kamerę.
MichaelHouse
Gdzie umieszczasz logikę do sterowania wieloma kamerami (patrz, obracaj, przesuwaj itp.)? Jak kontrolujesz wiele kamer?
plasmacel
@plasmacel Jeśli masz wiele obiektów, które współużytkują elementy sterujące, to twój system sterowania będzie odpowiedzialny za określenie, który obiekt otrzymuje dane wejściowe.
MichaelHouse
13

Oto jak do tego podszedłem:

Aparat fotograficzny

Mój aparat jest bytem, ​​jak każdy inny, który ma dołączone komponenty:

  1. Transformma Translation, Rotationa Scalewłaściwości, oprócz innych do prędkości, itd.

  2. Pov(Punkt widzenia) ma FieldOfView, AspectRatio, Near, Fari wszelkie inne informacje wymagane do wytworzenia macierzy projekcji, oprócz IsOrthoflagi używane do przełączania pomiędzy perspektywy i prostopadłych występów. Povudostępnia także ProjectionMatrixwłaściwość leniwe obciążenia używaną przez system renderowania, która jest wewnętrznie obliczana podczas odczytu i buforowana, dopóki dowolne inne właściwości nie zostaną zmodyfikowane.

Nie ma dedykowanego systemu kamer. System renderowania zachowuje listę Povi zawiera logikę służącą do określania, której użyć podczas renderowania.

Wkład

InputReceiverKomponent może być dołączony do dowolnego podmiotu. Ma dołączony moduł obsługi zdarzeń (lub lambda, jeśli Twój język to obsługuje), który jest używany do przechowywania danych wejściowych specyficznych dla jednostki, który pobiera parametry dla bieżącego i poprzedniego stanu klucza, bieżącej i poprzedniej lokalizacji myszy oraz stanu przycisku itp. (W rzeczywistości, istnieją osobne programy obsługi myszy i klawiatury).

Na przykład w grze testowej podobnej do asteroid, którą stworzyłem, przyzwyczajając się do Entity / Component, mam dwie wejściowe metody lambda. Jeden obsługuje nawigację statku, przetwarzając klawisze strzałek i spację (do wystrzelenia). Drugi obsługuje ogólne wprowadzanie z klawiatury - klawisze wyjścia, pauzy itp., Poziom restartu itp. Tworzę dwa komponenty, dołączam każdą lambda do własnego komponentu, a następnie przypisuję komponent odbiornika nawigacyjnego do jednostki statku, a drugą do niewidoczny obiekt procesora poleceń.

Oto procedura obsługi zdarzeń do obsługi kluczy przechowywanych między ramkami, które są dołączane do InputReceiverkomponentu statku (C #):

  void ship_input_Hold(object sender, InputEventArgs args)
    {
        var k = args.Keys;
        var e = args.Entity;

        var dt = (float)args.GameTime.ElapsedGameTime.TotalSeconds;

        var verlet = e.As<VerletMotion>();
        var transform = e.As<Transform>();

        if (verlet != null)
        {

        /// calculate applied force 
            var force = Vector3.Zero;
            var forward = transform.RotationMatrix.Up * Settings.ShipSpeedMax;

            if (k.Contains(Keys.W))
                force += forward;

            if (k.Contains(Keys.S))
                force -= forward;

            verlet.Force += force * dt;
        }

        if (transform != null)
        {
            var theta = Vector3.Zero;

            if (k.Contains(Keys.A))
                theta.Z += Settings.TurnRate;

            if (k.Contains(Keys.D))
                theta.Z -= Settings.TurnRate;

            transform.Rotation += theta * dt;
        }

        if (k.Contains(Keys.Space))
        {
            var time = (float)args.GameTime.TotalGameTime.TotalSeconds - _rapidFireLast;

            if (time >= _rapidFireDelay)
            {
                Fire();
                _rapidFireLast = (float)args.GameTime.TotalGameTime.TotalSeconds;
            }
        }
    }

Jeśli twój aparat jest mobilny, daj mu swój InputReceiveri swój Transformkomponent, podłącz lambda lub moduł obsługi, który realizuje dowolny rodzaj kontroli, którą chcesz i gotowe.

Jest to dość fajne, ponieważ można przenieść InputReceiverkomponent z przewodnikiem nawigacyjnym przymocowanym ze statku do asteroidy lub cokolwiek innego w tym zakresie i zamiast tego latać. Lub przypisując Povkomponent do dowolnej innej części sceny - asteroidy, lampy ulicznej itp. - możesz oglądać swoją scenę z perspektywy tego bytu.

InputSystemKlasa, która utrzymuje stan wewnętrzny dla klawiatury, myszy itp InputSystemfiltruje swój wewnętrzny zbiór encji do podmiotów, które mają InputReceiverkomponent. W swojej Update()metodzie iteruje tę kolekcję i wywołuje procedury obsługi danych wejściowych dołączone do każdego z tych komponentów w taki sam sposób, jak system renderujący rysuje każdą jednostkę za pomocą Renderablekomponentu.

Cząsteczki

To naprawdę zależy od tego, jak planujesz oddziaływać z cząsteczkami. Jeśli potrzebujesz tylko układu cząsteczek, który zachowuje się jak jeden obiekt - powiedzmy, fajerwerki pokazują, że gracz nie może dotknąć ani uderzyć - wtedy stworzyłbym pojedynczy byt i ParticleRenderGroupkomponent zawierający dowolne informacje potrzebne cząstkom - rozpad itp. - które nie są objęte twoim Renderableskładnikiem. Podczas renderowania system renderujący sprawdza, czy jednostka ma RenderParticleGroupdołączony element i odpowiednio go obsługuje.

Jeśli potrzebujesz pojedynczych cząstek do uczestniczenia w wykrywaniu kolizji, reagowania na dane wejściowe itp., Ale chcesz po prostu renderować je jako partię, utworzę Particlekomponent zawierający te informacje dla poszczególnych cząstek i stworzę je jako oddzielne podmioty. System renderowania nadal może je grupować, ale inne systemy będą traktować je jako osobne obiekty. (Działa to bardzo dobrze w przypadku wystąpienia).

Następnie, albo w twoim MotionSystem(lub jakimkolwiek użyciu, który obsługuje aktualizację pozycji bytu, itp.) Lub w dedykowanym ParticleSystem, wykonaj dowolne przetwarzanie wymagane dla każdej cząstki na ramkę. RenderSystemByłby odpowiedzialny za budowę / dozowania i buforowanie zbiory cząstek, jak są one tworzone i niszczone, i uczynić je w razie potrzeby.

Jedną fajną rzeczą w tym podejściu jest to, że nie musisz mieć żadnych specjalnych przypadków kolizji, ubijania itp. Dla cząstek; kod, który piszesz dla każdego innego rodzaju bytu, może być nadal używany.

Wniosek

Jeśli zastanawiasz się nad przejściem na wiele platform - nie ma zastosowania do JavaScript - cały kod specyficzny dla platformy (a mianowicie renderowanie i wprowadzanie danych) jest izolowany w dwóch systemach. Twoja logika gry pozostaje w klasach zależnych od platformy (ruch, kolizja itp.), Więc nie powinieneś dotykać ich podczas przenoszenia.

Rozumiem i zgadzam się ze stanowiskiem Seana, że ​​obrzucanie butami rzeczy we wzór w celu ścisłego trzymania się wzoru, zamiast dostosowywania wzoru w celu wypełnienia potrzeb twojej aplikacji, jest złe. Po prostu nie widzę niczego w danych wejściowych, kamerze lub cząsteczkach, które wymagałyby tego rodzaju leczenia.

3Dave
źródło
Gdzie umieszczasz logikę do sterowania wieloma kamerami (patrz, obracaj, przesuwaj itp.)?
plasmacel
7

Logika wprowadzania i gry będzie prawdopodobnie obsługiwana w dedykowanym fragmencie kodu poza systemem komponentu encji. Technicznie możliwe jest wprowadzenie go do projektu, ale nie ma to żadnej korzyści - logika gry i interfejs użytkownika są zuchwałe i pełne nieszczelnych abstrakcji bez względu na to, co robisz, a próba wtłoczenia kwadratowego kołka w okrągły otwór tylko ze względu na czystość architektoniczną jest marnotrawstwem czasu.

Podobnie, emitery cząstek są specjalnymi zwierzętami, szczególnie jeśli zależy ci na wydajności. Komponent emitera ma sens, ale grafika będzie robiła jakąś specjalną magię z tymi komponentami, zmieszanymi z magią do końca renderowania.

Jeśli chodzi o kamerę, po prostu nadaj kamerom aktywną flagę i być może indeks „głębokości” i pozwól systemowi graficznemu renderować wszystkie z nich, które są włączone. Jest to przydatne w przypadku wielu sztuczek, w tym GUI (chcesz, aby Twój GUI był renderowany w trybie ortograficznym na szczycie świata gry? Nie ma problemu, to tylko dwie kamery z różnymi maskami obiektów i GUI ustawionymi na wyższą warstwę). Jest także przydatny w przypadku warstw efektów specjalnych i tym podobnych.

Sean Middleditch
źródło
4

Czy kamera byłaby bytem, ​​czy po prostu elementem?

Nie jestem pewien, co tak naprawdę zadaje to pytanie. Biorąc pod uwagę, że jedynymi rzeczami, które masz w grze, są byty, to kamery muszą być bytami. Funkcjonalność kamery jest realizowana za pomocą pewnego rodzaju komponentu kamery. Nie posiadaj osobnych komponentów „Pozycja” i „Obrót” - to zdecydowanie za niski poziom. Powinny one zostać połączone w jakiś element WorldPosition, który miałby zastosowanie do każdego podmiotu zlokalizowanego na świecie. Jeśli chodzi o to, którego użyć ... musisz jakoś wprowadzić logikę do systemu. Albo zakodujesz go na stałe w systemie obsługi aparatu, albo załączysz skrypty lub coś w tym rodzaju. Możesz pomóc włączyć / wyłączyć flagę w komponencie kamery, jeśli to pomaga.

Jestem całkiem pewien, że same cząstki nie powinny być bytami

Ja też. Emiter cząstek byłby bytem, ​​a układ cząstek śledziłby cząstki związane z danym bytem. Takie rzeczy zdają sobie sprawę, że „wszystko jest bytem” jest absurdalnie niepraktyczne. W praktyce jedynymi rzeczami, które są bytami, są względnie złożone obiekty, które korzystają z kombinacji składników.

Jeśli chodzi o dane wejściowe: dane wejściowe nie istnieją w świecie gry jako takie, więc obsługuje je system. Niekoniecznie „system komponentów”, ponieważ nie wszystko w grze będzie się obracać wokół komponentów. Ale będzie system wejściowy. Możesz chcieć oznaczyć byt, który reaguje na dane wejściowe, jakimś rodzajem komponentu Player, ale dane wejściowe będą złożone i całkowicie specyficzne dla gry, więc nie ma sensu próbować tworzyć do tego komponentów.

Kylotan
źródło
1

Oto niektóre z moich pomysłów na rozwiązanie tych problemów. Prawdopodobnie będą mieli coś z nimi nie tak i prawdopodobnie będzie lepsze podejście, więc proszę, skierujcie mnie do tych w odpowiedzi!

Kamera :

Istnieje element „Kamera”, który można dodać do dowolnego elementu. Nie jestem jednak w stanie ustalić, jakie dane powinienem umieścić w tym elemencie: mogłem mieć osobne komponenty „Pozycja” i „Obrót”! followMetoda nie muszą być realizowane, ponieważ już po jednostka jest podłączona do! I mogę go swobodnie przenosić. Problem z tym systemem polegałby na wielu różnych obiektach kamery: skąd można RendererSystemwiedzieć, których użyć? Poza tym zwykłem po prostu przekazywać obiekt kamery, ale teraz wydaje się, że RendererSystemtrzeba będzie dwa razy iterować wszystkie byty: po pierwsze, aby znaleźć te, które działają jak kamery, a po drugie, aby wszystko zrenderować.

ParticleEmitter :

Byłby taki, ParticleSystemktóry aktualizowałby wszystkie podmioty, które miały komponent „Emiter”. Cząstki są głupimi obiektami we względnej przestrzeni współrzędnych wewnątrz tego komponentu. Występuje tutaj problem renderowania: musiałbym albo stworzyć ParticleRenderersystem, albo rozszerzyć funkcjonalność istniejącego.

System wejściowy :

Główną troską była dla mnie tutaj logika lub react()metoda. Jedyne rozwiązanie, jakie wymyśliłem, to osobny system i komponent dla każdego systemu, który wskazywałby, którego użyć. To naprawdę wydaje się zbyt hakerskie i nie wiem, jak sobie z tym poradzić. Jedną rzeczą jest to, że, o ile się martwię, Inputmogą pozostać zaimplementowane jako klasa, ale nie wiem, jak mogę to zintegrować z resztą gry.

jcora
źródło
RendererSystem tak naprawdę nie ma powodu do iteracji po wszystkich bytach - powinien już mieć listę urządzeń do rysowania (oraz kamer i świateł (chyba że światła są urządzeniami do rysowania)) lub wiedzieć, gdzie są te listy. Prawdopodobnie będziesz chciał wykonać culling dla kamer, które chcesz wyrenderować, więc może twoja kamera może zawierać listę identyfikujących bytów, które są widoczne dla niej. Możesz mieć wiele kamer i jedną aktywną lub jedną, która jest podłączona do różnych POV, z których obie mogą być kontrolowane przez dowolną liczbę rzeczy, takich jak skrypty, wyzwalacze i dane wejściowe
@ melak47, to prawda, też o tym myślałem, ale chciałem to wdrożyć w sposób, w jaki robi to Aremis. Ale to „systemy przechowują odniesienia do odpowiednich podmiotów” wydają się być coraz bardziej wadliwe ...
jcora
Czy Artemis nie przechowuje każdego typu komponentu na własnej liście? więc czy nie miałbyś dokładnie tych list elementów, które można wyciągać, elementów aparatu, świateł i czego nie gdzieś?