Jak zdecydować, który GameObject powinien poradzić sobie z kolizją?

28

W każdej kolizji występują dwa GameObjects, prawda? Chcę wiedzieć, jak zdecydować, który obiekt powinien zawierać mój OnCollision*?

Jako przykład załóżmy, że mam obiekt Player i obiekt Spike. Moją pierwszą myślą jest umieszczenie w odtwarzaczu skryptu zawierającego taki kod:

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Spike")) {
        Destroy(gameObject);
    }
}

Oczywiście taką samą funkcjonalność można uzyskać, zamiast tego mając skrypt na obiekcie Spike, który zawiera kod taki jak ten:

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Player")) {
        Destroy(coll.gameObject);
    }
}

Choć obie są poprawne, to więcej sensu dla mnie mieć skrypt na odtwarzaczu , ponieważ w tym przypadku, gdy w wyniku zderzenia się dzieje, akcja jest wykonywana na odtwarzaczu .

Jednak wątpię w to, że w przyszłości możesz chcieć dodać więcej obiektów, które zabiją gracza podczas kolizji, takich jak wróg, lawa, wiązka laserowa itp. Te obiekty prawdopodobnie będą miały inne znaczniki. Zatem skrypt w odtwarzaczu stałby się:

OnCollisionEnter(Collision coll) {
    GameObject other = coll.gameObject;
    if (other.compareTag("Spike") || other.compareTag("Lava") || other.compareTag("Enemy")) {
        Destroy(gameObject);
    }
}

Podczas gdy w przypadku, gdy skrypt znajdował się na Spike, wystarczy, że dodasz ten sam skrypt do wszystkich innych obiektów, które mogą zabić Gracza, i nadasz mu nazwę skryptu KillPlayerOnContact.

Ponadto, jeśli masz kolizję między Graczem a wrogiem, prawdopodobnie chcesz wykonać akcję na obu . Więc w takim przypadku, który obiekt powinien poradzić sobie z kolizją? A może obie powinny poradzić sobie z kolizją i wykonywać różne czynności?

Nigdy wcześniej nie budowałem żadnej gry o rozsądnych rozmiarach i zastanawiam się, czy kod może stać się niechlujny i trudny w utrzymaniu, ponieważ rośnie, jeśli na początku coś takiego się nie powiedzie. A może wszystkie sposoby są prawidłowe i to naprawdę nie ma znaczenia?


Każdy wgląd jest mile widziany! Dziękuję za Twój czas :)

Redtama
źródło
Po pierwsze, dlaczego literały łańcuchowe dla tagów? Wyliczanie jest znacznie lepsze, ponieważ literówka zamieni się w błąd kompilacji zamiast trudnego do wyśledzenia błędu (dlaczego mój kolec mnie nie zabija?).
maniak zapadkowy
Ponieważ śledziłem samouczki Unity i tak właśnie zrobili. Czy miałbyś po prostu publiczne wyliczenie o nazwie Tag, które zawiera wszystkie wartości tagów? Więc byłoby Tag.SPIKEzamiast tego?
Redtama,
Aby jak najlepiej wykorzystać oba światy, rozważ użycie struktury z wyliczeniem w środku, a także statycznych metod ToString i FromString
zcabjro

Odpowiedzi:

48

Ja osobiście wolę tak projektować systemy, aby żaden obiekt biorący udział w kolizji nie obsługiwał go, a zamiast tego niektóre proxy obsługujące kolizję mogłyby obsłużyć go za pomocą wywołania zwrotnego HandleCollisionBetween(GameObject A, GameObject B). W ten sposób nie muszę myśleć o tym, gdzie powinna być logika.

Jeśli nie ma takiej opcji, na przykład gdy nie masz kontroli nad architekturą reagowania na kolizje, sprawy stają się nieco rozmyte. Ogólnie odpowiedź brzmi „to zależy”.

Ponieważ oba obiekty otrzymają wywołania zwrotne kolizji, umieściłem kod w obu, ale tylko w kodzie, który jest istotny dla roli tego obiektu w świecie gry , jednocześnie starając się zachować jak największą rozszerzalność. Oznacza to, że jeśli jakakolwiek logika ma jakikolwiek sens w obu obiektach, wolę umieścić ją w obiekcie, który prawdopodobnie doprowadzi do mniejszej liczby zmian kodu na dłuższą metę wraz z dodaniem nowej funkcjonalności.

Innymi słowy, pomiędzy dowolnymi dwoma typami obiektów, które mogą kolidować, jeden z tych typów obiektów generalnie ma taki sam wpływ na więcej typów obiektów niż drugi. Umieszczenie kodu dla tego efektu w typie, który wpływa na więcej innych typów, oznacza mniej konserwacji na dłuższą metę.

Na przykład obiekt gracza i obiekt pocisku. Prawdopodobnie „przyjmowanie obrażeń w wyniku zderzenia z kulami” ma logiczny sens jako część gracza. „Zadawanie obrażeń trafionej rzeczy” ma logiczny sens jako część kuli. Jednak kula prawdopodobnie spowoduje uszkodzenie bardziej różnych rodzajów obiektów niż gracz (w większości gier). Gracz nie odniesie obrażeń z bloków terenu lub postaci niezależnych, ale kula nadal może wyrządzić im obrażenia. W takim razie wolałbym umieścić obsługę kolizji w celu spowodowania uszkodzenia pocisku.

Josh
źródło
1
Uważam, że odpowiedzi wszystkich są naprawdę pomocne, ale muszę zaakceptować twoje dla ich jasności. Twój ostatni akapit naprawdę to dla mnie podsumowuje :) Niezwykle pomocny! Dziękuję bardzo!
Redtama,
12

TL; DR Najlepszym miejscem do umieszczenia detektora kolizji jest miejsce, w którym uważasz, że jest to aktywny element w kolizji, zamiast elementu pasywnego w kolizji, ponieważ być może będziesz potrzebować dodatkowego zachowania, aby wykonać taką aktywną logikę (i przestrzeganie dobrych wytycznych OOP, takich jak SOLID, jest obowiązkiem aktywnego elementu).

Szczegółowe :

Zobaczmy ...

Moje podejście, gdy mam te same wątpliwości, niezależnie od silnika gry , polega na ustaleniu, które zachowanie jest aktywne, a które pasywne w stosunku do akcji, którą chcę uruchomić podczas zbierania.

Uwaga: Używam różnych zachowań jako abstrakcji różnych części naszej logiki, aby zdefiniować szkody i - jako kolejny przykład - inwentarz. Oba są osobnymi zachowaniami, które dodam później do Gracza, i nie zagracają mojego niestandardowego Gracza osobnymi obowiązkami, które byłyby trudniejsze do utrzymania. Używając adnotacji, takich jak RequireComponent, upewniłem się, że moje zachowanie odtwarzacza ma również zachowania, które teraz definiuję.

To tylko moja opinia: unikaj wykonywania logiki w zależności od znacznika, ale zamiast tego używaj różnych zachowań i wartości, takich jak wyliczenia, aby wybierać .

Wyobraź sobie te zachowania:

  1. Zachowanie określające obiekt jako stos na ziemi. Gry RPG używają tego do zbierania przedmiotów w ziemi.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
    
  2. Zachowanie w celu określenia obiektu zdolnego do zadawania obrażeń. Może to być lawa, kwas, każda substancja toksyczna lub latający pocisk.

    public class DamageDealer : MonoBehaviour {
        public Faction facton; //let's use it as well..
        public DamageType damageType = DamageType.BLUNT;
        public uint damage = 1;
    }
    

W tym zakresie rozważ DamageTypei InventoryObjectjako wyliczenia.

Zachowania te nie są aktywne , ponieważ będąc na ziemi lub latając, nie działają one same. Oni nic nie robią. Np. Jeśli masz kulę ognistą lecącą w pustym miejscu, nie jest ona gotowa do wyrządzenia obrażeń (być może inne zachowania powinny być uważane za aktywne w kuli ognistej, np. Kula ognista pochłania swoją moc podczas podróży i zmniejsza moc zadawania obrażeń w trakcie procesu kiedy potencjalne FuelingOutzachowanie może zostać opracowane, pozostaje to inne zachowanie, a nie to samo zachowanie DamageProducer), a jeśli widzisz obiekt w ziemi, nie sprawdza on wszystkich użytkowników i nie myśli, czy mogą mnie wybrać?

Potrzebujemy do tego aktywnych zachowań. Odpowiednikami byłyby:

  1. Jedno zachowanie, by przyjąć obrażenia (wymagałoby zachowania, które poradziłoby sobie z życiem i śmiercią, ponieważ obniżanie życia i otrzymywanie obrażeń są zwykle osobnymi zachowaniami, a ponieważ chcę, aby te przykłady były jak najprostsze) i być może opierać się obrażeniom.

    public class DamageReceiver : MonoBehaviour {
        public DamageType[] weakness;
        public DamageType[] halfResistance;
        public DamageType[] fullResistance;
        public Faction facton; //let's use it as well..
    
        private List<DamageDealer> dealers = new List<DamageDealer>(); 
    
        public void OnCollisionEnter(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && !this.dealers.Contains(dealer)) {
                this.dealers.Add(dealer);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && this.dealers.Contains(dealer)) {
                this.dealers.Remove(dealer);
            }
        }
    
        public void Update() {
            // actually, this should not run on EVERY iteration, but this is just an example
            foreach (DamageDealer element in this.dealers)
            {
                if (this.faction != element.faction) {
                    if (this.weakness.Contains(element)) {
                        this.SubtractLives(2 * element.damage);
                    } else if (this.halfResistance.Contains(element)) {
                        this.SubtractLives(0.5 * element.damage);
                    } else if (!this.fullResistance.Contains(element)) {
                        this.SubtractLives(element.damage);
                    }
                }
            }
        }
    
        private void SubtractLives(uint lives) {
            // implement it later
        }
    }
    

    Ten kod jest nieprzetestowany i powinien działać jako koncepcja . Jaki jest cel Uszkodzenie jest czymś, co ktoś odczuwa i jest związane z uszkodzeniem obiektu (lub formy żywej), jak się uważa. Oto przykład: w zwykłych grach RPG miecz cię uszkadza , podczas gdy w innych grach RPG go implementujących (np. Summon Night Swordcraft Story I i II ) miecz może również otrzymać obrażenia, trafiając w twardy element lub blokując zbroję, i może potrzebować naprawy . Ponieważ więc logika uszkodzeń odnosi się do odbiorcy, a nie do nadawcy, umieszczamy tylko odpowiednie dane w nadawcy, a logikę w odbiorcy.

  2. To samo dotyczy posiadacza ekwipunku (inne wymagane zachowanie dotyczyłoby obiektu znajdującego się na ziemi i możliwości jego usunięcia z tego miejsca). Być może jest to właściwe zachowanie:

    public class Inventory : MonoBehaviour {
        private Dictionary<InventoryObject, uint> = new Dictionary<InventoryObject, uint>();
        private List<Treasure> pickables = new List<Treasure>();
    
        public void OnCollisionEnter(Collision col) {
            Treasure treasure = col.gameObject.GetComponent<Treasure>();
            if (treasure != null && !this.pickables.Contains(treasure)) {
                this.pickables.Add(treasure);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer treasure = col.gameObject.GetComponent<DamageDealer>();
            if (treasure != null && this.pickables.Contains(treasure)) {
                this.pickables.Remove(treasure);
            }
        }
    
        public void Update() {
            // when certain key is pressed, pick all the objects and add them to the inventory if you have a free slot for them. also remove them from the ground.
        }
    }
    
Luis Masuelli
źródło
7

Jak widać na podstawie różnorodnych odpowiedzi tutaj, w podejmowaniu tych decyzji jest dość osobisty styl, a ocena oparta na potrzebach konkretnej gry - nie ma absolutnie najlepszego podejścia.

Moim zaleceniem jest przemyślenie tego pod kątem skalowania twojego podejścia wraz z rozwojem gry , dodawaniem i powtarzaniem funkcji. Czy umieszczenie logiki obsługi kolizji w jednym miejscu doprowadzi później do bardziej złożonego kodu? Czy obsługuje bardziej modularne zachowanie?

Masz więc kolce, które powodują obrażenia gracza. Zapytaj siebie:

  • Czy są inne rzeczy niż gracz, które kolce mogą chcieć uszkodzić?
  • Czy są inne rzeczy niż kolce, od których gracz powinien otrzymać obrażenia podczas kolizji?
  • Czy każdy poziom z graczem będzie miał kolce? Czy każdy poziom z kolcami będzie miał gracza?
  • Czy możesz mieć różne warianty kolców, które muszą robić różne rzeczy? (np. różne kwoty obrażeń / rodzaje uszkodzeń?)

Oczywiście nie chcemy dać się ponieść emocjom i zbudować nadmiernie skonstruowany system, który może obsłużyć milion skrzynek zamiast tylko jednego przypadku, którego potrzebujemy. Ale jeśli w obu przypadkach jest to równa praca (tj. Umieść te 4 wiersze w klasie A w porównaniu z klasą B), ale jedna skaluje się lepiej, jest to dobra ogólna zasada przy podejmowaniu decyzji.

W tym przykładzie, w szczególności w Unity, skłaniam się ku umieszczeniu logiki zadawania obrażeń w kolcach , w postaci czegoś w rodzaju CollisionHazardkomponentu, który po zderzeniu nazywa metodę przyjmowania obrażeń w Damageablekomponencie. Dlatego:

  • Nie musisz odkładać znacznika dla każdego innego uczestnika kolizji, który chcesz wyróżnić.
  • W miarę dodawania kolejnych odmian zderzających się interakcji (np. Lawy, pocisków, sprężyn, ulepszeń ...) klasa gracza (lub komponent do uszkodzenia) nie gromadzi gigantycznego zestawu skrzynek if / else / switch do obsługi każdego z nich.
  • Jeśli połowa twoich poziomów nie ma w nich żadnych kolców, nie marnujesz czasu w programie obsługi kolizji gracza, sprawdzając, czy kolec był zaangażowany. Kosztują tylko wtedy, gdy są zaangażowani w kolizję.
  • Jeśli później zdecydujesz, że kolce powinny zabijać wrogów i rekwizyty, nie musisz kopiować kodu z klasy specyficznej dla gracza, po prostu przyklej Damageablena nich komponent (jeśli potrzebujesz odporności na same kolce, możesz dodać rodzaje uszkodzeń i pozwól, aby Damageablekomponent obsługiwał immunitety)
  • Jeśli pracujesz w zespole, osoba, która musi zmienić zachowanie szczytowe i sprawdzi klasę zagrożenia / zasoby, nie blokuje wszystkich, którzy muszą zaktualizować interakcje z graczem. Dla nowego członka zespołu (zwłaszcza projektantów poziomów) intuicyjny jest także skrypt, na który musi patrzeć, aby zmienić zachowanie skoków - to ten dołączony do skoków!
DMGregory
źródło
Istnieje system Entity-Component, aby uniknąć takich IF. Te IF zawsze się mylą (tylko te if). Dodatkowo, jeśli kolec się porusza (lub jest pociskiem), moduł obsługi kolizji zostanie uruchomiony, mimo że kolidującym obiektem jest gracz.
Luis Masuelli,
2

Naprawdę nie ma znaczenia, kto poradzi sobie z kolizją, ale bądź spójny z czyją to pracą.

W moich grach staram się wybrać GameObjectcoś, co „robi” coś drugiemu obiektowi, jako tego, który kontroluje kolizję. IE Spike uszkadza gracza, więc obsługuje kolizję. Kiedy oba obiekty „robią” coś ze sobą, niech każdy z nich poradzi sobie z ich działaniem na drugim obiekcie.

Proponuję również spróbować zaprojektować kolizje tak, aby kolizje były obsługiwane przez obiekt, który reaguje na kolizje za pomocą mniejszej liczby rzeczy. Spike musi tylko wiedzieć o kolizjach z odtwarzaczem, dzięki czemu kod kolizji w skrypcie Spike jest ładny i zwarty. Obiekt odtwarzacza może kolidować z wieloma innymi rzeczami, co spowoduje rozdęty skrypt kolizji.

Na koniec postaraj się, aby procedury obsługi kolizji były bardziej abstrakcyjne. Kolec nie musi wiedzieć, że zderzył się z graczem, musi tylko wiedzieć, że zderzył się z czymś, czemu musi zadać obrażenia. Powiedzmy, że masz skrypt:

public abstract class DamageController
{
    public Faction faction; //GOOD or BAD
    public abstract void ApplyDamage(float damage);
}

Możesz DamageControllerpodklasować w GameObject gracza, aby poradzić sobie z uszkodzeniem, a następnie wprowadzić Spikekod kolizji

OnCollisionEnter(Collision coll) {
    DamageController controller = coll.gameObject.GetComponent<DamageController>();
    if(controller){
        if(controller.faction == Faction.GOOD){
          controller.ApplyDamage(spikeDamage);
        }
    }
}
spectacularbob
źródło
2

Miałem ten sam problem, kiedy zaczynałem, więc oto: W tym konkretnym przypadku użyj wroga. Zwykle chcesz, aby kolizją zajmował się obiekt, który wykonuje akcję (w tym przypadku - wróg, ale jeśli twój gracz miał broń, broń powinna poradzić sobie z kolizją), ponieważ jeśli chcesz poprawić atrybuty (na przykład obrażenia wroga) zdecydowanie chcesz to zmienić w enemyscript, a nie w scripts (szczególnie jeśli masz wielu graczy). Podobnie, jeśli chcesz zmienić obrażenia jakiejkolwiek broni, której używa Gracz, na pewno nie chcesz jej zmieniać u każdego wroga twojej gry.

Treeton
źródło
2

Oba obiekty poradzą sobie z kolizją.

Niektóre obiekty zostaną natychmiast zdeformowane, złamane lub zniszczone w wyniku kolizji. (kule, strzały, balony z wodą)

Inni po prostu odbijali się i przewracali na bok. (skały) Mogą one nadal otrzymywać obrażenia, jeśli trafiona rzecz była wystarczająco trudna. Skały nie otrzymują obrażeń od uderzającego w nie człowieka, ale mogą pochodzić od młota.

Niektóre miękkie przedmioty, takie jak ludzie, odniosą obrażenia.

Inni byliby ze sobą związani. (magnesy)

Obie kolizje zostaną umieszczone w kolejce obsługi zdarzeń dla aktora działającego na obiekcie w określonym czasie i miejscu (i prędkości). Pamiętaj tylko, że program obsługi powinien zajmować się tylko tym, co dzieje się z obiektem. Możliwe jest nawet, że moduł obsługi umieści innego modułu obsługi w kolejce, aby obsłużyć dodatkowe efekty kolizji w oparciu o właściwości aktora lub obiektu, na którym działa. (Bomba wybuchła, a wynikająca z niej eksplozja musi teraz przetestować kolizje z innymi obiektami).

Podczas pisania skryptów używaj znaczników z właściwościami, które zostałyby przetłumaczone na implementacje interfejsu przez Twój kod. Następnie zapoznaj się z interfejsami w kodzie obsługi.

Na przykład miecz może mieć znacznik Walki Wręcz, co przekłada się na interfejs o nazwie Walka Wręcz, który może rozszerzyć interfejs Obrażeń, który ma właściwości dla rodzaju obrażeń i wielkości obrażeń określonych za pomocą stałej wartości lub zasięgu itp. A bomba może mieć znacznik wybuchowy, którego interfejs wybuchowy rozszerza również interfejs DamageGiving, który ma dodatkową właściwość dla promienia i możliwie jak szybko rozproszenie obrażeń.

Twój silnik gry kodowałby następnie interfejsy, a nie określone typy obiektów.

Rick Ryker
źródło