Come decidere quale GameObject dovrebbe gestire la collisione?


28

In ogni collisione, ci sono due GameObject coinvolti, giusto? Quello che voglio sapere è, come faccio a decidere quale oggetto dovrebbe contenere il mio OnCollision*?

Ad esempio, supponiamo che io abbia un oggetto Player e un oggetto Spike. Il mio primo pensiero è quello di mettere uno script sul lettore che contenga un codice come questo:

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

Naturalmente, la stessa identica funzionalità può essere ottenuta invece con uno script sull'oggetto Spike che contiene codice come questo:

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

Sebbene entrambi siano validi, per me ha più senso avere la sceneggiatura sul Player perché, in questo caso, quando si verifica la collisione, viene eseguita un'azione sul Player .

Tuttavia, ciò che mi fa dubitare di ciò è che in futuro potresti voler aggiungere altri oggetti che uccideranno il giocatore in caso di collisione, come un nemico, una lava, un raggio laser, ecc. Questi oggetti probabilmente avranno tag diversi. Quindi lo script sul Player diventerebbe:

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

Considerando che, nel caso in cui lo script fosse su Spike, tutto ciò che dovresti fare è aggiungere lo stesso script a tutti gli altri oggetti che possono uccidere il Player e nominare lo script in modo simile KillPlayerOnContact.

Inoltre, se hai una collisione tra il giocatore e un nemico, probabilmente vorrai eseguire un'azione su entrambi . Quindi, in tal caso, quale oggetto dovrebbe gestire la collisione? O entrambi dovrebbero gestire la collisione ed eseguire azioni diverse?

Non ho mai costruito un gioco di dimensioni ragionevoli prima e mi chiedo se il codice può diventare disordinato e difficile da mantenere man mano che cresce se si sbaglia questo tipo di cose all'inizio. O forse tutti i modi sono validi e non importa davvero?


Qualsiasi intuizione è molto apprezzata! Grazie per il tuo tempo :)


Prima perché i letterali stringa per i tag? Un enum è molto meglio perché un refuso si trasformerà in un errore di compilazione anziché in un bug difficile da rintracciare (perché il mio picco non mi sta uccidendo?).
maniaco del cricchetto

Perché ho seguito i tutorial di Unity ed è quello che hanno fatto. Quindi, avresti solo un enum pubblico chiamato Tag che contiene tutti i valori dei tag? Quindi sarebbe Tag.SPIKEinvece?
Redtama,

Per ottenere il meglio da entrambi i mondi, prendi in considerazione l'uso di una struttura, con un enum interno, nonché metodi ToString e FromString statici
zcabjro

Risposte:


48

Personalmente preferisco progettare sistemi in modo tale che nessuno degli oggetti coinvolti in una collisione lo gestisca, e invece alcuni proxy di gestione delle collisioni riescono a gestirlo con un callback come HandleCollisionBetween(GameObject A, GameObject B). In questo modo non devo pensare a quale logica dovrebbe essere dove.

Se questa non è un'opzione, come quando non hai il controllo dell'architettura di risposta alla collisione, le cose diventano un po 'sfocate. Generalmente la risposta è "dipende".

Dato che entrambi gli oggetti riceveranno callback di collisione, inserirò il codice in entrambi, ma solo il codice che è rilevante per il ruolo di quell'oggetto nel mondo di gioco , cercando anche di mantenere l'estensibilità il più possibile. Ciò significa che se una logica ha lo stesso senso in entrambi gli oggetti, preferisce inserirla nell'oggetto che probabilmente porterà a un minor numero di modifiche al codice nel lungo periodo con l'aggiunta di nuove funzionalità.

Detto in altro modo, tra due tipi di oggetti che possono scontrarsi, uno di questi tipi di oggetto ha generalmente lo stesso effetto su più tipi di oggetto rispetto all'altro. Inserire il codice per quell'effetto nel tipo che interessa più altri tipi significa meno manutenzione a lungo termine.

Ad esempio, un oggetto giocatore e un oggetto proiettile. Probabilmente, "subire danni in caso di collisione con proiettili" ha un senso logico come parte del giocatore. "Applicare danni alla cosa che colpisce" ha un senso logico come parte del proiettile. Tuttavia, è probabile che un proiettile danneggi più tipi di oggetti diversi rispetto a un giocatore (nella maggior parte dei giochi). Un giocatore non subirà danni da blocchi di terreno o NPC, ma un proiettile potrebbe comunque infliggere danni a quelli. Quindi preferirei mettere la gestione delle collisioni per infliggere danni al proiettile, in quel caso.


1
Ho trovato le risposte di tutti davvero utili, ma devo accettare le tue per la sua chiarezza. Il tuo ultimo paragrafo lo riassume davvero per me :) Estremamente utile! Grazie mille!
Redtama,

12

TL; DR Il posto migliore per posizionare il rilevatore di collisione è il luogo in cui si considera che è l' elemento attivo nella collisione, anziché l' elemento passivo nella collisione, perché forse sarà necessario un comportamento aggiuntivo da eseguire in tale logica attiva (e, seguendo le buone linee guida OOP come SOLID, è la responsabilità dell'elemento attivo ).

Dettagliato :

Vediamo ...

Il mio approccio quando ho lo stesso dubbio, indipendentemente dal motore di gioco , è determinare quale comportamento è attivo e quale comportamento è passivo rispetto all'azione che voglio innescare al momento della raccolta.

Nota: utilizzo comportamenti diversi come astrazioni delle diverse parti della nostra logica per definire il danno e, come altro esempio, l'inventario. Entrambi sono comportamenti separati che aggiungerei in seguito a un Giocatore e non ingombrano il mio Giocatore personalizzato con responsabilità separate che sarebbe più difficile da mantenere. Usando annotazioni come RequireComponent, mi assicurerei che anche il comportamento del mio lettore abbia avuto i comportamenti che sto definendo ora.

Questa è solo la mia opinione: evitare di eseguire la logica in base al tag, ma invece utilizzare comportamenti e valori diversi come enum tra cui scegliere .

Immagina questi comportamenti:

  1. Comportamento per specificare un oggetto come pila sul terreno. I giochi di ruolo usano questo per oggetti nel terreno che possono essere raccolti.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
  2. Comportamento per specificare un oggetto in grado di produrre danno. Potrebbe trattarsi di lava, acido, qualsiasi sostanza tossica o proiettile volante.

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

In questa misura, considera DamageTypee InventoryObjectcome enumerazioni.

Questi comportamenti non sono attivi , nel modo in cui essendo a terra o volando in giro non agiscono da soli. Non fanno niente. Ad esempio se hai una palla di fuoco che vola in uno spazio vuoto, non è pronta a fare danni (forse altri comportamenti dovrebbero essere considerati attivi in ​​una palla di fuoco, come la palla di fuoco che consuma il suo potere mentre viaggia e diminuisce il suo potere di danno nel processo, ma anche quando un potenziale FuelingOutcomportamento può essere sviluppato, è un altro comportamento, e non lo stesso comportamento DamageProducer), e se vedi un oggetto nel terreno, non sta controllando tutti gli utenti e pensando che possono scegliermi?

Abbiamo bisogno di comportamenti attivi per questo. Le controparti sarebbero:

  1. Un comportamento per subire danni (richiederebbe un comportamento per gestire la vita e la morte, dal momento che abbassare la vita e ricevere danni sono di solito comportamenti separati e perché voglio mantenere questi esempi il più semplice possibile) e forse resistere al danno.

    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
        }
    }

    Questo codice non è testato e dovrebbe funzionare come un concetto . Qual è lo scopo? Il danno è qualcosa che qualcuno sente ed è relativo all'oggetto (o alla forma vivente) che viene danneggiato, come consideri. Questo è un esempio: nei normali giochi di ruolo, una spada ti danneggia , mentre in altri giochi di ruolo che la implementano (ad esempio Summon Night Swordcraft Story I e II ), una spada può anche subire danni colpendo una parte dura o bloccando un'armatura, e potrebbe essere necessario riparazioni . Pertanto, poiché la logica del danno è relativa al destinatario e non al mittente, inseriamo solo i dati rilevanti nel mittente e la logica nel destinatario.

  2. Lo stesso vale per un detentore dell'inventario (un altro comportamento richiesto sarebbe quello relativo all'oggetto sul terreno e alla possibilità di rimuoverlo da lì). Forse questo è il comportamento appropriato:

    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.
        }
    }

7

Come puoi vedere dalla varietà di risposte qui, c'è un buon grado di stile personale coinvolto in queste decisioni e il giudizio basato sulle esigenze del tuo gioco particolare - nessuno il miglior approccio assoluto.

La mia raccomandazione è di pensarci in termini di come il tuo approccio si ridimensionerà man mano che cresci il tuo gioco , aggiungendo e ripetendo le funzionalità. Mettere la logica di gestione delle collisioni in un posto porterà a un codice più complesso in seguito? O supporta un comportamento più modulare?

Quindi, hai punte che danneggiano il giocatore. Chiedilo a te stesso:

  • Ci sono cose diverse dal giocatore che le punte potrebbero voler danneggiare?
  • Ci sono cose diverse dai picchi che il giocatore dovrebbe subire danni in caso di collisione?
  • Ogni livello con un giocatore avrà punte? Ogni livello con punte avrà un giocatore?
  • Potresti avere variazioni sui picchi che devono fare cose diverse? (es. diversi importi di danno / tipi di danno?)

Ovviamente, non vogliamo lasciarci trasportare da questo e costruire un sistema troppo ingegnerizzato in grado di gestire un milione di casi invece del solo caso di cui abbiamo bisogno. Ma se è uguale lavoro in entrambi i modi (es. Metti queste 4 righe in classe A vs classe B) ma una scala meglio, è una buona regola empirica per prendere la decisione.

Per questo esempio, in Unity in particolare, mi spingerei a mettere la logica di infliggere danno nei picchi , sotto forma di qualcosa di simile a un CollisionHazardcomponente, che in caso di collisione chiama un metodo di infliggere danno in un Damageablecomponente. Ecco perché:

  • Non è necessario mettere da parte un tag per ogni diverso partecipante alla collisione che si desidera distinguere.
  • Man mano che aggiungi più varietà di interazioni collezionabili (ad es. Lava, proiettili, molle, potenziamenti ...) la tua classe di giocatori (o componente Damageable) non accumula un set gigante di casi if / else / switch per gestirli.
  • Se metà dei tuoi livelli finiscono per non avere punte, non stai perdendo tempo nel gestore delle collisioni del giocatore che controlla se è stata coinvolta una punta. Ti costano solo quando sono coinvolti nella collisione.
  • Se in seguito decidi che anche i picchi dovrebbero uccidere nemici e oggetti di scena, non è necessario duplicare il codice da una classe specifica del giocatore, basta attaccare il Damageablecomponente su di essi (se ne hai bisogno per essere immune ai picchi, puoi aggiungere tipi di danno e lasciare che il Damageablecomponente gestisca le immunità)
  • Se stai lavorando in una squadra, la persona che deve cambiare il comportamento del picco e verifica la classe / le risorse di pericolo non sta bloccando tutti coloro che devono aggiornare le interazioni con il giocatore. È anche intuitivo per un nuovo membro del team (in particolare i progettisti di livelli) quale script devono guardare per cambiare i comportamenti degli spike - è quello attaccato agli spike!

Il sistema Entity-Component esiste per evitare IF del genere. Questi IF sono sempre sbagliati (solo quegli if). Inoltre, se il picco si sta muovendo (o è un proiettile), l'handler di collisione verrà attivato nonostante l'oggetto in collisione sia o meno il giocatore.
Luis Masuelli,

2

Non importa davvero chi gestisce la collisione, ma sii coerente con il lavoro che svolge.

Nei miei giochi, provo a scegliere GameObjectciò che "fa" qualcosa all'altro oggetto come colui che controlla la collisione. IE Spike danneggia il giocatore in modo che gestisca la collisione. Quando entrambi gli oggetti "fanno" qualcosa l'uno con l'altro, ognuno deve gestire la propria azione sull'altro oggetto.

Inoltre, suggerirei di provare a progettare le collisioni in modo che le collisioni vengano gestite dall'oggetto che reagisce alle collisioni con un numero inferiore di cose. Uno spike dovrà solo conoscere le collisioni con il giocatore, rendendo il codice di collisione nello script Spike bello e compatto. L'oggetto giocatore può scontrarsi con molte altre cose che faranno uno script di collisione gonfio.

Infine, prova a rendere i gestori delle collisioni più astratti. Uno Spike non ha bisogno di sapere che si è scontrato con un giocatore, deve solo sapere che si è scontrato con qualcosa a cui ha bisogno per infliggere danno. Diciamo che hai una sceneggiatura:

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

Puoi subclassare DamageControllernel tuo GameObject del giocatore per gestire il danno, quindi nel Spikecodice di collisione

OnCollisionEnter(Collision coll) {
    DamageController controller = coll.gameObject.GetComponent<DamageController>();
    if(controller){
        if(controller.faction == Faction.GOOD){
          controller.ApplyDamage(spikeDamage);
        }
    }
}

2

Ho avuto lo stesso problema quando ho iniziato, quindi eccolo qui: in questo caso particolare usa il nemico. Di solito vuoi che la collisione venga gestita dall'oggetto che esegue l'azione (in questo caso - il nemico, ma se il tuo giocatore aveva un'arma, l'arma dovrebbe gestire la collisione), perché se vuoi modificare gli attributi (ad esempio il danno del nemico) vuoi decisamente cambiarlo nel cliché e non nel giocatore (specialmente se hai più giocatori). Allo stesso modo se vuoi cambiare il danno di un'arma che il giocatore usa sicuramente non vuoi cambiarlo in ogni nemico del tuo gioco.


2

Entrambi gli oggetti gestiranno la collisione.

Alcuni oggetti sarebbero immediatamente deformati, rotti o distrutti dalla collisione. (proiettili, frecce, palloncini d'acqua)

Altri rimbalzavano e rotolavano di lato. (rocce) Questi potrebbero ancora subire danni se la cosa che colpiscono fosse abbastanza dura. Le rocce non subiscono danni da un umano che le colpisce, ma potrebbero da una mazza.

Alcuni oggetti squishy come gli umani subirebbero danni.

Altri sarebbero legati l'uno all'altro. (magneti)

Entrambe le collisioni verrebbero messe in una coda di gestione degli eventi per un attore che agisce su un oggetto in un determinato momento e luogo (e velocità). Ricorda solo che il gestore dovrebbe occuparsi solo di ciò che accade all'oggetto. È anche possibile che un gestore inserisca un altro gestore sulla coda per gestire ulteriori effetti della collisione in base alle proprietà dell'attore o dell'oggetto su cui si recita. (La bomba è esplosa e l'esplosione risultante ora deve verificare le collisioni con altri oggetti.)

Durante lo scripting, utilizzare i tag con proprietà che verrebbero tradotte in implementazioni di interfaccia dal proprio codice. Quindi fare riferimento alle interfacce nel codice di gestione.

Ad esempio, una spada potrebbe avere un tag per Melee che si traduce in un'interfaccia chiamata Melee che potrebbe estendere un'interfaccia DamageGiving che ha proprietà per tipo di danno e un ammontare di danno definito usando un valore o intervalli fissi, ecc. E una bomba potrebbe avere un tag Explosive la cui interfaccia Explosive estende anche l'interfaccia DamageGiving che ha una proprietà aggiuntiva per raggio e possibilmente con quale velocità si diffonde il danno.

Il tuo motore di gioco dovrebbe quindi codificare rispetto alle interfacce, non a tipi di oggetti specifici.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.