Wdrażam wariant systemu encji, który ma:
Klasa Entity czyli niewiele więcej niż ID, który wiąże ze sobą składniki
Kilka klas komponentów , które nie mają „logiki komponentów”, tylko dane
Kilka klas systemowych (zwanych także „podsystemami”, „menedżerami”). Wykonują one wszystkie przetwarzanie logiki encji. W najbardziej podstawowych przypadkach systemy po prostu iterują listę podmiotów, którymi są zainteresowani, i wykonują akcję na każdym z nich
Obiekt klasy MessageChannel, który jest współużytkowany przez wszystkie systemy gier. Każdy system może subskrybować określony rodzaj wiadomości, których ma słuchać, a także może używać kanału do nadawania wiadomości do innych systemów
Początkowy wariant obsługi komunikatów systemowych wyglądał mniej więcej tak:
- Uruchom aktualizację dla każdego systemu gry sekwencyjnie
Jeśli system robi coś ze składnikiem, a działanie to może zainteresować inne systemy, system wysyła odpowiedni komunikat (na przykład, wywołanie systemowe
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
za każdym razem, gdy jednostka jest przenoszona)
Każdy system, który subskrybuje określoną wiadomość, otrzymuje swoją metodę obsługi wiadomości
Jeśli system obsługuje zdarzenie, a logika przetwarzania zdarzenia wymaga wysłania innej wiadomości, wiadomość jest natychmiast wysyłana i wywoływany jest kolejny łańcuch metod przetwarzania wiadomości
Ten wariant był w porządku, dopóki nie zacząłem optymalizować systemu wykrywania kolizji (robiło się naprawdę powoli wraz ze wzrostem liczby jednostek). Na początku po prostu iterowałby każdą parę bytów za pomocą prostego algorytmu brutalnej siły. Następnie dodałem „indeks przestrzenny”, który ma siatkę komórek, która przechowuje jednostki znajdujące się w obszarze określonej komórki, umożliwiając w ten sposób sprawdzanie tylko jednostek w sąsiednich komórkach.
Za każdym razem, gdy jednostka się porusza, system kolizji sprawdza, czy jednostka koliduje z czymś na nowej pozycji. Jeśli tak, kolizja zostanie wykryta. A jeśli obie zderzające się jednostki są „obiektami fizycznymi” (oba mają komponent RigidBody i mają się odepchnąć, aby nie zajmować tej samej przestrzeni), dedykowany sztywny system separacji ciała prosi system ruchu o przeniesienie jednostek do niektórych konkretne pozycje, które je rozdzieliłyby. To z kolei powoduje, że system ruchu wysyła wiadomości powiadamiające o zmianie pozycji bytu. System wykrywania kolizji ma reagować, ponieważ musi zaktualizować swój indeks przestrzenny.
W niektórych przypadkach powoduje to problem, ponieważ zawartość komórki (ogólna lista obiektów encji w języku C #) jest modyfikowana podczas iteracji, co powoduje, że iterator zgłasza wyjątek.
Więc ... jak mogę zapobiec przerwaniu systemu kolizji podczas sprawdzania kolizji?
Oczywiście mógłbym dodać trochę „sprytnej” / „podstępnej” logiki, która zapewnia prawidłowe iterowanie zawartości komórki, ale myślę, że problem nie leży w samym systemie kolizji (miałem również podobne problemy w innych systemach), ale sposób wiadomości są obsługiwane podczas podróży z systemu do systemu. Potrzebuję jakiegoś sposobu, aby zapewnić, że określona metoda obsługi zdarzeń wykona swoje zadanie bez żadnych zakłóceń.
Co próbowałem:
- Przychodzące kolejki wiadomości . Za każdym razem, gdy jakiś system rozgłasza komunikat, jest on dodawany do kolejek systemów, które są nim zainteresowane. Te komunikaty są przetwarzane, gdy aktualizacja systemu jest wywoływana dla każdej ramki. Problem : jeśli system A dodaje komunikat do kolejki systemu B, działa dobrze, jeśli system B ma zostać zaktualizowany później niż system A (w tej samej ramce gry); w przeciwnym razie wiadomość przetworzy następną ramkę gry (nie jest pożądane w niektórych systemach)
- Wychodzące kolejki wiadomości . Podczas gdy system obsługuje zdarzenie, wszystkie wysyłane przez niego wiadomości są dodawane do kolejki wiadomości wychodzących. Wiadomości nie muszą czekać na przetworzenie aktualizacji systemu: są obsługiwane „od razu” po zakończeniu początkowej procedury obsługi komunikatów. Jeśli obsługa wiadomości powoduje emisję innych wiadomości, one również są dodawane do kolejki wychodzącej, więc wszystkie wiadomości są obsługiwane w tej samej ramce. Problem: jeśli system istnienia encji (wdrożyłem zarządzanie cyklem życia encji za pomocą systemu) tworzy encję, powiadamia o tym niektóre systemy A i B. Podczas gdy system A przetwarza komunikat, powoduje to łańcuch komunikatów, które ostatecznie powodują zniszczenie tworzonego bytu (na przykład istota pocisku została utworzona dokładnie tam, gdzie zderza się z jakąś przeszkodą, która powoduje samozniszczenie pocisku). Podczas rozwiązywania łańcucha komunikatów system B nie otrzymuje komunikatu o utworzeniu encji. Tak więc, jeśli system B jest również zainteresowany komunikatem o zniszczeniu bytu, otrzymuje go i dopiero po zakończeniu rozwiązywania „łańcucha” otrzymuje komunikat o początkowym utworzeniu bytu. To powoduje, że komunikat zniszczenia zostaje zignorowany, komunikat o stworzeniu zostaje „zaakceptowany”,
EDYCJA - ODPOWIEDZI NA PYTANIA, KOMENTARZE:
- Kto modyfikuje zawartość komórki, podczas gdy system kolizji iteruje nad nimi?
Podczas gdy system kolizji sprawdza kolizje niektórych jednostek i ich sąsiadów, kolizja może zostać wykryta, a system jednostek wyśle komunikat, na który zareagują natychmiast inne systemy. Reakcja na wiadomość może spowodować, że inne wiadomości zostaną utworzone i obsługiwane od razu. Tak więc inny system może utworzyć komunikat, który system kolizji musiałby natychmiast przetworzyć (na przykład jednostka została przeniesiona, więc system kolizji musi zaktualizować swój indeks przestrzenny), nawet jeśli wcześniejsze kontrole kolizji jeszcze się nie zakończyły.
- Nie możesz pracować z globalną kolejką wiadomości wychodzących?
Ostatnio próbowałem jednej globalnej kolejki. To powoduje nowe problemy. Problem: Przenoszę element czołgu do elementu ściany (zbiornik jest kontrolowany za pomocą klawiatury). Potem postanawiam zmienić kierunek czołgu. Aby oddzielić zbiornik i ścianę od każdej ramki, CollidingRigidBodySeparationSystem odsunął zbiornik od ściany w możliwie najmniejszej ilości. Kierunek separacji powinien być przeciwny do kierunku ruchu czołgu (gdy rozpoczyna się losowanie gry, czołg powinien wyglądać tak, jakby nigdy nie poruszał się w ścianie). Ale kierunek staje się przeciwny do kierunku NOWEGO, co powoduje przesunięcie czołgu na inną stronę ściany niż początkowo. Dlaczego występuje problem: Oto jak teraz obsługiwane są wiadomości (kod uproszczony):
public void Update(int deltaTime)
{
m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
while (m_messageQueue.Count > 0)
{
Message message = m_messageQueue.Dequeue();
this.Broadcast(message);
}
}
private void Broadcast(Message message)
{
if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
{
// NOTE: all IMessageListener objects here are systems.
List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
foreach (IMessageListener listener in messageListeners)
{
listener.ReceiveMessage(message);
}
}
}
Kod płynie w ten sposób (załóżmy, że nie jest to pierwsza ramka gry):
- Systemy zaczynają przetwarzać TimePassedMessage
- InputHandingSystem konwertuje naciśnięcia klawiszy na akcję encji (w tym przypadku lewa strzałka zmienia się w akcję MoveWest). Działanie encji jest przechowywane w komponencie ActionExecutor
- ActionExecutionSystem w reakcji na akcję encji dodaje MovementDirectionChangeRequestedMessage na końcu kolejki komunikatów
- MovementSystem przenosi pozycję encji na podstawie danych komponentu Velocity i dodaje komunikat PositionChangedMessage na końcu kolejki. Ruch odbywa się przy użyciu kierunku / prędkości ruchu poprzedniej klatki (powiedzmy północ)
- Systemy przestają przetwarzać TimePassedMessage
- Systemy zaczynają przetwarzać MovementDirectionChangeRequestedMessage
- MovementSystem zmienia prędkość / kierunek ruchu jednostki zgodnie z żądaniem
- Systemy przestają przetwarzać MovementDirectionChangeRequestedMessage
- Systemy zaczynają przetwarzać PositionChangedMessage
- CollisionDetectionSystem wykrywa, że ponieważ obiekt się poruszył, wpadł na inny byt (zbiornik wszedł w ścianę). Dodaje do kolejki CollisionOccuredMessage
- Systemy przestają przetwarzać PositionChangedMessage
- Systemy zaczynają przetwarzać CollisionOccuredMessage
- CollidingRigidBodySeparationSystem reaguje na kolizję, oddzielając zbiornik i ścianę. Ponieważ ściana jest statyczna, poruszany jest tylko zbiornik. Kierunek ruchu czołgów jest wykorzystywany jako wskaźnik pochodzenia zbiornika. Jest przesunięty w przeciwnym kierunku
BŁĄD: Kiedy czołg poruszał się tą ramą, poruszał się zgodnie z kierunkiem ruchu z poprzedniej ramki, ale kiedy był oddzielany, stosowano kierunek ruchu z TEJ ramki, chociaż była już inna. To nie tak powinno działać!
Aby zapobiec temu błędowi, trzeba gdzieś zapisać stary kierunek ruchu. Mógłbym dodać go do jakiegoś komponentu, aby naprawić ten konkretny błąd, ale czy ten przypadek nie wskazuje na jakiś zasadniczo zły sposób obsługi wiadomości? Dlaczego system separacji powinien dbać o to, jakiego kierunku ruchu używa? Jak mogę elegancko rozwiązać ten problem?
- Możesz przeczytać gamadu.com/artemis, aby zobaczyć, co zrobili z Aspectami, po której stronie znajdują się niektóre z problemów, które widzisz.
Właściwie od dłuższego czasu znam Artemis. Zbadałem jego kod źródłowy, czytałem fora itp. Ale widziałem, że „Aspekty” wymieniane są tylko w kilku miejscach i, o ile rozumiem, w zasadzie oznaczają „Systemy”. Ale nie widzę, jak Artemida rozwiązuje niektóre z moich problemów. Nawet nie używa wiadomości.
- Zobacz także: „Komunikacja jednostek: kolejka komunikatów vs publikowanie / subskrybowanie vs sygnał / sloty”
Przeczytałem już wszystkie pytania gamedev.stackexchange dotyczące systemów jednostek. Ten wydaje się nie omawiać problemów, przed którymi stoję. Czy coś brakuje?
- Obie sprawy traktuj inaczej, aktualizacja siatki nie musi polegać na komunikatach ruchowych, ponieważ jest to część systemu kolizji
Nie jestem pewny co masz na myśli. Starsze implementacje CollisionDetectionSystem po prostu sprawdzały kolizje podczas aktualizacji (gdy obsługiwano TimePassedMessage), ale musiałem zminimalizować kontrole tak bardzo, jak mogłem ze względu na wydajność. Więc przełączyłem się na sprawdzanie kolizji, gdy jednostka się porusza (większość jednostek w mojej grze jest statyczna).