¿Cómo decidir qué GameObject debería manejar la colisión?

28

En cualquier colisión, hay dos GameObjects involucrados, ¿verdad? Lo que quiero saber es, ¿cómo decido qué objeto debe contener mi OnCollision*?

Como ejemplo, supongamos que tengo un objeto Player y un objeto Spike. Mi primer pensamiento es poner un script en el reproductor que contenga un código como este:

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

Por supuesto, se puede lograr exactamente la misma funcionalidad al tener un script en el objeto Spike que contiene un código como este:

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

Si bien ambos son válidos, tenía más sentido para mí tener el guión en el Jugador porque, en este caso, cuando ocurre la colisión, se está realizando una acción en el Jugador .

Sin embargo, lo que me hace dudar es que en el futuro es posible que desee agregar más objetos que matarán al jugador en caso de colisión, como un enemigo, lava, rayo láser, etc. Es probable que estos objetos tengan etiquetas diferentes. Entonces el guión en el reproductor se convertiría en:

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

Mientras que, en el caso de que el script estuviera en Spike, todo lo que tendría que hacer es agregar ese mismo script a todos los demás objetos que pueden matar al jugador y nombrar el script de forma similar KillPlayerOnContact.

Además, si tiene una colisión entre el Jugador y un Enemigo, es probable que desee realizar una acción en ambos . Entonces, en ese caso, ¿qué objeto debería manejar la colisión? ¿O ambos deben manejar la colisión y realizar diferentes acciones?

Nunca antes había creado un juego de un tamaño razonable y me pregunto si el código puede volverse desordenado y difícil de mantener a medida que crece si al principio te equivocas. ¿O tal vez todas las formas son válidas y realmente no importa?


¡Cualquier idea es muy apreciada! Gracias por tu tiempo :)

Redtama
fuente
Primero, ¿por qué los literales de cadena para las etiquetas? Una enumeración es mucho mejor porque un error tipográfico se convertirá en un error de compilación en lugar de un error difícil de rastrear (¿por qué mi espiga no me está matando?).
monstruo de trinquete
Porque he estado siguiendo los tutoriales de Unity y eso es lo que hicieron. Entonces, ¿tendría una enumeración pública llamada Etiqueta que contiene todos sus valores de etiqueta? ¿Entonces sería en su Tag.SPIKElugar?
Redtama
Para obtener lo mejor de ambos mundos, considere usar una estructura, con una enumeración dentro, así como métodos estáticos ToString y FromString
zcabjro

Respuestas:

48

Personalmente, prefiero diseñar sistemas de modo que ninguno de los objetos involucrados en una colisión lo maneje, y en su lugar algún proxy de manejo de colisión lo maneje con una devolución de llamada como HandleCollisionBetween(GameObject A, GameObject B). De esa manera no tengo que pensar en qué lógica debería estar dónde.

Si esa no es una opción, como cuando no tienes el control de la arquitectura de respuesta de colisión, las cosas se ponen un poco confusas. En general, la respuesta es "depende".

Dado que ambos objetos recibirán devoluciones de llamada por colisión, pondría código en ambos, pero solo código que sea relevante para el papel de ese objeto en el mundo del juego , al mismo tiempo que trato de mantener la extensibilidad tanto como sea posible. Esto significa que si alguna lógica tiene el mismo sentido en cualquiera de los objetos, prefiera ponerla en el objeto que probablemente conducirá a menos cambios de código a largo plazo a medida que se agregue nueva funcionalidad.

Dicho de otra manera, entre dos tipos de objetos que pueden colisionar, uno de esos tipos de objetos generalmente tiene el mismo efecto en más tipos de objetos que el otro. Poner código para ese efecto en el tipo que afecta a otros tipos significa menos mantenimiento a largo plazo.

Por ejemplo, un objeto jugador y un objeto bala. Podría decirse que "recibir daño por colisión con balas" tiene sentido lógico como parte del jugador. "Aplicar daño a lo que golpea" tiene sentido lógico como parte de la bala. Sin embargo, es probable que una bala aplique daño a más tipos diferentes de objetos que un jugador (en la mayoría de los juegos). Un jugador no sufrirá daños por bloques de terreno o NPC, pero una bala aún podría aplicar daño a esos. Por lo tanto, preferiría poner el manejo de colisión para impartir daños en la bala, en ese caso.

Josh
fuente
1
Encontré las respuestas de todos realmente útiles, pero tengo que aceptar las suyas por su claridad. Tu último párrafo realmente lo resume :) ¡Muy útil! Muchas gracias!
Redtama
12

TL; DR El mejor lugar para colocar el detector de colisión es el lugar donde considera que es el elemento activo en la colisión, en lugar del elemento pasivo en la colisión, porque quizás necesitará un comportamiento adicional para ejecutarse en dicha lógica activa (y, siguiendo buenas pautas de OOP como SOLID, es responsabilidad del elemento activo ).

Detallado :

Veamos ...

Mi enfoque cuando tengo la misma duda, independientemente del motor del juego , es determinar qué comportamiento está activo y qué comportamiento es pasivo con respecto a la acción que quiero activar en la colección.

Tenga en cuenta: uso diferentes comportamientos como abstracciones de las diferentes partes de nuestra lógica para definir el daño y, como otro ejemplo, el inventario. Ambas son conductas separadas que agregaría más tarde a un Jugador, y no saturan mi Jugador personalizado con responsabilidades separadas que serían más difíciles de mantener. Usando anotaciones como RequireComponent, me aseguraría de que mi comportamiento de jugador también tuviera los comportamientos que estoy definiendo ahora.

Esta es solo mi opinión: evite ejecutar la lógica dependiendo de la etiqueta, pero en su lugar use diferentes comportamientos y valores como enumeraciones para elegir .

Imagina estos comportamientos:

  1. Comportamiento para especificar un objeto como una pila en el suelo. Los juegos de rol utilizan esto para los elementos en el suelo que se pueden recoger.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
  2. Comportamiento para especificar un objeto capaz de producir daño. Esto podría ser lava, ácido, cualquier sustancia tóxica o un proyectil volador.

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

En este sentido, considere DamageTypey InventoryObjectcomo enumeraciones.

Estos comportamientos no son activos , ya que al estar en el suelo o volar no actúan por sí mismos. Ellos no hacen nada. Por ejemplo, si tienes una bola de fuego volando en un espacio vacío, no está lista para hacer daño (quizás otros comportamientos deberían considerarse activos en una bola de fuego, como la bola de fuego que consume su poder mientras viaja y disminuye su poder de daño en el proceso, pero incluso cuando se FuelingOutpodría desarrollar un comportamiento potencial , todavía es otro comportamiento, y no el mismo comportamiento de DamageProducer), y si ve un objeto en el suelo, no está revisando a todos los usuarios y pensando ¿ pueden elegirme?

Necesitamos comportamientos activos para eso. Las contrapartes serían:

  1. Un comportamiento para recibir daño (se requeriría un comportamiento para manejar la vida y la muerte, ya que reducir la vida y recibir daño generalmente son comportamientos separados, y porque quiero mantener estos ejemplos lo más simple posible) y tal vez resistir el daño.

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

    Este código no ha sido probado y debería funcionar como un concepto . ¿Cuál es el propósito? El daño es algo que alguien siente , y está relacionado con el objeto (o la forma de vida) que está siendo dañado, como usted lo considera. Este es un ejemplo: en los juegos de rol regulares, una espada te daña , mientras que en otros juegos de rol que lo implementan (por ejemplo, Summon Night Swordcraft Story I y II ), una espada también puede recibir daño al golpear una parte dura o bloquear una armadura, y podría necesitar reparaciones . Entonces, dado que la lógica de daño es relativa al receptor y no al remitente, solo colocamos datos relevantes en el remitente y la lógica en el receptor.

  2. Lo mismo se aplica a un titular de inventario (otro comportamiento requerido sería uno relacionado con el objeto que está en el suelo y la capacidad de eliminarlo de allí). Quizás este es el comportamiento apropiado:

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

Como puede ver en la variedad de respuestas aquí, hay un cierto grado de estilo personal involucrado en estas decisiones y un juicio basado en las necesidades de su juego en particular: no hay un mejor enfoque absoluto.

Mi recomendación es pensarlo en términos de cómo su enfoque se ampliará a medida que crece su juego , agregando e iterando características. ¿Poner la lógica de manejo de colisiones en un lugar conducirá a un código más complejo más adelante? ¿O admite un comportamiento más modular?

Entonces, tienes picos que dañan al jugador. Pregúntese:

  • ¿Hay otras cosas además del jugador que los picos podrían querer dañar?
  • ¿Hay otras cosas además de los picos de los que el jugador debería recibir daño en caso de colisión?
  • ¿Cada nivel con un jugador tendrá picos? ¿Cada nivel con picos tendrá un jugador?
  • ¿Podría tener variaciones en los picos que necesitan hacer cosas diferentes? (por ejemplo, diferentes cantidades de daño / tipos de daño?)

Obviamente, no queremos dejarnos llevar por esto y construir un sistema sobre-diseñado que pueda manejar un millón de casos en lugar del único caso que necesitamos. Pero si es igual de trabajo en ambos sentidos (es decir, coloque estas 4 líneas en la clase A frente a la clase B) pero una escala mejor, es una buena regla general para tomar la decisión.

Para este ejemplo, en Unity específicamente, me inclinaría por poner la lógica de daño en los picos , en forma de algo así como un CollisionHazardcomponente, que en caso de colisión llama a un método de daño en un Damageablecomponente. Este es el por qué:

  • No necesita reservar una etiqueta para cada participante de colisión diferente que desea distinguir.
  • A medida que agrega más variedades de interacciones colisionables (por ejemplo, lava, balas, resortes, potenciadores ...) su clase de jugador (o componente Daños) no acumula un conjunto gigante de cajas if / else / switch para manejar cada una.
  • Si la mitad de tus niveles terminan sin picos en ellos, no estás perdiendo el tiempo en el controlador de colisión del jugador comprobando si hubo un pico involucrado. Solo te cuestan cuando están involucrados en la colisión.
  • Si luego decides que los picos también deberían matar a los enemigos y los accesorios, no necesitas duplicar el código de una clase específica del jugador, solo pega el Damageablecomponente en ellos (si necesitas que algunos sean inmunes solo a los picos, puedes agregar tipos de daño y deje que el Damageablecomponente maneje las inmunidades)
  • Si está trabajando en un equipo, la persona que necesita cambiar el comportamiento de la espiga y verifica la clase / activos de peligro no está bloqueando a todos los que necesitan actualizar las interacciones con el jugador. También es intuitivo para un nuevo miembro del equipo (especialmente los diseñadores de niveles) qué script necesitan observar para cambiar los comportamientos de los picos: ¡es el que está asociado a los picos!
DMGregory
fuente
El sistema de entidad-componente existe para evitar IF como ese. Esos IF siempre están equivocados (solo esos ifs). Además, si la espiga se mueve (o es un proyectil), el controlador de colisión se activará a pesar de que el objeto colisionador sea o no el jugador.
Luis Masuelli
2

Realmente no importa quién maneje la colisión, pero sea coherente con el trabajo de quién es.

En mis juegos, trato de elegir lo GameObjectque está "haciendo" algo al otro objeto como el que controla la colisión. IE Spike daña al jugador por lo que maneja la colisión. Cuando ambos objetos "hacen" algo el uno al otro, haga que cada uno maneje su acción sobre el otro objeto.

Además, sugeriría intentar diseñar sus colisiones para que las colisiones sean manejadas por el objeto que reacciona a las colisiones con menos cosas. Un spike solo necesitará saber sobre colisiones con el jugador, lo que hace que el código de colisión en el script Spike sea agradable y compacto. El objeto jugador puede colisionar con muchas más cosas que harán un guión de colisión hinchado.

Por último, trate de hacer que sus controladores de colisión sean más abstractos. Un Spike no necesita saber que colisionó con un jugador, solo necesita saber que colisionó con algo a lo que necesita hacerle daño. Digamos que tienes un script:

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

Puedes subclasificar DamageControlleren tu GameObject de jugador para manejar el daño, luego en Spikeel código de colisión

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

Tuve el mismo problema cuando comencé, así que aquí va: en este caso particular, use el enemigo. Por lo general, desea que la colisión sea manejada por el objeto que realiza la acción (en este caso, el enemigo, pero si su jugador tenía un arma, entonces el arma debería manejar la colisión), porque si desea modificar los atributos (por ejemplo, el daño del enemigo) definitivamente quieres cambiarlo en el script del enemigo y no en el script del jugador (especialmente si tienes varios jugadores). Del mismo modo, si quieres cambiar el daño de cualquier arma que el jugador use, definitivamente no quieres cambiarlo en cada enemigo de tu juego.

Treeton
fuente
2

Ambos objetos manejarían la colisión.

Algunos objetos serían inmediatamente deformados, rotos o destruidos por la colisión. (balas, flechas, globos de agua)

Otros simplemente rebotarían y rodarían a un lado. (rocas) Todavía podrían sufrir daños si lo que golpearon fue lo suficientemente duro. Las rocas no reciben daño de un humano que las golpea, pero podrían recibir un martillo.

Algunos objetos blandos como los humanos sufrirían daños.

Otros estarían unidos el uno al otro. (imanes)

Ambas colisiones se colocarían en una cola de manejo de eventos para un actor que actúa sobre un objeto en un momento y lugar particular (y velocidad). Solo recuerda que el controlador solo debe tratar con lo que le sucede al objeto. Incluso es posible que un controlador coloque otro controlador en la cola para manejar efectos adicionales de la colisión en función de las propiedades del actor o del objeto sobre el que se actúa. (La bomba explotó y la explosión resultante ahora tiene que probar colisiones con otros objetos).

Cuando realice scripts, use etiquetas con propiedades que su código traduciría en implementaciones de interfaz. Luego, haga referencia a las interfaces en su código de manejo.

Por ejemplo, una espada puede tener una etiqueta para Melee que se traduce en una interfaz llamada Melee que puede extender una interfaz DamageGiving que tiene propiedades para el tipo de daño y una cantidad de daño definida usando un valor fijo o rangos, etc. Y una bomba podría tener una etiqueta explosiva cuya interfaz explosiva también extiende la interfaz DamageGiving que tiene una propiedad adicional para el radio y posiblemente con qué rapidez se difunde el daño.

Su motor de juego codificaría contra las interfaces, no los tipos de objetos específicos.

Rick Ryker
fuente