Jak połączyć w sieć ten system encji?

33

Zaprojektowałem system encji dla FPS. Zasadniczo działa tak:

Mamy „światowy” obiekt o nazwie GameWorld. Zawiera tablicę GameObject, a także tablicę ComponentManager.

GameObject zawiera tablicę Component. Zapewnia również bardzo prosty mechanizm zdarzeń. Same komponenty mogą wysyłać zdarzenie do jednostki, które jest transmitowane do wszystkich komponentów.

Komponent jest w zasadzie czymś, co nadaje GameObject określone właściwości, a ponieważ GameObject jest tak naprawdę tylko ich pojemnikiem, wszystko, co ma związek z obiektem gry, dzieje się w Components. Przykłady obejmują ViewComponent, PhysicsComponent i LogicComponent. Jeśli potrzebna jest komunikacja między nimi, można to zrobić za pomocą zdarzeń.

ComponentManager to po prostu interfejs podobny do Component, a dla każdej klasy Component powinna zasadniczo istnieć jedna klasa ComponentManager. Ci menedżerowie komponentów są odpowiedzialni za tworzenie komponentów i inicjowanie ich właściwościami odczytanymi z czegoś takiego jak plik XML.

ComponentManager zajmuje się także masowymi aktualizacjami komponentów, takich jak PhysicsComponent, w którym będę korzystać z zewnętrznej biblioteki (która robi wszystko na świecie na raz).

W celu konfiguracji będę używać fabryki dla encji, które będą czytały plik XML lub skrypt, utworzą komponenty określone w pliku (który również dodaje odniesienie do niego w odpowiednim menedżerze komponentów dla masowych aktualizacji) i następnie wstrzyknij je do obiektu GameObject.

Teraz pojawia się mój problem: spróbuję użyć tego do gier wieloosobowych. Nie mam pojęcia, jak do tego podejść.

Po pierwsze: jakie podmioty powinny mieć klienci od samego początku? Powinienem zacząć od wyjaśnienia, w jaki sposób silnik dla jednego gracza określiłby, które podmioty stworzyć.

W edytorze poziomów możesz tworzyć „pędzle” i „byty”. Pędzle służą do takich rzeczy jak ściany, podłogi i sufity, w zasadzie proste kształty. Podmioty to GameObject, o którym ci mówiłem. Podczas tworzenia jednostek w edytorze poziomów można określić właściwości dla każdego z jego składników. Te właściwości są przekazywane bezpośrednio do konstruktora w skrypcie encji.

Po zapisaniu poziomu do załadowania silnik jest rozkładany na listę encji i powiązanych z nimi właściwości. Pędzle są konwertowane na „świat odradzający się”.

Gdy ładujesz ten poziom, po prostu inicjuje on wszystkie jednostki. Brzmi prosto, prawda?

Teraz w sieciach podmiotów napotykam wiele problemów. Po pierwsze, jakie podmioty powinny istnieć na kliencie od samego początku? Zakładając, że zarówno serwer, jak i klient mają plik poziomu, klient może równie dobrze zaimplementować wszystkie jednostki na poziomie, nawet jeśli są one tylko do celów reguł gry na serwerze.

Inną możliwością jest to, że klient inicjuje jednostkę, gdy tylko serwer wyśle ​​o niej informacje, a to oznacza, że ​​klient będzie miał tylko jednostki, których potrzebuje.

Kolejny problem dotyczy sposobu przesyłania informacji. Myślę, że serwer mógłby użyć kompresji delta, co oznacza, że ​​wysyła nowe informacje tylko wtedy, gdy coś się zmienia, zamiast wysyłać migawkę do klienta w każdej ramce. Oznacza to jednak, że serwer musi śledzić to, co wie każdy klient.

I w końcu, jak należy wstrzykiwać sieci do silnika? Myślę o komponencie NetworkComponent, który jest wstrzykiwany do każdej encji, która ma być połączona w sieć. Ale w jaki sposób składnik sieciowy powinien wiedzieć, jakie zmienne podłączyć do sieci i jak uzyskać do nich dostęp, a na koniec, w jaki sposób odpowiedni składnik sieciowy na kliencie powinien wiedzieć, jak zmienić zmienne sieciowe?

Mam ogromne problemy z podejściem do tego. Byłbym bardzo wdzięczny, gdybyś pomógł mi w drodze. Jestem otwarty na wskazówki, jak ulepszyć projekt systemu komponentów, więc nie bój się tego sugerować.

Furman
źródło

Odpowiedzi:

13

To cholerna (przebaczająca) bestia z pytaniem z dużą ilością szczegółów +1. Zdecydowanie wystarczy, aby pomóc ludziom, którzy się na nią natkną.

Po prostu chciałem dodać 2 centy za to, że nie wysyłałem danych fizyki !! Szczerze mówiąc, nie mogę tego wystarczająco podkreślić. Nawet jeśli masz go do tej pory zoptymalizowany, możesz praktycznie wysłać 40 kulek, które odbijają się z mikro kolizją i może osiągnąć pełną prędkość w wstrząsającym pokoju, który nawet nie zmniejsza częstotliwości klatek. Mam na myśli wykonanie „kompresji / kodowania delta”, znanego również jako omawianie różnic danych. Jest dość podobny do tego, co zamierzałem poruszyć.

Dead Reckoning VS Data Differencing: są wystarczająco różne i tak naprawdę nie wykorzystują tych samych metod, co oznacza, że ​​można je wdrożyć, aby jeszcze bardziej zwiększyć optymalizację! Uwaga: nie użyłem ich obu razem, ale pracowałem z obiema.

Kodowanie Delta lub różnicowanie danych: Serwer przenosi dane o tym, co wiedzą klienci, i wysyła tylko różnice między starymi danymi a tym, co należy zmienić. np. pseudo-> w jednym przykładzie możesz wysłać dane „315 435 222 3546 33”, gdy dane są już „310 435 210 4000 40” Niektóre są tylko nieznacznie zmienione, a jeden w ogóle się nie zmienia! Zamiast tego wysłałbyś (w delcie) „5 0 12 -454 -7”, który jest znacznie krótszy.

Lepsze przykłady mogą być czymś, co zmienia się znacznie dalej niż na przykład, powiedzmy, że mam teraz połączoną listę z 45 połączonymi obiektami. Chcę zabić 30 z nich, więc to robię, a następnie przesyłam wszystkim, czym są nowe dane pakietowe, co spowolniłoby serwer, gdyby nie był jeszcze zbudowany do robienia takich rzeczy, i stało się tak, ponieważ próbował na przykład poprawić się. W kodowaniu delta po prostu umieściłbyś (pseudo) „list.kill 30 na 5” i usunąłby 30 obiektów z listy po 5., a następnie uwierzytelniłby dane, ale na każdym kliencie, a nie na serwerze.

Plusy: (Teraz mogę myśleć tylko o jednym z nich)

  1. Szybkość: Oczywiście w moim ostatnim przykładzie opisałem. Byłoby to o wiele większa różnica niż w poprzednim przykładzie. Ogólnie rzecz biorąc, nie mogę uczciwie powiedzieć z doświadczenia, które z nich byłyby bardziej powszechne, ponieważ pracuję o wiele więcej przy martwych rachunkach

Cons:

  1. Jeśli aktualizujesz system i chcesz dodać więcej danych, które powinny być edytowane przez deltę, musisz utworzyć nowe funkcje, aby zmienić te dane! (np. jak wcześniej „list.kill 30 o 5” Cholera, potrzebuję metody cofnięcia dodanej do klienta! ”list.kill cofnij”)

Martwe obliczanie: Mówiąc wprost, oto analogia. Piszę mapę dla kogoś, jak dostać się do lokalizacji, i ogólnie uwzględniam tylko punkty, do których należy się udać, ponieważ jest ona wystarczająco dobra (zatrzymaj się przy budowie, skręć w lewo). Czyjaś mapa zawiera nazwy ulic, a także o ile stopni należy skręcić w lewo, czy to w ogóle jest konieczne? (Nie...)

Martwe rozliczanie polega na tym, że każdy klient ma algorytm stały dla każdego klienta. Dane zmieniają się, mówiąc, które dane należy zmienić i jak to zrobić. Klient sam zmienia dane. Przykładem jest to, że jeśli mam postać, która nie jest moim graczem, ale jest przenoszona przez inną osobę grającą ze mną, nie powinienem aktualizować danych w każdej klatce, ponieważ duża część danych jest spójna!

Powiedzmy, że moja postać porusza się w określonym kierunku, wiele serwerów wysyła dane do klientów, które mówią (prawie na ramkę), gdzie jest gracz i że się porusza (z powodów animacji). To tyle niepotrzebnych danych! Dlaczego, do diabła, muszę aktualizować każdą ramkę, gdzie jest jednostka i w jakim kierunku jest skierowana ORAZ że się porusza? Mówiąc wprost: nie. Aktualizujesz klientów tylko wtedy, gdy zmienia się kierunek, kiedy zmienia się czasownik (isMoving = true?) I czym jest obiekt! Następnie każdy klient odpowiednio przeniesie obiekt.

Osobiście jest to taktyka zdrowego rozsądku. Wydawało mi się, że od dawna sprytnie wpadłem na pomysł, który przez cały czas był wykorzystywany.

Odpowiedzi

Szczerze mówiąc, przeczytaj post Jamesa i przeczytaj, co powiedziałem o danych. Tak, zdecydowanie powinieneś użyć kodowania delta, ale pomyśl także o martwym liczeniu.

Osobiście tworzyłbym dane na kliencie, gdy otrzymuje informacje o nim z serwera (coś, co zasugerowałeś).

Tylko obiekty, które można zmienić, powinny być w pierwszej kolejności oznaczone jako edytowalne, prawda? Podoba mi się pomysł, aby obiekt zawierał dane sieciowe za pośrednictwem systemu komponentów i encji! Jest sprytny i powinien działać dobrze. Ale nigdy nie powinieneś podawać pędzli (ani żadnych danych, które są absolutnie spójne) jakichkolwiek metod sieciowych. Nie potrzebują tego, ponieważ jest to coś, czego nawet nie można zmienić (klient to klient).

Jeśli jest to coś w rodzaju drzwi, dałbym mu dane sieciowe, ale tylko logiczną informację, czy jest otwarta, czy nie, to oczywiście jaki to obiekt. Klient powinien wiedzieć, jak to zmienić, np. Jest otwarty, zamknij go, każdy klient otrzymuje, że wszyscy powinni go zamknąć, więc zmieniasz dane logiczne, a następnie animujesz drzwi do zamknięcia.

Jeśli chodzi o to, jak powinien wiedzieć, jakie zmienne podłączyć do sieci, mogę mieć komponent, który naprawdę jest pod-obiektem, i dać mu komponenty, które chciałbyś połączyć w sieć. Innym pomysłem jest nie tylko mieć, AddComponent("whatever")ale także AddNetComponent("and what have you")dlatego, że osobiście brzmi to mądrzej.

Joshua Hedges
źródło
To absurdalnie długa odpowiedź! Bardzo mi przykro z tego powodu. Ponieważ zamierzałem dostarczyć tylko niewielką ilość wiedzy, a następnie moje 2 centy na niektóre rzeczy. Rozumiem więc, że wiele z nich może być trochę niepotrzebnych do odnotowania.
Joshua Hedges
3

Chciałem napisać komentarz, ale zdecydowałem, że może to być wystarczająca informacja do odpowiedzi.

Po pierwsze +1 za tak ładnie napisane pytanie z mnóstwem szczegółów, według których można ocenić odpowiedź.

Do ładowania danych chciałbym, aby klient załadował świat z pliku world. Jeśli twoje byty mają identyfikatory, które pochodzą z pliku danych, załadowałbym je również domyślnie, aby twój system sieciowy mógł się do nich odwoływać, aby wiedzieć, o których obiektach mówi. Każdy ładujący te same dane początkowe powinien oznaczać, że wszystkie mają takie same identyfikatory dla tych obiektów.

Po drugie, nie twórz składnika NetworkComponent, ponieważ nie zrobiłoby to nic innego, jak replikowanie danych w innych istniejących komponentach (fizyka, animacja i tym podobne to niektóre typowe rzeczy do przesłania). Aby użyć własnego nazewnictwa, możesz chcieć stworzyć NetworkComponentManager. Byłoby to nieco inne od innych relacji między komponentami a ComponentManager, ale może to zostać utworzone natychmiast po uruchomieniu gry w sieci, a wszelkie komponenty, które mają aspekt sieciowy, przekazują swoje dane menedżerowi, aby mógł je spakować i wyślij to. W tym miejscu można użyć funkcji Zapisz / Załaduj, jeśli masz jakiś mechanizm serializacji / deserializacji, którego można również użyć do pakowania danych, jak wspomniano,

Biorąc pod uwagę twoje pytanie i poziom informacji, nie sądzę, żebym musiał szczegółowo omawiać szczegóły, ale jeśli coś jest niejasne, napisz komentarz, a ja zaktualizuję odpowiedź, aby rozwiązać ten problem.

Mam nadzieję że to pomoże.

James
źródło
Mówisz więc, że komponenty, które powinny być połączone w sieć, powinny implementować taki interfejs ?: void SetNetworkedVariable (nazwa ciągu, wartość NetworkedVariable); NetworkedVariable GetNetworkedVariable (nazwa ciągu); Gdzie NetworkedVariable jest używany do celów interpolacji i innych rzeczy sieciowych. Nie wiem jednak, jak rozpoznać, które komponenty to implementują. Mógłbym użyć identyfikacji typu środowiska wykonawczego, ale wydaje mi się to brzydkie.
Carter