Física básica de plataformas en 2D, parte 6: Respuesta de colisión entre objetos
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
En la entrega anterior de la serie, implementamos un mecanismo de detección de colisión entre los objetos del juego. En esta parte, usaremos el mecanismo de detección de colisión para construir una respuesta física simple pero robusta entre los objetos.
La demostración muestra el resultado final de este tutorial. Usa WASD para mover el personaje. El botón central del mouse genera una plataforma unidireccional, el botón derecho del mouse engendra un mosaico sólido y la barra espaciadora genera un clon de caracteres. Los controles deslizantes cambian el tamaño del personaje del jugador.
La demostración se publicó bajo Unity
5.4.0f3 y el código fuente también es compatible con esta versión de
Unity.
Respuesta de colisión
Ahora que tenemos todos los datos de colisión del trabajo que hemos realizado en la parte anterior, podemos agregar una respuesta simple a los objetos en colisión. Nuestro objetivo aquí es hacer posible que los objetos no se atraviesen como si estuvieran en un plano diferente; queremos que sean sólidos y actúen como un obstáculo o una plataforma para otros objetos. Para eso, tenemos que hacer una sola cosa: mover el objeto de una superposición, si ocurre.
Cubra los Datos Adicionales
Necesitaremos algunos datos
adicionales para la clase MovingObject
para manejar la respuesta objeto
contra objeto. Antes
que nada, es bueno tener un booleano para marcar un objeto como
cinemático, es decir, este objeto no será empujado por ningún otro
objeto.
Estos objetos funcionarán bien como plataformas, y también pueden ser plataformas móviles. Se supone que son las cosas más pesadas, por lo que su posición no se corregirá de ninguna manera; otros objetos necesitarán alejarse para dejar espacio para ellos.
public bool mIsKinematic = false;
Los otros datos que me gustaría tener son información sobre si estamos parados sobre un objeto o hacia su lado izquierdo o derecho, etc. Hasta ahora solo podíamos interactuar con los mosaicos, pero ahora también podemos interactuar con otros objetos.
Para traer algo de armonía a esto, necesitaremos un nuevo conjunto de variables que describan si el personaje está empujando algo a la izquierda, derecha, arriba o abajo.
public bool mPushesRight = false; public bool mPushesLeft = false; public bool mPushesBottom = false; public bool mPushesTop = false; public bool mPushedTop = false; public bool mPushedBottom = false; public bool mPushedRight = false; public bool mPushedLeft = false; public bool mPushesLeftObject = false; public bool mPushesRightObject = false; public bool mPushesBottomObject = false; public bool mPushesTopObject = false; public bool mPushedLeftObject = false; public bool mPushedRightObject = false; public bool mPushedBottomObject = false; public bool mPushedTopObject = false; public bool mPushesRightTile = false; public bool mPushesLeftTile = false; public bool mPushesBottomTile = false; public bool mPushesTopTile = false; public bool mPushedTopTile = false; public bool mPushedBottomTile = false; public bool mPushedRightTile = false; public bool mPushedLeftTile = false;
Ahora son muchas variables. En un entorno de producción, valdría la pena convertirlos en banderas y tener un solo entero en lugar de todos estos booleanos, pero en aras de la simplicidad manejaremos estos como están.
Como puede notar, aquí tenemos datos bastante precisos. Sabemos si el personaje empuja o empuja un obstáculo en una dirección particular en general, pero también podemos preguntar fácilmente si estamos al lado de un azulejo o un objeto.
Salir de la superposición
Vamos
a crear la función UpdatePhysicsResponse
, en la que manejaremos la
respuesta objeto contra objeto.
private void UpdatePhysicsResponse() { }
En primer lugar, si el objeto está marcado como cinemático, simplemente volvemos. No manejamos la respuesta porque el objeto cinemático no necesita responder a ningún otro objeto; los otros objetos necesitan responder a él.
if (mIsKinematic) return;
Ahora, esto supone que no necesitaremos un objeto cinemático para tener los datos correctos con respecto a si está empujando un objeto en el lado izquierdo, etc. Si ese no es el caso, entonces tendría que modificarse un poco, lo que profundizare más adelante.
Ahora comencemos a manejar las variables que acabamos de declarar recientemente.
mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;
Guardamos los resultados del cuadro anterior en las variables apropiadas y por ahora suponemos que no estamos tocando ningún otro objeto.
Comencemos iterando a través de todos nuestros datos de colisión ahora.
for (int i = 0; i < mAllCollidingObjects.Count; ++i) { var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap; }
Primero, manejemos los casos en que los objetos apenas se tocan entre sí, sin superponerse realmente. En este caso, sabemos que realmente no tenemos que mover nada, solo establecemos las variables.
Como se mencionó anteriormente, el indicador de que los objetos se están tocando es que la superposición en uno de los ejes es igual a 0. Comencemos por verificar el eje x.
if (overlap.x == 0.0f) { }
Si la condición es verdadera, necesitamos ver si el otro objeto está en el lado izquierdo o derecho de nuestra AABB.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { } else { } }
Finalmente,
si está a la derecha, configure mPushesRightObject
en true y configure
la velocidad para que no sea mayor que 0, porque nuestro objeto ya no
puede moverse hacia la derecha ya que la ruta está bloqueada.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { } }
Manejemos el lado izquierdo de la misma manera.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } }
Finalmente, sabemos que no necesitaremos hacer nada más aquí, así que continuemos con la siguiente iteración de bucle.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } continue; }
Manejemos el eje y de la misma manera.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } continue; } else if (overlap.y == 0.0f) { if (other.mAABB.center.y > mAABB.center.y) { mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } continue; }
Este es también un buen lugar para establecer las variables para un cuerpo cinemático, si es que necesitamos hacerlo. No nos importaría si la superposición es igual a cero o no, porque de todos modos no vamos a mover un objeto cinemático. También necesitaríamos omitir el ajuste de velocidad ya que no queremos detener un objeto cinemático. Sin embargo, omitiremos hacer todo esto para la demostración, ya que no vamos a usar las variables auxiliares para objetos cinemáticos.
Ahora que está cubierto, podemos manejar los objetos que se han superpuesto correctamente con nuestra AABB. Sin embargo, antes de hacer eso, permítanme explicar el enfoque que tomé para la respuesta de colisión en la demostración.
En primer lugar, si el objeto no se mueve y nos topamos con él, el otro objeto no se moverá. Lo tratamos como un cuerpo cinemático. Decidí ir por este camino porque creo que es más genérico, y el comportamiento de empuje siempre se puede manejar más adelante en la actualización personalizada de un objeto en particular.

Si ambos objetos se movieron durante la colisión, dividimos la superposición entre ellos en función de su velocidad. Cuanto más rápido iban, la mayor parte del valor de superposición se reubicaría.
El último punto es que, de forma similar al enfoque de respuesta de mapa de mosaico, si un objeto se cae y mientras baja rasguña otro objeto incluso en un píxel horizontalmente, el objeto no se deslizará y continuará hacia abajo, sino que se colocará en ese píxel.

Creo que este es el enfoque más maleable, y modificarlo no debería ser muy difícil si quieres manejar alguna respuesta de manera diferente.
Continuemos la implementación calculando el vector de velocidad absoluta para ambos objetos durante la colisión. También necesitaremos la suma de las velocidades, así que sabemos qué porcentaje de la superposición se debe mover nuestro objeto.
Vector2 absSpeed1 = new Vector2(Mathf.Abs(data.pos1.x - data.oldPos1.x), Mathf.Abs(data.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = new Vector2(Mathf.Abs(data.pos2.x - data.oldPos2.x), Mathf.Abs(data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;
Tenga en cuenta que en lugar de usar la velocidad guardada en los datos de colisión, estamos utilizando el desplazamiento entre la posición en el momento de la colisión y el marco anterior a eso. Esto será más preciso en este caso, ya que la velocidad representa el vector de movimiento antes de la corrección física. Las posiciones mismas se corrigen si el objeto ha golpeado un azulejo sólido, por ejemplo, así que si queremos obtener un vector de velocidad corregido debemos calcularlo así.
Ahora comencemos a calcular la relación de velocidad para nuestro objeto. Si el otro objeto es cinemático, estableceremos la relación de velocidad en uno, para asegurarnos de mover todo el vector de superposición, respetando la regla de que el objeto cinemático no debe moverse.
float speedRatioX, speedRatioY; if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else { }
Ahora
comencemos con un caso extraño en el que ambos objetos se superponen
pero ninguno tiene velocidad alguna. En
realidad, esto no debería suceder, pero si un objeto se genera
superponiendo otro objeto, nos gustaría que se separen de forma natural.
En ese caso, nos gustaría que ambos se muevan en un 50% del vector de
superposición.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else { if (speedSum.x == 0.0f && speedSum.y == 0.0f) { speedRatioX = speedRatioY = 0.5f; } }
Otro caso es
cuando speedSum
en el eje x es igual a cero. En
ese caso calculamos la relación adecuada para el eje y, y establecemos
que debemos mover el 50% de la superposición para el eje x.
if (speedSum.x == 0.0f && speedSum.y == 0.0f) { speedRatioX = speedRatioY = 0.5f; } else if (speedSum.x == 0.0f) { speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; }
Del mismo
modo, manejamos el caso en el que speedSum
es cero solo en el eje y, y
para el último caso calculamos ambas proporciones adecuadamente.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else { if (speedSum.x == 0.0f && speedSum.y == 0.0f) { speedRatioX = speedRatioY = 0.5f; } else if (speedSum.x == 0.0f) { speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; } else if (speedSum.y == 0.0f) { speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = 0.5f; } else { speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y; } }
Ahora que se calculan las relaciones, podemos ver cuánto necesitamos para compensar nuestro objeto.
float offsetX = overlap.x * speedRatioX; float offsetY = overlap.y * speedRatioY;
Ahora, antes de decidir si debemos mover el objeto fuera de la colisión en el eje x o el eje y, calculemos la dirección desde la cual ocurrió la superposición. Hay tres posibilidades: o nos topamos con otro objeto horizontal, vertical o diagonalmente.
En el primer caso, queremos salir de la superposición en el eje x, en el segundo caso queremos salir de la superposición en el eje y, y en el último caso, queremos salir de la superposición en cualquiera de los dos; eje tenía la menor superposición.

Recuerde que para superponer con otro objeto necesitamos que las AABB se superpongan entre sí en los ejes xey. Para comprobar si chocamos con un objeto horizontalmente, veremos si el marco anterior ya estaba superponiendo el objeto en el eje y. Si ese es el caso, y no hemos estado superponiendo en el eje x, entonces la superposición debe haber sucedido porque en el marco actual las AABB comenzaron a superponerse en el eje x, y por lo tanto deducimos que tropezamos con otro objeto horizontalmente.
Primero, calculemos si nos superpusimos con la otra AABB en el cuadro anterior.
bool overlappedLastFrameX = Mathf.Abs(data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX; bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;
Ahora configuremos la condición para salir de la superposición horizontalmente. Como se explicó anteriormente, necesitábamos haber superpuesto en el eje Y y no superpuesto en el eje x en el cuadro anterior.
if (!overlappedLastFrameX && overlappedLastFrameY) { }
Si ese no es el caso, saldremos de la superposición en el eje y.
if (!overlappedLastFrameX && overlappedLastFrameY) { } else { }
Como se mencionó anteriormente, también tenemos que cubrir el escenario de toparse con el objeto diagonalmente. Chocamos con el objeto en diagonal si nuestras AABB no se superponían en el cuadro anterior en ninguno de los ejes, porque sabemos que en el cuadro actual se superponen en ambos, por lo que la colisión debe haber sucedido en ambos ejes simultáneamente.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && overlappedLastFrameY)) { } else { }
Pero queremos salir de la superposición en el eje en caso de una protuberancia diagonal solo si la superposición en el eje x es menor que la superposición en el eje y.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { } else { }
Eso es todos los casos resueltos. Ahora realmente necesitamos mover el objeto de la superposición.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { mPosition.x += offsetX; if (overlap.x < 0.0f) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { }
Como puede ver, lo manejamos de manera muy similar al caso en que apenas tocamos otra AABB, pero adicionalmente movemos nuestro objeto por la compensación calculada.
La corrección vertical se realiza de la misma manera.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && !overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { mPosition.x += offsetX; if (overlap.x < 0.0f) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { mPosition.y += offsetY; if (overlap.y < 0.0f) { mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } }
Eso es casi todo; solo hay una advertencia más que cubrir. Imagine el escenario en el que aterrizamos en dos objetos simultáneamente. Tenemos dos instancias de datos de colisión casi idénticas. A medida que iteramos a través de todas las colisiones, corregimos la posición de la colisión con el primer objeto, moviéndonos un poco hacia arriba.
Luego, manejamos la colisión para el segundo objeto. La superposición guardada en el momento de la colisión ya no está actualizada, como ya nos hemos movido desde la posición original, y si tuviéramos que manejar la segunda colisión de la misma manera que manejamos la primera, volveríamos a subir un poco , haciendo que nuestro objeto sea corregido el doble de la distancia que se suponía que debía.

Para solucionar este problema, realizaremos un seguimiento de
cuánto hemos corregido ya el objeto. Declaremos el vector offsetSum
justo antes de comenzar a iterar a través de todas las colisiones.
Vector2 offsetSum = Vector2.zero;
Ahora, asegurémonos de sumar todas las compensaciones que aplicamos a nuestro objeto en este vector.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && !overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { mPosition.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { mPosition.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f) { mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } }
Y, por último, vamos a compensar la superposición de cada colisión consecutiva por el vector acumulativo de correcciones que hemos realizado hasta ahora.
var overlap = data.overlap - offsetSum;
Ahora si aterrizamos en dos objetos de la misma altura al mismo tiempo, la primera colisión se procesará correctamente y la superposición de la segunda colisión se compensaría a cero, lo que ya no movería nuestro objeto.

Ahora que nuestra función está lista, asegurémonos de usarla. Un
buen lugar para llamar a esta función sería después de la llamada
CheckCollisions
. Esto
requerirá que dividamos nuestra función UpdatePhysics
en dos partes,
así que vamos a crear la segunda parte ahora mismo, en la clase
MovingObject
.
public void UpdatePhysicsP2() { UpdatePhysicsResponse(); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; }
En la
segunda parte llamamos a nuestra función UpdatePhysicsResponse
recién
terminada y actualizamos las variables generales izquierda, derecha,
inferior y superior. Después de esto, solo tenemos que aplicar el puesto.
public void UpdatePhysicsP2() { UpdatePhysicsResponse(); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; //update the aabb mAABB.center = mPosition; //apply the changes to the transform transform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y), mSpriteDepth); transform.localScale = new Vector3(ScaleX, ScaleY, 1.0f); }
Ahora, en
el ciclo de actualización del juego principal, llamemos a la segunda
parte de la actualización de física después de la llamada a
CheckCollisions
.
void FixedUpdate() { for (int i = 0; i < mObjects.Count; ++i) { switch (mObjects[i].mType) { case ObjectType.Player: case ObjectType.NPC: ((Character)mObjects[i]).CustomUpdate(); mMap.UpdateAreas(mObjects[i]); mObjects[i].mAllCollidingObjects.Clear(); break; } } mMap.CheckCollisions(); for (int i = 0; i < mObjects.Count; ++i) mObjects[i].UpdatePhysicsP2(); }
¡Hecho! Ahora nuestros objetos no pueden superponerse entre sí. Por supuesto, en una configuración de juego tendríamos que agregar algunas cosas como grupos de colisión, etc., por lo que no es obligatorio detectar o responder a una colisión con cada objeto, pero estos son elementos que dependen de cómo desee tener las cosas establecidas en su juego, por lo que no vamos a profundizar en eso.
Resumen
Eso es todo por otra parte de la simple serie de física de plataformas 2D. Hicimos uso del mecanismo de detección de colisión implementado en la parte anterior para crear una respuesta física simple entre los objetos.
Con estas herramientas, es posible crear objetos estándar, como plataformas móviles, bloques de empuje, obstáculos personalizados y muchos otros tipos de objetos que no pueden ser parte del mapa de mosaicos, pero que aún deben formar parte del terreno nivelado de alguna manera. Hay una característica más que nuestra implementación de la física aún no tiene, y esas son las pendientes.
Esperemos que en la siguiente parte comencemos a ampliar nuestro mapa de mosaicos con el soporte para estos, que completaría el conjunto básico de características que debería tener una implementación de física sencilla para un juego de plataformas en 2D, y eso terminaría con la serie.
Por supuesto, siempre hay margen de mejora, así que si tienes alguna pregunta o consejo sobre cómo hacer algo mejor, o simplemente tienes una opinión sobre el tutorial, ¡no dudes en utilizar la sección de comentarios para avisarme!
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post