Como decidir qual GameObject deve lidar com a colisão?

28

Em qualquer colisão, há dois GameObjects envolvidos, certo? O que eu quero saber é: Como decido qual objeto deve conter o meu OnCollision*?

Como exemplo, vamos supor que eu tenho um objeto Player e um objeto Spike. Meu primeiro pensamento é colocar um script no player que contenha algum código como este:

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

Obviamente, a mesma funcionalidade exata pode ser alcançada com um script no objeto Spike que contém código como este:

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

Embora ambos sejam válidos, fazia mais sentido para mim ter o script no Player porque, nesse caso, quando a colisão acontece, uma ação está sendo executada no Player .

No entanto, o que me faz duvidar disso é que, no futuro, você poderá adicionar mais objetos que matarão o Player em colisão, como Inimigo, Lava, Laser Beam, etc. Esses objetos provavelmente terão tags diferentes. Então o script no Player se tornaria:

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

Enquanto que, no caso em que o script estava no Spike, tudo o que você precisava fazer era adicionar esse mesmo script a todos os outros objetos que podem matar o Player e nomear o script como algo semelhante KillPlayerOnContact.

Além disso, se você tiver uma colisão entre o jogador e um inimigo, provavelmente desejará executar uma ação em ambos . Então, nesse caso, qual objeto deve lidar com a colisão? Ou devem lidar com a colisão e executar ações diferentes?

Eu nunca construí um jogo de tamanho razoável antes e estou me perguntando se o código pode se tornar confuso e difícil de manter à medida que cresce, se você entender esse tipo de coisa errada no começo. Ou talvez todas as formas sejam válidas e isso realmente não importa?


Qualquer ideia é bem apreciada! Obrigado pelo seu tempo :)

Redtama
fonte
Primeiro, por que a string literal para tags? Um enum é muito melhor porque um erro de digitação se transformará em um erro de compilação em vez de um bug difícil de rastrear (por que meu pico não está me matando?).
catraca aberração
Porque tenho acompanhado os tutoriais do Unity e foi isso que eles fizeram. Então, você teria uma enumeração pública chamada Tag que contém todos os seus valores de tag? Então seria assim Tag.SPIKE?
Redtama
Para obter o melhor dos dois mundos, considere usar um struct, com um interior enum, assim como ToString estática e fromString métodos
zcabjro

Respostas:

48

Pessoalmente, prefiro arquitetar sistemas de modo que nenhum objeto envolvido em uma colisão lide com isso, e, em vez disso, algum proxy de manipulação de colisão consegue lidar com ele com um retorno de chamada HandleCollisionBetween(GameObject A, GameObject B). Dessa forma, não preciso pensar em qual lógica deveria estar onde.

Se isso não é uma opção, como quando você não está no controle da arquitetura de resposta a colisões, as coisas ficam um pouco confusas. Geralmente a resposta é "depende".

Como os dois objetos receberão retornos de colisão, eu colocaria o código em ambos, mas apenas o código relevante para o papel desse objeto no mundo do jogo , enquanto também tentava manter a extensibilidade o máximo possível. Isso significa que, se alguma lógica faz sentido em ambos os objetos, prefira colocá-la no objeto que provavelmente levará a menos alterações de código a longo prazo à medida que novas funcionalidades forem adicionadas.

Dito de outra forma, entre dois tipos de objetos que podem colidir, um desses tipos de objetos geralmente tem o mesmo efeito em mais tipos de objetos que o outro. Colocar código para esse efeito no tipo que afeta mais outros tipos significa menos manutenção a longo prazo.

Por exemplo, um objeto de jogador e um objeto de marcador. Indiscutivelmente, "sofrer dano em colisão com balas" faz sentido lógico como parte do jogador. "Aplicar dano à coisa que ela atinge" faz sentido lógico como parte da bala. No entanto, é provável que uma bala cause danos a mais tipos diferentes de objetos do que um jogador (na maioria dos jogos). Um jogador não sofrerá dano de blocos de terreno ou NPCs, mas uma bala ainda poderá causar dano a eles. Então, eu prefiro colocar o tratamento de colisão para causar danos à bala, nesse caso.

Josh
fonte
1
Achei as respostas de todos realmente úteis, mas tenho que aceitar as suas por sua clareza. Seu último parágrafo realmente resume para mim :) Extremamente útil! Muito obrigado!
Redtama 9/08/16
12

TL; DR O melhor local para colocar o detector de colisão é o local em que você considera que é o elemento ativo na colisão, em vez do elemento passivo na colisão, porque talvez você precise de um comportamento adicional para ser executado nessa lógica ativa (e, seguindo boas diretrizes de POO, como o SOLID, é de responsabilidade do elemento ativo ).

Detalhado :

Vamos ver ...

Minha abordagem quando tenho a mesma dúvida, independentemente do mecanismo do jogo , é determinar qual comportamento está ativo e qual é passivo em relação à ação que quero desencadear na coleta.

Observe: eu uso comportamentos diferentes como abstrações das diferentes partes de nossa lógica para definir o dano e, como outro exemplo, o inventário. Ambos são comportamentos separados que eu adicionaria mais tarde a um Player, e não sobrecarregar meu Player personalizado com responsabilidades separadas que seriam mais difíceis de manter. Usando anotações como RequireComponent, eu garantiria que o comportamento do Player tivesse, também, os comportamentos que estou definindo agora.

Esta é apenas a minha opinião: evite executar a lógica dependendo da tag, mas use comportamentos e valores diferentes, como enumerações, para escolher .

Imagine este comportamento:

  1. Comportamento para especificar um objeto como sendo uma pilha no chão. Jogos de RPG usam isso para itens no chão que podem ser apanhados.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
    
  2. Comportamento para especificar um objeto capaz de produzir danos. Pode ser lava, ácido, qualquer substância tóxica ou um projétil voador.

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

Nesta medida, considere DamageTypee InventoryObjectcomo enumerações.

Esses comportamentos não são ativos , da maneira que, estando no chão ou voando ao redor, eles não agem sozinhos. Eles não fazem nada. Por exemplo, se você tem uma bola de fogo voando em um espaço vazio, ela não está pronta para causar danos (talvez outros comportamentos devam ser considerados ativos em uma bola de fogo, como a bola de fogo consumindo seu poder enquanto viaja e diminuindo seu poder de dano no processo, mas até quando um FuelingOutcomportamento em potencial pode ser desenvolvido, ainda é outro comportamento, e não o mesmo comportamento do DamageProducer), e se você vê um objeto no chão, ele não está verificando todos os usuários e pensando que eles podem me pegar?

Precisamos de comportamentos ativos para isso. As contrapartes seriam:

  1. Um comportamento para sofrer danos (exigiria um comportamento para lidar com a vida e a morte, já que diminuir a vida e receber danos geralmente são comportamentos separados, e porque eu quero manter esses exemplos o mais simples possível) e talvez resistir a danos.

    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 não foi testado e deve funcionar como um conceito . Qual é o propósito? Dano é algo que alguém sente e é relativo ao objeto (ou forma viva) sendo danificado, como você considera. Este é um exemplo: em jogos regulares de RPG, uma espada danifica você , enquanto em outros RPGs que a implementam (por exemplo, Evocar Night Swordcraft Story I e II ), uma espada também pode receber dano ao acertar uma parte difícil ou bloquear uma armadura e pode precisar reparos . Portanto, como a lógica de dano é relativa ao receptor e não ao remetente, apenas colocamos dados relevantes no remetente e a lógica no receptor.

  2. O mesmo se aplica a um detentor de inventário (outro comportamento necessário seria o relacionado ao objeto estar no chão e a capacidade de removê-lo de lá). Talvez este seja o comportamento apropriado:

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

Como você pode ver pela variedade de respostas aqui, há um bom grau de estilo pessoal envolvido nessas decisões e julgamento baseado nas necessidades do seu jogo em particular - nenhuma abordagem absolutamente melhor.

Minha recomendação é pensar nisso em termos de como sua abordagem será escalada à medida que você progride , adicionando e iterando recursos. A colocação da lógica de manipulação de colisões em um só lugar levará a códigos mais complexos posteriormente? Ou ele suporta um comportamento mais modular?

Então, você tem picos que danificam o jogador. Pergunte a si mesmo:

  • Existem outras coisas além do jogador que os picos podem querer danificar?
  • Existem outras coisas além de picos que o jogador deve sofrer danos na colisão?
  • Todos os níveis de um jogador terão picos? Todos os níveis com espinhos terão um jogador?
  • Você pode ter variações nos picos que precisam fazer coisas diferentes? (por exemplo, diferentes quantidades de danos / tipos de danos?)

Obviamente, não queremos nos deixar levar por isso e criar um sistema com excesso de engenharia que possa lidar com um milhão de casos, em vez de apenas o que precisamos. Mas se o trabalho é igual de qualquer maneira (por exemplo, coloque essas quatro linhas na classe A versus classe B), mas se a escala for melhor, é uma boa regra geral para a tomada de decisão.

Neste exemplo, especificamente no Unity, eu me inclinaria a colocar a lógica de causar danos nos picos , na forma de algo como um CollisionHazardcomponente, que após a colisão chama um método de causar danos em um Damageablecomponente. Aqui está o porquê:

  • Você não precisa reservar uma tag para cada participante de colisão diferente que deseja distinguir.
  • À medida que você adiciona mais variedades de interações colidíveis (por exemplo, lava, balas, fontes, powerups ...), sua classe de jogadores (ou componente Danificável) não acumula um conjunto gigante de casos if / else / switch para lidar com cada um.
  • Se metade dos seus níveis não tiver picos, você não estará perdendo tempo no controle de colisão de jogadores, verificando se houve um aumento. Eles custam apenas quando estão envolvidos na colisão.
  • Se você decidir mais tarde que os espinhos também matam inimigos e acessórios, não é necessário duplicar o código de uma classe específica de jogador, basta colar o Damageablecomponente neles (se você precisar que alguns sejam imunes apenas a espinhos, você pode adicionar tipos de danos e deixe o Damageablecomponente lidar com imunidades)
  • Se você estiver trabalhando em equipe, a pessoa que precisa alterar o comportamento do pico e verificar a classe / ativos de perigo não está bloqueando todos que precisam atualizar as interações com o jogador. Também é intuitivo para um novo membro da equipe (especialmente designers de nível) qual script eles precisam analisar para alterar o comportamento do pico - é o que está associado aos picos!
DMGregory
fonte
O sistema Entity-Component existe para evitar IFs assim. Esses IFs estão sempre errados (apenas esses ifs). Além disso, se o pico estiver em movimento (ou for um projétil), o manipulador de colisão será acionado, apesar de o objeto em colisão ser ou não o jogador.
Luis Masuelli 8/08/16
2

Realmente não importa quem lida com a colisão, mas seja consistente com o trabalho de quem é.

Nos meus jogos, tento escolher o GameObjectque está "fazendo" algo para o outro objeto como aquele que controla a colisão. O IE Spike danifica o jogador, então ele lida com a colisão. Quando os dois objetos "fazem" algo um com o outro, peça que cada um lide com sua ação no outro objeto.

Além disso, eu sugeriria tentar projetar suas colisões para que elas sejam tratadas pelo objeto que reage às colisões com menos coisas. Um pico apenas precisará saber sobre colisões com o jogador, tornando o código de colisão no script Spike agradável e compacto. O objeto do jogador pode colidir com muito mais coisas que criarão um script de colisão inchado.

Por fim, tente tornar seus manipuladores de colisão mais abstratos. Um Spike não precisa saber que colidiu com um jogador, ele apenas precisa saber que colidiu com algo que precisa causar dano. Digamos que você tenha um script:

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

Você pode subclassificar DamageControllerno GameObject do seu jogador para lidar com o dano e, em seguida, no Spikecódigo de colisão

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

Eu tive o mesmo problema quando comecei, então aqui vai: Neste caso específico, use o Inimigo. Você geralmente quer que a Colisão seja manipulada pelo Objeto que executa a ação (neste caso - o inimigo, mas se o seu jogador tiver uma arma, a arma deve lidar com a colisão), porque se você quiser ajustar os atributos (por exemplo, o dano do inimigo), você definitivamente quer mudá-lo no manuscrito e não no manuscrito (especialmente se você tiver vários jogadores). Da mesma forma, se você quiser alterar o dano de qualquer arma que o Jogador use, você definitivamente não deseja alterá-lo em todos os inimigos do seu jogo.

Treeton
fonte
2

Ambos os objetos lidariam com a colisão.

Alguns objetos seriam imediatamente deformados, quebrados ou destruídos pela colisão. (balas, flechas, balões de água)

Outros simplesmente pulavam e rolavam para o lado. (pedras) Eles ainda poderiam sofrer danos se a coisa que atingissem fosse forte o suficiente. Rochas não sofrem dano de um ser humano atingindo-a, mas podem ser causadas por uma marreta.

Alguns objetos macios como humanos sofreram danos.

Outros estariam ligados um ao outro. (ímãs)

Ambas as colisões seriam colocadas em uma fila de manipulação de eventos para um ator atuando em um objeto em um determinado momento e local (e velocidade). Lembre-se de que o manipulador deve lidar apenas com o que acontece com o objeto. É até possível para um manipulador colocar outro manipulador na fila para lidar com efeitos adicionais da colisão com base nas propriedades do ator ou do objeto em que está sendo atuado. (A bomba explodiu e a explosão resultante agora precisa testar colisões com outros objetos.)

Ao criar scripts, use tags com propriedades que seriam traduzidas em implementações de interface pelo seu código. Em seguida, faça referência às interfaces no seu código de manipulação.

Por exemplo, uma espada pode ter um tag para Melee, que se traduz em uma interface chamada Melee, que pode estender uma interface DamageGiving que possui propriedades para o tipo de dano e uma quantidade de dano definida usando um valor fixo ou intervalos, etc. E uma bomba pode ter uma etiqueta explosiva cuja interface explosiva também estende a interface DamageGiving, que possui uma propriedade adicional de raio e possivelmente a rapidez com que o dano se difunde.

Seu mecanismo de jogo codificaria contra as interfaces, não tipos de objetos específicos.

Rick Ryker
fonte