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:
Aquí una representación de los volúmenes de colisión (Show > Player Collision):
Sin embargo, podemos aproximar el volumen de colisión de la silla con uno más simple. Desde el editor de static mesh:
Ahora las simulaciones quedaría:
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:
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:
- ¿Le has dado a algo?
- Si le has dado, ¿a qué objeto?
- ¿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:
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:
¿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
Y podemos definir el comportamiento (ó respuesta) de cada actor para cada canal en las propiedades de cada actor:
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:
Fíjate que como entradas tenemos:
- Que definir la figura geométrica imaginaria
- El channel que vamos a usar
- 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):
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:
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:
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
Para aplicar un preset a un actor:
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:
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.
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.
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:
O si tienes varios componentes y quieres tener un control fino, puedes hacer un bind sobre el component que genera el overlap:
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
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.
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)
{
}