Colisiones, colisiones everywhere

Las colisiones en Unreal son una característica transversal: la puedes encontrar en el editor de static mesh, en los physics asset, en las propiedades de muchos componentes, en los project settings ¡y en más sitios!.

Tenemos conceptos importantes como canales, tipos de objetos y profiles que están íntimimamente ligados a las colisiones.

Entender como funcionan y poner en orden estos conceptos será el objetivo de este post.

Vamos a entender la filosofía que hay detrás de las colisiones y después de ello nos tiraremos al fango y haremos varios ejemplos.

¿Dónde están mis colisiones?

Las colisiones vienen en dos sabores: simple y compleja.

Fíjate en el siguiente ejemplo:

Colisión compleja

Aquí una representación de los volúmenes de colisión (Show > Player Collision):

Volúmenes de colisión para colisiones complejas

Sin embargo, podemos aproximar el volumen de colisión de la silla con uno más simple. Desde el editor de static mesh:

Aproximar el volumen de colisión por uno más simple

Ahora las simulaciones quedaría:

Colisión simple
Volumen de colisión simple

Aproximar el volumen de colisión con otras formas más simples es lo que se llama volumen de colisión simple. Mientras que usar la propia geometría como volumen de colisión es la forma compleja.

En el editor de static mesh podemos editar su forma simple. Incluso podemos obligar a usar siempre la forma simple o la forma compleja:

Colisión simple o compleja

La pregunta sería ¿por qué mantener dos volúmenes de colisión (simple y complejo)?

La primera respuesta sería por rendimiento. La primera simulación (la compleja) es mucho más costosa computacionalmente hablando que la segunda (la simple).

La segunda respuesta es porque nos interesa. Imagínate un personaje humano. Podemos aproximar su forma simple con una cápsula. De este modo todas las colisiones con el entorno cuando ande, corra o salte, son muy eficientes.

Sin embargo, cuando lo disparemos podemos usar la colisión en su forma compleja para saber dónde exactamente dió el disparo.

Sabiendo esto, vamos a ver que podemos hacer con las colisiones.

Block, Overlap & Ignore

UE4 define tres sucesos que pueden ocurrir cuando dos objetos colisionan:

  • Block – que dos objetos se bloqueen cuando colisionen. Por ejemplo cuando un personaje choca contra la pared. O cuando una pelota impacta contra la silla.
  • Overlap – no hay colisión y, por tanto, el objeto traspasa al otro objeto cuál fantasma. El engine nos informará cuando ésto ocurre. Por ejemplo, podríamos definir un área invisible que sirva como checkpoint o lugar dónde tenga que llegar el personaje.
  • Ignore – no hay colisión y, por tanto, el objeto traspasa al otro objeto cuál fantasma. El engine ignora completamente la colisión y no sabremos de ella.

Las colisiones en UE4 sirven principalmente para tres cosas:

  • ¿Qué hacer cuando dos actores colisionan?
  • Simulación física
  • Tracing

Vamos a empezar por la parte difícil: el tracing.

Tracing

¿Qué es tracing? Es preguntar al engine si un objeto colisiona con una figura geométrica (línea, esfera, cubo, etc) imaginaria. ¿Qué demonios es eso? Veámoslo con un par de ejemplos.

Imagina que en tu juego shooter tienes un arma. Cuando dispares nos gustaría responder a las siguientes cuestiones:

  1. ¿Le has dado a algo?
  2. Si le has dado, ¿a qué objeto?
  3. ¿En qué posición ha sido el impacto?

Para responder a estas preguntas podemos definir una línea imaginaria (esa es la figura imaginaria) que va desde la boca del arma hasta el alcance de la misma y le preguntamos al engine si esa línea colisiona con algo.

El engine nos devolverá todos los objetos junto con la información de dónde ha sido el impacto "imaginario".

Otro ejemplo. Imagina que tienes una granada. Cuando estalle, nos gustaría preguntarnos que actores están en su radio.

Para responder a esta pregunta, definimos una esfera (esa es la figura imaginaria) con el radio de la explosión y preguntamos al engine que objetos colisionan con esa esfera (y por tanto están dentro del radio de explosión).

Por lo tanto, para hacer un trace primeramente necesitamos definir la forma geometrica imaginaria. Por ejemplo para una línea bastaría definir su punto inicial y final, para una esfera su centro y radio, etc.,

Sin embargo, antes de bajar al suelo y echar un vistazo concreto a como quedaría en código, necesitamos un ingrediente más.

Trace Channels

Los trace channels son el ingrediente final para hacer un trace.

Cuando lanzamos un trace solo preguntamos por los objetos que bloqueen a un channel en concreto.

Para cada actor en la escena definimos su comportamiento (Block, Overlap, Ignore) para cada channel individual.

Parece lioso pero es más fácil de lo que parece. Veamos el siguiente ejemplo:

Tracing del arma. El disparo da contra la pared, pero el láser colisiona antes con la planta

El arma tiene una mirilla laser que colisiona con la planta. Pero el proyectil del arma no colisiona con la planta, si no que la ignora y colisiona contra la pared (ilustrado en la imagen con una esfera amarilla).

En la siguiente imagen hemos desplazado el arma y la planta ya no estorba, por lo que la mirilla colisiona en el mismo sitio que el proyectil:

La mirilla láser y el disparo colisionan en el mismo lugar

¿Cómo modelamos esto en UE4?

Cuando lanzamos un Line Trace para saber dónde colocar el extremo del láser de la mirilla, sabemos que dicho trace debe colisionar con la planta y colisionar contra la pared. Es decir, debe colisionar con todo lo "visible". La planta es visible y la pared también.

Por otra parte, el proyectil no debe colisionar con la planta. Sí con la pared. Podríamos decir que el proyectil colisiona con todo lo "impactable". La planta no es "impactable".

Parece que tenemos al menos dos canales, el canal "visible" y el canal "impactable".

Y una vez definidos, concretamos el comportamiento de cada actor para cada canal tal que así:

Actor Canal Comportamiento
Planta Visible Block
Planta Impactable Overlap ó Ignore
Pared Visible Block
Pared Impactable Block

¿Cómo hacemos esto en el engine?

Pues UE4 ya trae de serie un par de canales: Visibility y Camera pero podemos añadir nuevos canales en Project Settings > Collision

Añadir un nuevl canal para tracing

Y podemos definir el comportamiento (ó respuesta) de cada actor para cada canal en las propiedades de cada actor:

Collision Response

De ste modo en nuestro ejemplo:

Si hacemos un line trace en el canal "visible" desde la boca del arma, ¿con qué objeto colisionará nuestra línea imaginaria? Respuesta: Con la planta.

Pero si hacemos el mismo trace con el canal "impactable" el engine ignorará la planta y dirá que hemos colisionado con la pared. Porque la planta tiene como comportamiento Overlap (ó Ignore) para el canal "impactable".

Toma Trace

Pues ya tenemos todos los ingredientes para ver como hacer un trace en Unreal.

En blueprint podemos lanzar trace con los nodos:

Blueprint Trace

Fíjate que como entradas tenemos:

  1. Que definir la figura geométrica imaginaria
  2. El channel que vamos a usar
  3. Y además podemos usar la colisión en su forma compleja o simple.

En C++:

FHitResult OutHit;

FVector Start = ...;
FVector End = ...;
ECollisionChannel TraceChannel = ECC_GameTraceChannel1; // ECC_xxxxx

FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(...);
QueryParams.bReturnPhysicalMaterial = true;
QueryParams.bTraceComplex = true;

UWorld* World = GetWorld();
bool bHit = World->LineTraceSingleByChannel(OutHit,
                                            Start,
                                            End,
                                            TraceChannel,
                                            QueryParams);

if (bHit)
{
	// Do something:
	// OutHit.ImpactPoint;
	// AActor* HitActor = OutHit.GetActor();
	// OutHit.Distance
	// ...
}

Dónde ECC_GameTraceChannel1 es el primer canal personalizado en Project Settings > Collision. En nuestro ejemplo el canal Impactable. Para los canales predeterminados podemos usar constantes ya establecidas como ECC_Visibility ó ECC_Camera.

Object Channels

Los object channels son los hermanos de los trace channels. A cada actor se le asigna un tipo de objeto (es decir, un object channel):

Object Type

Ya hay unos cuantos predefinidos que podemos usar: WorldStatic, WorldDynamic, Pawn, PhysicsBody, Vehicle y Destructible pero puedes añadir más si los necesitas en Project Settings > Collisions.

¿Cuando usar object channel frente a trace channel? Un ejemplo sería una explosión. Al igual que antes, puedes definir una esfera imaginaria con el radio de la explosión. Pero en vez de usar un trace channel, puedes preguntar por object channel para encontrar todos los objetos de tipo Pawn o PhysicsBody en un radio y aplicarles un daño.

En blueprint puedes hacer trace con object channel usando los nodos:

Trace for Objects

Y en C++:

FCollisionObjectQueryParams ObjectQueryParams;
ObjectQueryParams.AddObjectTypesToQuery(ECC_GameTraceChannel2);

FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
QueryParams.bTraceComplex = true;
QueryParams.bReturnPhysicalMaterial = true;

FVector Start = ...;
FVector End = ...;

UWorld* World = GetWorld();
bool bHit = World->LineTraceTestByObjectType(Start,
					     End,
					     ObjectQueryParams,
					     QueryParams);
if (bHit)
{
	// do something
}

Recuerda que en C++ tenemos las constantes ECC_GameTraceChannelXXX para usar los channels personalizados que definimos en project settings:

Game Trace Channel

Si abres el fichero DefaultEngine.ini podemos ver a qué canal corresponde cada ECC_GameTraceChannelXXX:

+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,Name="TestTrace1",DefaultResponse=ECR_Block,bTraceType=True,bStaticObject=False)
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel2,Name="TestTrace2",DefaultResponse=ECR_Block,bTraceType=True,bStaticObject=False)
+DefaultChannelResponses=(Channel=ECC_GameTraceChannel3,Name="TestObject1",DefaultResponse=ECR_Block,bTraceType=False,bStaticObject=False)

Tal y como se puede ver, ECC_GameTraceChannel1 corresponde al trace channel TestTrace1, ECC_GameTraceChannel2 corresponde al trace channel TestTrace2 y ECC_GameTraceChannel3 corresponde al object channel TestObject1.

Collision Presets

Volviendo al ejemplo del arma:

Se me ocurre que muchos objetos presentan el mismo comportamiento frente a los channels que las plantas. Por ejemplo: cortinas de agua ó columnas de humo. Serían visibles pero no impactables.

Con lo que sabemos hasta ahora, tendríamos que ir a las propiedades de cada actor cortina de agua y columna de humo y modificar su respuesta para cada channel tal y como hicimos con la planta.

Sin embargo, para evitar esta repetición, UE4 introduce el concepto de collision presets.

Los collision preset, como su propio nombre indica, permiten definir el comportamiento para cada channel y luego basta con seleccionar dicho preset en el actor.

El engine ya nos ofrece varios collision presets predeterminados pero podemos crear nuevos en Project Settings > Collision > Preset

New Collision Preset

Para aplicar un preset a un actor:

Collision Presets

Colisiones entre objetos que se mueven

Hasta ahora solo hemos visto el tracing ¿pero que ocurre cuando las colisiones ocurren entre objetos en movimiento?

Cada objeto en UE4 conoce su propio Object Channel porque lo seteamos en su propiedad Object Type:

Object Type

Esta propiedad Object Type nos servía para el tracing con object channels pero también nos va a servir de utilidad para gestionar las colisiones entre objetos que se mueven.

¿Que ocurre cuando un objeto del tipo A colisiona con un objeto de tipo B? UE4 toma los valores que dicen su object responses y resuelve con el menos restrictivo.

Object Reponses Channels

Lo mejor es ilustrarlo con un ejemplo:

En el ejemplo, tenemos tres actores: un personaje, una pirámide y un cubo.

Tal y como se puede ver en las propiedads, el personaje es de tipo Pawn y si colisiona con un World Static la respuesta es Block.

Puedes comprobar como la pirámide es un World Static, al igual que el cubo y si la pirámide colisiona con un Pawn la respuesta es Ignore y para el cubo la respuesta es Block.

Entonces ¿que ocurrirá cuando el personaje se mueva de izquierda a derecha y colisione con la pirámide y el cubo?

Cuando el personaje colisiona con la pirámide el engine pregunta al personaje que hacer si colisiona con un objeto del tipo World Static (recuerda que la pirámide es del tipo World Static). La respuesta es Block. El engine ahora le pregunta a la pirámide que hacer si colisiona contra un Pawn (recuerda que el personaje es del tipo Pawn) y la respuesta es Ignore.

¿Cuál de las dos respuestas (Block e Ignore) es la menos restrictiva? Ignore. Pues así resuelve el engine y eso es lo ocurre. El personaje ignora la pirámide cuando colisiona con ella.

Sin embargo con el cubo ambos actores responden Block por lo que se resuelve con un bloqueo y por eso el personaje no puede traspasar el cubo.

Overlap y los eventos

¿Qué diferencia hay entre overlap e ignore? Si tienen el checkbox "Generate Overlap Events" desactivados, no hay ninguna diferencia.

Generate Overlap Events

Pero si el engine resuelve una colisión como overlap y AMBOS componentes tienen activado "Generate Overlap Events" entonces ambos actores son notificados por código.

En blueprint se puede usar el evento:

Overlap Event

O si tienes varios componentes y quieres tener un control fino, puedes hacer un bind sobre el component que genera el overlap:

Overlap Event Blueprint Bind

También tienes el evento OnComponentEndOverlap.

Importante volver a subrayar que para que el evento sea llamado el engine debe resolver Overlap y ambos deben tener marcados "Generate Overlap Events". Si alguno no lo tiene marcado, no se lanzará el evento. Esto se hace por razones de rendimiento.

Del mismo modo que con los overlap, también tenemos un evento asociado a las colisiones para cuando se resuelva Block. En este caso el evento es Hit y se activa con el checkbox: Simulation Generates Hit Event

Simulation Generates Hit Events

A diferencia que ocurre con los eventos Overlap, para los eventos Hit no se requiere que ambos componentes tengan Simulation Generates Hit Event activados para que se lance el evento.

Eventos Hit

El equivalente en C++:

// Evento generico para overlap
void AMyActor::NotifyActorBeginOverlap(AActor* OtherActor)
{

}

// Evento generico para hit
void AMyActor::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
{

}

O también se puede hacer con grano fino:

void AMyActor::BeginPlay()
{
	Super::BeginPlay();

	UPrimitiveComponent* AnyComponent = ...;
	// AnyComponent puede ser un BoxComponent,
	// SphereComponent, CapsuleComponent,
	// StaticMeshComponent y, en definitiva,
	// cualquier componente que herede de PrimitiveComponent

	AnyComponent->bGenerateOverlapEvents = true;
	AnyComponent->OnComponentBeginOverlap.AddDynamic(this, &AMyActor::OnCustomEvent_BeginOverlap);

	AnyComponent->SetNotifyRigidBodyCollision(true); // Simulation Generate Hit Events
	AnyComponent->OnComponentHit.AddDynamic(this, &AMyActor::OnCustomEvent_OnHit);
}

void AMyActor::OnCustomEvent_OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{

}

void AMyActor::OnCustomEvent_BeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{

}