Comment décider quel GameObject doit gérer la collision?

28

Dans toute collision, deux GameObjects sont impliqués, non? Ce que je veux savoir, c'est comment décider quel objet doit contenir mon OnCollision*?

Par exemple, supposons que j'ai un objet Player et un objet Spike. Ma première pensée est de mettre sur le lecteur un script contenant du code comme celui-ci:

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

Bien sûr, la même fonctionnalité peut être obtenue en ayant à la place un script sur l'objet Spike qui contient du code comme celui-ci:

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

Bien que les deux soient valides, il était plus logique pour moi d'avoir le script sur le lecteur car, dans ce cas, lorsque la collision se produit, une action est exécutée sur le lecteur .

Cependant, ce qui me fait douter, c'est qu'à l'avenir, vous souhaiterez peut-être ajouter plus d'objets qui tueront le joueur en cas de collision, tels qu'un ennemi, une lave, un faisceau laser, etc. Ces objets auront probablement des balises différentes. Alors le script sur le Player deviendrait:

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

Alors que, dans le cas où le script était sur le Spike, tout ce que vous auriez à faire est d'ajouter ce même script à tous les autres objets qui peuvent tuer le joueur et nommer le script quelque chose comme KillPlayerOnContact.

De plus, si vous avez une collision entre le joueur et un ennemi, vous voudrez probablement effectuer une action sur les deux . Dans ce cas, quel objet devrait gérer la collision? Ou doit-il à la fois gérer la collision et effectuer différentes actions?

Je n'ai jamais construit de jeu d'une taille raisonnable auparavant et je me demande si le code peut devenir compliqué et difficile à maintenir car il se développe si vous vous trompez ce genre de chose au début. Ou peut-être que tous les moyens sont valides et que cela n'a pas vraiment d'importance?


Toute idée est très appréciée! Merci pour votre temps :)

Redtama
la source
D'abord pourquoi les littéraux de chaîne pour les balises? Une énumération est bien meilleure car une faute de frappe se transformera en une erreur de compilation au lieu d'un bug difficile à retrouver (pourquoi mon pic ne me tue-t-il pas?).
ratchet freak
Parce que j'ai suivi les tutoriels Unity et c'est ce qu'ils ont fait. Alors, auriez-vous juste une énumération publique appelée Tag qui contient toutes vos valeurs de tag? Ce serait donc à la Tag.SPIKEplace?
Redtama
Pour tirer le meilleur parti des deux mondes, envisagez d'utiliser une structure, avec une énumération à l'intérieur, ainsi que des méthodes ToString et FromString statiques
zcabjro

Réponses:

48

Personnellement, je préfère architecturer des systèmes tels qu'aucun objet impliqué dans une collision ne le gère, et à la place, un proxy de gestion des collisions peut le gérer avec un rappel comme HandleCollisionBetween(GameObject A, GameObject B). De cette façon, je n'ai pas à penser à quelle logique devrait être où.

Si ce n'est pas une option, par exemple lorsque vous ne contrôlez pas l'architecture de réponse aux collisions, les choses deviennent un peu floues. Généralement, la réponse est "cela dépend".

Étant donné que les deux objets recevront des rappels de collision, je mettrais du code dans les deux, mais uniquement du code pertinent pour le rôle de cet objet dans le monde du jeu , tout en essayant de maintenir l'extensibilité autant que possible. Cela signifie que si une logique a le même sens dans l'un ou l'autre objet, préférez la mettre dans l'objet, ce qui entraînera probablement moins de modifications de code à long terme à mesure que de nouvelles fonctionnalités sont ajoutées.

Autrement dit, entre deux types d'objets qui peuvent entrer en collision, l'un de ces types d'objet a généralement le même effet sur plus de types d'objet que l'autre. Mettre du code pour cet effet dans le type qui affecte plus d'autres types signifie moins de maintenance à long terme.

Par exemple, un objet joueur et un objet balle. On peut dire que «subir des dégâts en cas de collision avec des balles» est logique en tant que partie intégrante du joueur. "Appliquer des dommages à la chose qu'elle frappe" est logique dans le cadre de la balle. Cependant, une balle est susceptible d’endommager plus de types d’objets différents qu’un joueur (dans la plupart des jeux). Un joueur ne subira pas de dégâts de blocs de terrain ou de PNJ, mais une balle pourrait tout de même leur infliger des dégâts. Je préfère donc mettre la gestion de collision pour infliger des dommages à la balle, dans ce cas.

Josh
la source
1
J'ai trouvé les réponses de tout le monde très utiles, mais je dois accepter les vôtres pour leur clarté. Votre dernier paragraphe le résume vraiment pour moi :) Extrêmement utile! Merci beaucoup!
Redtama
12

TL; DR Le meilleur endroit pour placer le détecteur de collision est l'endroit où vous considérez qu'il est l' élément actif dans la collision, au lieu de l' élément passif dans la collision, car vous aurez peut-être besoin d'un comportement supplémentaire pour être exécuté dans une telle logique active (et, suivant de bonnes directives de POO comme SOLID, est la responsabilité de l' élément actif ).

Détaillé :

Voyons voir ...

Mon approche lorsque j'ai le même doute, quel que soit le moteur de jeu , est de déterminer quel comportement est actif et quel comportement est passif par rapport à l'action que je souhaite déclencher lors de la collecte.

Veuillez noter: j'utilise différents comportements comme abstractions des différentes parties de notre logique pour définir les dommages et - comme autre exemple - l'inventaire. Les deux sont des comportements distincts que j'ajouterais plus tard à un joueur, et n'encombrent pas mon lecteur personnalisé avec des responsabilités distinctes qui seraient plus difficiles à maintenir. En utilisant des annotations comme RequireComponent, je m'assurerais que mon comportement de joueur a également les comportements que je définis maintenant.

C'est juste mon avis: évitez d'exécuter la logique en fonction de la balise, mais utilisez plutôt des comportements et des valeurs différents comme des énumérations parmi lesquelles choisir .

Imaginez ces comportements:

  1. Comportement pour spécifier un objet comme étant une pile au sol. Les jeux RPG l'utilisent pour les objets dans le sol pouvant être ramassés.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
  2. Comportement pour spécifier un objet capable de produire des dommages. Il peut s'agir de lave, d'acide, de toute substance toxique ou d'un projectile volant.

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

Dans cette mesure, considérez DamageTypeet en InventoryObjecttant qu'énumérations.

Ces comportements ne sont pas actifs , de la sorte qu'en étant au sol ou en volant, ils n'agissent pas d'eux-mêmes. Ils ne font rien. Par exemple, si vous avez une boule de feu volant dans un espace vide, elle n'est pas prête à faire des dégâts (peut-être que d'autres comportements devraient être considérés comme actifs dans une boule de feu, comme la boule de feu consommant sa puissance en voyageant et diminuant sa puissance de dégâts dans le processus, mais même quand un FuelingOutcomportement potentiel pourrait être développé, c'est toujours un autre comportement, et pas le même comportement DamageProducer), et si vous voyez un objet dans le sol, il ne vérifie pas l'ensemble des utilisateurs et pense qu'ils peuvent me choisir?

Nous avons besoin de comportements actifs pour cela. Les homologues seraient:

  1. Un comportement pour subir des dommages (il faudrait un comportement pour gérer la vie et la mort, car réduire la vie et recevoir des dommages sont généralement des comportements distincts, et parce que je veux garder ces exemples aussi simples que possible) et peut-être résister aux dommages.

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

    Ce code n'est pas testé et devrait fonctionner comme un concept . Quel est le but? Les dommages sont quelque chose que quelqu'un ressent et sont relatifs à l'objet (ou à la forme vivante) endommagé, comme vous le pensez. Ceci est un exemple: dans les jeux de RPG réguliers, une épée vous endommage , tandis que dans d'autres RPG qui l'implémentent (par exemple Summon Night Swordcraft Story I et II ), une épée peut également recevoir des dégâts en frappant une partie dure ou en bloquant l'armure, et pourrait avoir besoin réparations . Ainsi, puisque la logique de dommage est relative au récepteur et non à l'expéditeur, nous mettons uniquement les données pertinentes dans l'expéditeur et la logique dans le récepteur.

  2. Il en va de même pour un détenteur d'inventaire (un autre comportement requis serait celui lié à la présence de l'objet au sol et à la possibilité de l'enlever). C'est peut-être le comportement approprié:

    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
la source
7

Comme vous pouvez le voir dans la variété des réponses ici, il y a un bon degré de style personnel impliqué dans ces décisions et un jugement basé sur les besoins de votre jeu particulier - il n'y a pas de meilleure approche absolue.

Ma recommandation est de réfléchir à la façon dont votre approche évoluera à mesure que vous développez votre jeu , en ajoutant et en itérant sur les fonctionnalités. Le fait de mettre la logique de gestion des collisions au même endroit aboutira-t-il à un code plus complexe plus tard? Ou prend-il en charge un comportement plus modulaire?

Donc, vous avez des pointes qui endommagent le joueur. Demande toi:

  • Y a-t-il d'autres choses que le joueur que les pointes pourraient vouloir endommager?
  • Y a-t-il d'autres choses que des pointes dont le joueur devrait subir des dégâts lors d'une collision?
  • Est-ce que chaque niveau avec un joueur aura des pointes? Chaque niveau avec des pointes aura-t-il un joueur?
  • Pourriez-vous avoir des variations sur les pointes qui doivent faire des choses différentes? (par exemple, différents montants / types de dommages?)

De toute évidence, nous ne voulons pas nous laisser emporter par cela et construire un système sur-conçu qui peut gérer un million de cas au lieu du seul cas dont nous avons besoin. Mais si c'est un travail égal dans les deux sens (c.-à-d. Mettre ces 4 lignes dans la classe A contre la classe B) mais que l'une évolue mieux, c'est une bonne règle de base pour prendre la décision.

Pour cet exemple, dans Unity en particulier, je pencherais pour mettre la logique de dégâts dans les pointes , sous la forme de quelque chose comme un CollisionHazardcomposant, qui lors d'une collision appelle une prise de dégâts méthode de dans un Damageablecomposant. Voici pourquoi:

  • Vous n'avez pas besoin de mettre de côté une balise pour chaque participant de collision différent que vous souhaitez distinguer.
  • Au fur et à mesure que vous ajoutez de nouvelles interactions interactives (par exemple, lave, balles, ressorts, bonus ...), votre classe de joueur (ou composant endommageable) n'accumule pas un ensemble géant de cas if / else / switch pour gérer chacun d'eux.
  • Si la moitié de vos niveaux ne contiennent aucun pic, vous ne perdez pas de temps dans le gestionnaire de collision des joueurs à vérifier si un pic est impliqué. Ils ne vous coûtent que lorsqu'ils sont impliqués dans la collision.
  • Si vous décidez plus tard que les pointes devraient également tuer les ennemis et les accessoires, vous n'avez pas besoin de dupliquer le code d'une classe spécifique au joueur, il suffit de coller le Damageablecomposant dessus (si vous en avez besoin pour être immunisé contre les pointes uniquement, vous pouvez ajouter types de dommages et laissez le Damageablecomposant gérer les immunités)
  • Si vous travaillez en équipe, la personne qui doit changer le comportement des pics et vérifier la classe / les actifs de danger ne bloque pas tous ceux qui doivent mettre à jour les interactions avec le joueur. Il est également intuitif pour un nouveau membre de l'équipe (en particulier les concepteurs de niveaux) quel script ils doivent regarder pour changer les comportements des pointes - c'est celui attaché aux pointes!
DMGregory
la source
Le système Entity-Component existe pour éviter des FI comme ça. Ces FI sont toujours fausses (juste ces ifs). De plus, si la pointe se déplace (ou est un projectile), le gestionnaire de collision sera déclenché malgré que l'objet entrant en collision soit ou non le joueur.
Luis Masuelli
2

Peu importe qui gère la collision, mais soyez cohérent avec le travail dont il s'agit.

Dans mes jeux, j'essaie de choisir celui GameObjectqui "fait" quelque chose à l'autre objet comme celui qui contrôle la collision. IE Spike endommage le joueur afin qu'il gère la collision. Lorsque les deux objets se "font" quelque chose l'un à l'autre, demandez-leur de gérer chacun leur action sur l'autre objet.

Aussi, je suggérerais d'essayer de concevoir vos collisions de sorte que les collisions soient gérées par l'objet qui réagit aux collisions avec moins de choses. Un pic n'aura besoin que de connaître les collisions avec le joueur, ce qui rend le code de collision dans le script Spike agréable et compact. L'objet joueur peut entrer en collision avec beaucoup d'autres choses qui feront un script de collision gonflé.

Enfin, essayez de rendre vos gestionnaires de collisions plus abstraits. Un Spike n'a pas besoin de savoir qu'il est entré en collision avec un joueur, il a juste besoin de savoir qu'il est entré en collision avec quelque chose qu'il doit infliger des dégâts. Disons que vous avez un script:

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

Vous pouvez sous DamageController-classer dans votre lecteur GameObject pour gérer les dégâts, puis dans Spikele code de collision de

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

J'ai eu le même problème quand j'ai commencé, alors voilà: dans ce cas particulier, utilisez l'ennemi. Vous voulez généralement que la collision soit gérée par l'objet qui exécute l'action (dans ce cas - l'ennemi, mais si votre joueur avait une arme, alors l'arme devrait gérer la collision), parce que si vous voulez modifier les attributs (par exemple les dégâts de l'ennemi) vous voulez certainement le changer dans l'énémyscript et non dans le playerScript (surtout si vous avez plusieurs joueurs). De même, si vous souhaitez modifier les dégâts d'une arme que le joueur utilise, vous ne voulez certainement pas la modifier pour chaque ennemi de votre jeu.

Treeton
la source
2

Les deux objets géreraient la collision.

Certains objets seraient immédiatement déformés, cassés ou détruits par la collision. (balles, flèches, ballons d'eau)

D'autres rebondissaient et roulaient sur le côté. (rochers) Ceux-ci pourraient encore subir des dégâts si la chose qu'ils frappaient était suffisamment dure. Les rochers ne subissent pas de dégâts d'un humain qui les frappe, mais ils pourraient le faire avec un marteau.

Certains objets visqueux comme les humains subiraient des dégâts.

D'autres seraient liés les uns aux autres. (aimants)

Les deux collisions seraient placées dans une file d'attente de gestion d'événements pour un acteur agissant sur un objet à un moment et un lieu (et une vitesse) particuliers. N'oubliez pas que le gestionnaire ne doit gérer que ce qui arrive à l'objet. Il est même possible pour un gestionnaire de placer un autre gestionnaire dans la file d'attente pour gérer les effets supplémentaires de la collision en fonction des propriétés de l'acteur ou de l'objet sur lequel on agit. (La bombe a explosé et l'explosion qui en résulte doit maintenant tester les collisions avec d'autres objets.)

Lors de l'écriture de scripts, utilisez des balises avec des propriétés qui seraient traduites en implémentations d'interface par votre code. Référencez ensuite les interfaces dans votre code de gestion.

Par exemple, une épée peut avoir une balise pour Melee qui se traduit par une interface nommée Melee qui peut étendre une interface DamageGiving qui a des propriétés pour le type de dégâts et un montant de dégâts défini en utilisant soit une valeur fixe soit des plages, etc. Et une bombe peut avoir une étiquette Explosive dont l'interface Explosive étend également l'interface DamageGiving qui a une propriété supplémentaire pour le rayon et éventuellement la vitesse à laquelle les dommages se diffusent.

Votre moteur de jeu coderait alors par rapport aux interfaces, et non à des types d'objets spécifiques.

Rick Ryker
la source