Wie entscheide ich, welches GameObject mit der Kollision umgehen soll?

28

Bei jeder Kollision sind zwei GameObjects beteiligt, oder? Ich möchte wissen, wie ich entscheide, welches Objekt mein enthalten sollOnCollision* ?

Angenommen, ich habe ein Player-Objekt und ein Spike-Objekt. Mein erster Gedanke ist, ein Skript auf den Player zu schreiben, das folgenden Code enthält:

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

Die exakt gleiche Funktionalität kann natürlich erreicht werden, indem stattdessen ein Skript auf dem Spike-Objekt vorhanden ist, das Code wie den folgenden enthält:

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

Während beide gültig sind, war es für mich sinnvoller, das Skript auf dem Player zu haben, da in diesem Fall, wenn die Kollision auftritt, eine Aktion auf dem Player ausgeführt wird .

Ich bezweifle jedoch, dass Sie in Zukunft weitere Objekte hinzufügen möchten, die den Player bei einer Kollision töten, z. B. einen Feind, eine Lava, einen Laserstrahl usw. Diese Objekte haben wahrscheinlich unterschiedliche Tags. Dann würde das Skript auf dem Player folgendermaßen aussehen:

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

In dem Fall, in dem sich das Skript auf dem Spike befand, müssten Sie nur dasselbe Skript zu allen anderen Objekten hinzufügen, die den Player beenden und das Skript so benennen können KillPlayerOnContact.

Wenn Sie eine Kollision zwischen dem Spieler und einem Feind haben, möchten Sie wahrscheinlich eine Aktion auf beiden ausführen . Welches Objekt sollte in diesem Fall mit der Kollision umgehen? Oder sollten beide die Kollision bewältigen und unterschiedliche Aktionen ausführen?

Ich habe noch nie ein Spiel mit einer vernünftigen Größe gebaut und frage mich, ob der Code unübersichtlich und schwierig zu warten sein kann, wenn Sie anfangs so etwas falsch verstehen. Oder sind vielleicht alle Wege gültig und es ist nicht wirklich wichtig?


Jeder Einblick wird sehr geschätzt! Vielen Dank für Ihre Zeit :)

Redtama
quelle
Zuerst, warum die String-Literale für Tags? Eine Aufzählung ist viel besser, weil aus einem Tippfehler ein Kompilierungsfehler wird, anstatt einen schwer auffindbaren Fehler (warum bringt mich mein Spike nicht um?).
Ratschenfreak
Weil ich Unity-Tutorials befolgt habe und genau das haben sie getan. Hätten Sie einfach eine öffentliche Enumeration namens Tag, die alle Ihre Tag-Werte enthält? Also wäre es Tag.SPIKEstattdessen?
Redtama
Um das Beste aus beiden Welten herauszuholen, sollten Sie eine Struktur mit einer darin enthaltenen Aufzählung sowie statische ToString- und FromString-Methoden verwenden
zcabjro

Antworten:

48

Ich persönlich bevorzuge es, Systeme so zu entwerfen, dass kein an einer Kollision beteiligtes Objekt damit umgeht, und stattdessen kann ein kollisionsbehandelnder Proxy mit einem Rückruf wie diesem umgehen HandleCollisionBetween(GameObject A, GameObject B). Auf diese Weise muss ich nicht darüber nachdenken, welche Logik wo sein soll.

Wenn dies keine Option ist, wenn Sie beispielsweise nicht die Kontrolle über die Kollisionsreaktionsarchitektur haben, wird es etwas unübersichtlich. Im Allgemeinen lautet die Antwort "es kommt darauf an".

Da beide Objekte Kollisionsrückrufe erhalten, würde ich in beide nur Code einfügen, der für die Rolle des Objekts in der Spielwelt relevant ist , und gleichzeitig versuchen, die Erweiterbarkeit so weit wie möglich aufrechtzuerhalten. Das bedeutet, wenn eine Logik in beiden Objekten gleich sinnvoll ist, ziehen Sie es vor, sie in das Objekt einzufügen, das auf lange Sicht wahrscheinlich zu weniger Codeänderungen führt, wenn neue Funktionen hinzugefügt werden.

Anders ausgedrückt, zwischen zwei Objekttypen, die kollidieren können, hat einer dieser Objekttypen im Allgemeinen die gleichen Auswirkungen auf mehr Objekttypen als der andere. Das Einfügen von Code für diesen Effekt in den Typ, der mehr andere Typen betrifft, bedeutet auf lange Sicht weniger Wartung.

Zum Beispiel ein Spielerobjekt und ein Kugelobjekt. Wahrscheinlich ist es als Teil des Spielers logisch, "Schaden durch Kollision mit Kugeln zu erleiden". "Schaden an dem Ding anrichten, auf das es trifft" ist als Teil der Kugel logisch sinnvoll. Eine Kugel kann jedoch mehr verschiedene Arten von Objekten beschädigen als ein Spieler (in den meisten Spielen). Ein Spieler erleidet keinen Schaden durch Geländeblöcke oder NPCs, aber eine Kugel kann diesen dennoch Schaden zufügen. In diesem Fall würde ich es vorziehen, die Kollisionsabwicklung für das Verursachen von Schaden in die Kugel zu stecken.

Josh
quelle
1
Ich fand die Antworten aller sehr hilfreich, muss sie aber für ihre Klarheit akzeptieren. Dein letzter Absatz fasst es wirklich für mich zusammen :) Sehr hilfreich! Ich danke dir sehr!
Redtama
12

TL; DR Der beste Ort, um den Kollisionsdetektor zu platzieren, ist der Ort, an dem Sie das aktive Element in der Kollision anstelle des passiven Elements in der Kollision betrachten, weil Sie möglicherweise zusätzliches Verhalten benötigen, um in einer solchen aktiven Logik ausgeführt zu werden (und nach guten OOP-Richtlinien wie SOLID liegt es in der Verantwortung des aktiven Elements).

Detailliert :

Wir werden sehen ...

Wenn ich unabhängig von der Spiel-Engine den gleichen Zweifel habe, gehe ich so vor, dass ich feststelle, welches Verhalten aktiv und welches passiv in Bezug auf die Aktion ist, die ich beim Sammeln auslösen möchte.

Bitte beachten Sie: Ich benutze unterschiedliche Verhaltensweisen als Abstraktionen der verschiedenen Teile unserer Logik, um den Schaden und - als weiteres Beispiel - das Inventar zu definieren. Beides sind unterschiedliche Verhaltensweisen, die ich später zu einem Spieler hinzufügen würde, und nicht dazu, meinen benutzerdefinierten Spieler mit unterschiedlichen Verantwortlichkeiten zu überfrachten, die schwerer zu pflegen wären. Mit Anmerkungen wie RequireComponent würde ich sicherstellen, dass mein Player-Verhalten auch das Verhalten aufweist, das ich jetzt definiere.

Dies ist nur meine Meinung: Vermeiden Sie es, Logik abhängig vom Tag auszuführen, sondern verwenden Sie stattdessen unterschiedliche Verhaltensweisen und Werte wie Aufzählungen zur Auswahl .

Stellen Sie sich folgendes Verhalten vor:

  1. Verhalten, um ein Objekt als Stapel auf dem Boden anzugeben. RPG-Spiele verwenden dies für Gegenstände im Boden, die aufgenommen werden können.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
  2. Verhalten, um ein Objekt anzugeben, das Schaden verursachen kann. Dies kann Lava, Säure, eine giftige Substanz oder ein fliegendes Projektil sein.

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

Betrachten Sie diesbezüglich DamageTypeund InventoryObjectals Aufzählungen.

Diese Verhaltensweisen sind nicht aktiv , da sie auf dem Boden liegen oder herumfliegen und nicht von selbst handeln. Die machen nichts. Wenn Sie beispielsweise einen Feuerball in einem leeren Raum fliegen lassen, ist er nicht bereit, Schaden anzurichten (möglicherweise sollten andere Verhaltensweisen in einem Feuerball als aktiv angesehen werden, z. B. wenn der Feuerball auf Reisen seine Kraft verbraucht und dabei seine Schadenskraft verringert, aber sogar Wenn ein potenzielles FuelingOutVerhalten entwickelt werden könnte, ist es ein anderes Verhalten und nicht dasselbe DamageProducer-Verhalten. Wenn Sie ein Objekt im Boden sehen, werden nicht alle Benutzer überprüft und es wird nachgedacht, ob sie mich auswählen können.

Dafür brauchen wir aktives Verhalten. Die Gegenstücke wären:

  1. Ein Verhalten, um Schaden zu nehmen (Es würde ein Verhalten erfordern, um mit Leben und Tod umzugehen, da das Verringern des Lebens und das Empfangen von Schaden normalerweise getrennte Verhaltensweisen sind und weil ich diese Beispiele so einfach wie möglich halten möchte) und vielleicht dem Schaden widerstehen .

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

    Dieser Code ist nicht getestet und sollte als Konzept funktionieren . Was ist der Zweck? Schaden ist etwas, das jemand empfindet und das sich auf das Objekt (oder die lebende Form) bezieht, das beschädigt wird, wie Sie meinen. Dies ist ein Beispiel: In normalen RPG-Spielen fügt Ihnen ein Schwert Schaden zu , während Sie in anderen RPGs (z. B. Summon Night Swordcraft Story I und II ) Schaden erleiden können, wenn Sie einen harten Teil treffen oder die Rüstung blockieren Reparaturen . Da sich die Schadenslogik auf den Empfänger und nicht auf den Absender bezieht, werden nur relevante Daten in den Absender und die Logik in den Empfänger eingefügt.

  2. Gleiches gilt für einen Inventarinhaber (ein weiteres erforderliches Verhalten ist dasjenige, das sich auf das Objekt bezieht, das sich auf dem Boden befindet, und die Fähigkeit, es von dort zu entfernen). Vielleicht ist dies das richtige Verhalten:

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

Wie Sie anhand der Vielzahl der Antworten hier sehen können, sind diese Entscheidungen mit einem angemessenen Maß an persönlichem Stil und einem Urteilsvermögen verbunden, das auf den Bedürfnissen Ihres jeweiligen Spiels basiert - kein absolut bester Ansatz.

Ich empfehle, darüber nachzudenken, wie sich Ihr Ansatz skalieren lässt, wenn Sie Ihr Spiel erweitern , Funktionen hinzufügen und wiederholen. Führt das Platzieren der Kollisionsbehandlungslogik später zu komplexerem Code? Oder unterstützt es modulareres Verhalten?

Sie haben also Stacheln, die den Spieler beschädigen. Frag dich selbst:

  • Gibt es andere Dinge als den Spieler, die Spikes möglicherweise beschädigen möchten?
  • Gibt es andere Dinge als Stacheln, die der Spieler bei einer Kollision beschädigen sollte?
  • Wird jedes Level mit einem Spieler Stacheln haben? Wird jedes Level mit Spikes einen Spieler haben?
  • Könnten Sie Variationen der Spikes haben, die verschiedene Dinge tun müssen? (zB unterschiedliche Schadensbeträge / Schadensarten?)

Offensichtlich wollen wir uns nicht mitreißen lassen und ein überentwickeltes System aufbauen, das eine Million Fälle verarbeiten kann, anstatt nur den einen Fall, den wir benötigen. Aber wenn es so oder so gleich ist (dh diese 4 Zeilen in Klasse A gegen Klasse B stecken), eine aber besser skaliert, ist es eine gute Faustregel, um die Entscheidung zu treffen.

Speziell für dieses Beispiel in Unity, ich würde neigen zu der Putten auf Schaden Logik in den Spitzen , in Form von so etwas wie eine CollisionHazardKomponente, die bei einer Kollision einen ruft Schaden Mitnahmen Methode in einer DamageableKomponente. Hier ist der Grund:

  • Sie müssen nicht für jeden Kollisionsteilnehmer, den Sie unterscheiden möchten, ein Tag festlegen.
  • Wenn Sie mehr Arten kollidierbarer Interaktionen hinzufügen (z. B. Lava, Kugeln, Federn, Power-Ups ...), sammelt Ihre Spielerklasse (oder die Damageable-Komponente) keine riesigen Mengen von if / else / -Schalter-Fällen, um die einzelnen zu handhaben.
  • Wenn die Hälfte Ihres Levels keine Spikes enthält, verschwenden Sie keine Zeit damit, im Player Collision Handler zu prüfen, ob ein Spike beteiligt war. Sie kosten dich nur, wenn sie an der Kollision beteiligt sind.
  • Wenn Sie später entscheiden, dass Spikes auch Feinde und Requisiten töten sollen, müssen Sie keinen Code aus einer spielerspezifischen Klasse duplizieren. Kleben Sie einfach die DamageableKomponente darauf (wenn Sie einige benötigen, die nur gegen Spikes immun sind, können Sie hinzufügen) Schadensarten & lassen Sie dieDamageable Immunität Komponente)
  • Wenn Sie in einem Team arbeiten, blockiert die Person, die das Spike-Verhalten ändern und die Gefahrenklasse / -werte überprüfen muss, nicht alle, die Interaktionen mit dem Spieler aktualisieren müssen. Es ist auch für ein neues Teammitglied (insbesondere für Level-Designer) intuitiv, welches Skript sie betrachten müssen, um das Verhalten der Spikes zu ändern - es ist dasjenige, das mit den Spikes verknüpft ist!
DMGregory
quelle
Das Entity-Component-System ist vorhanden, um solche IFs zu vermeiden. Diese IFs sind immer falsch (nur diese Ifs). Wenn sich der Spike bewegt (oder ein Projektil ist), wird der Kollisions-Handler ausgelöst, obwohl das kollidierende Objekt der Spieler ist oder nicht.
Luis Masuelli
2

Es ist wirklich egal, wer mit der Kollision umgeht, aber es ist wichtig, mit wem zusammenzuarbeiten.

In meinen Spielen versuche ich, GameObjectdasjenige zu wählen, das etwas mit dem anderen Objekt "tut", als dasjenige, das die Kollision kontrolliert. IE Spike beschädigt den Spieler, so dass er die Kollision handhabt. Wenn beide Objekte sich gegenseitig etwas antun, lassen Sie sie jeweils ihre Aktion auf dem anderen Objekt ausführen.

Außerdem würde ich vorschlagen, dass Sie versuchen, Ihre Kollisionen so zu gestalten, dass die Kollisionen von dem Objekt verarbeitet werden, das auf Kollisionen mit weniger Dingen reagiert. Ein Spike muss nur über Kollisionen mit dem Player Bescheid wissen, wodurch der Kollisionscode im Spike-Skript schön und kompakt wird. Das Spielerobjekt kann mit vielen anderen Dingen kollidieren, wodurch ein aufgeblähtes Kollisionsskript entsteht.

Versuchen Sie schließlich, Ihre Kollisionsbehandlungsroutinen abstrakter zu gestalten. Ein Spike muss nicht wissen, dass er mit einem Spieler kollidiert ist, er muss nur wissen, dass er mit etwas kollidiert ist, an dem er Schaden anrichten muss. Nehmen wir an, Sie haben ein Skript:

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

Sie können eine Unterklasse DamageControllerin Ihrem Spieler-GameObject erstellen, um den Schaden zu beheben, und dann in dessen SpikeKollisionscode

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

Ich hatte das gleiche Problem, als ich anfing, also hier ist es: In diesem speziellen Fall benutze den Enemy. Normalerweise möchten Sie, dass die Kollision von dem Objekt behandelt wird, das die Aktion ausführt (in diesem Fall vom Feind, aber wenn Ihr Spieler eine Waffe hatte, sollte die Waffe die Kollision behandeln), denn wenn Sie Attribute optimieren möchten (zum Beispiel den Schaden) des Feindes) Sie möchten es auf jeden Fall im Feindeskript und nicht im Spielerskript ändern (besonders wenn Sie mehrere Spieler haben). Wenn Sie den Schaden einer vom Spieler verwendeten Waffe ändern möchten, möchten Sie dies auf keinen Fall bei jedem Gegner Ihres Spiels tun.

Treeton
quelle
2

Beide Objekte würden die Kollision bewältigen.

Einige Objekte würden durch die Kollision sofort deformiert, zerbrochen oder zerstört. (Kugeln, Pfeile, Wasserballons)

Andere würden einfach abprallen und zur Seite rollen. (Felsen) Diese könnten immer noch Schaden nehmen, wenn das Ding, auf das sie treffen, hart genug wäre. Steine ​​nehmen keinen Schaden von einem Menschen, der sie schlägt, aber sie können von einem Vorschlaghammer stammen.

Einige matschige Gegenstände wie Menschen würden Schaden nehmen.

Andere würden aneinander gebunden sein. (Magnete)

Beide Kollisionen würden in eine Ereignisbehandlungswarteschlange für einen Akteur gestellt, der zu einem bestimmten Zeitpunkt und an einem bestimmten Ort (und mit einer bestimmten Geschwindigkeit) auf ein Objekt einwirkt. Denken Sie daran, dass der Handler sich nur mit dem befassen sollte, was mit dem Objekt passiert. Es ist sogar möglich, dass ein Handler einen anderen Handler in die Warteschlange stellt, um zusätzliche Auswirkungen der Kollision auf der Grundlage der Eigenschaften des Akteurs oder des Objekts, auf das gewirkt wird, zu behandeln. (Bombe explodiert und die resultierende Explosion muss nun auf Kollisionen mit anderen Objekten getestet werden.)

Verwenden Sie beim Skripten Tags mit Eigenschaften, die von Ihrem Code in Schnittstellenimplementierungen übersetzt werden. Verweisen Sie dann auf die Schnittstellen in Ihrem Bearbeitungscode.

Beispielsweise könnte ein Schwert ein Tag für Nahkampf haben, das in eine Schnittstelle mit dem Namen Nahkampf übersetzt wird, die eine DamageGiving-Schnittstelle erweitern könnte, die Eigenschaften für den Schadenstyp und eine Schadensmenge aufweist, die entweder mit einem festen Wert oder mit Reichweiten usw. definiert wurde ein Explosive-Tag, dessen Explosive-Schnittstelle auch die DamageGiving-Schnittstelle erweitert, die eine zusätzliche Eigenschaft für den Radius und möglicherweise für die Ausbreitung des Schadens aufweist.

Ihre Spiel-Engine würde dann gegen die Schnittstellen codieren, nicht gegen bestimmte Objekttypen.

Rick Ryker
quelle