Física básica de plataformas en 2D, parte 8: pendientes
Spanish (Español) translation by Luis Chiabrera (you can also view the original English article)



Demostración
La demostración muestra el resultado final de la implementación de la pendiente. Usa WASD para mover el personaje. El botón derecho del mouse crea un mosaico. Puede usar la rueda de desplazamiento o las teclas de flecha para seleccionar un mosaico que desea colocar. Los controles deslizantes cambian el tamaño del personaje del jugador.
La demostración se publicó bajo Unity 5.5.2f1, y el código fuente también es compatible con esta versión de Unity.
Antes de empezar...
Al igual que en las partes anteriores de la serie, continuaremos nuestro trabajo donde lo dejamos en la última parte. La última vez calculamos y almacenamos en caché los datos necesarios para mover los objetos fuera de la colisión de las pendientes y cambiamos la forma en que se verifican las colisiones contra el mapa de mosaicos. En esta parte necesitaremos la misma configuración desde el final de la última parte.
Puede descargar los archivos del proyecto de la parte anterior y escribir el código junto con este tutorial.
En esta parte implementaremos la colisión con pendientes u otras fichas personalizadas, añadiremos pendientes de un solo sentido y posibilitará que el objeto del juego se desplace suavemente por las laderas.
Implementación de pendientes
Control vertical de la pendiente
¡Finalmente podemos llegar a las pistas! En primer lugar, trataremos de manejar cuando el borde inferior del objeto se encuentre dentro de un mosaico de pendiente.
Vamos a echar un vistazo a nuestra función CollidesWithTileBottom, particularmente la parte donde manejamos las fichas.
1 |
switch (tileCollisionType) |
2 |
{
|
3 |
default://slope |
4 |
break; |
5 |
case TileCollisionType.Empty: |
6 |
break; |
7 |
case TileCollisionType.Full: |
8 |
state.onOneWay = false; |
9 |
state.pushesBottomTile = true; |
10 |
state.bottomTile = new Vector2i(x, bottomleftTile.y); |
11 |
return true; |
12 |
} |
Para poder ver si nuestro objeto colisiona con la pendiente, primero debemos obtener los desplazamientos de la función que creamos anteriormente, que hace la mayor parte de nuestro trabajo.
1 |
Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y); |
2 |
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType); |
Dado que estamos comprobando un píxel debajo de nuestro personaje, tenemos que ajustar el desplazamiento.
1 |
Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y); |
2 |
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType); |
3 |
sf.freeUp -= 1; |
4 |
sf.collidingBottom -= 1; |
La condición
para la colisión es que el freeUp es mayor o igual
a 0, lo que significa que o movemos el personaje hacia arriba o el
personaje está de pie en la pendiente.
1 |
Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y); |
2 |
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType); |
3 |
sf.freeUp -= 1; |
4 |
sf.collidingBottom -= 1; |
5 |
|
6 |
if (sf.freeUp >= 0) |
7 |
{
|
8 |
}
|
Sin embargo, no deberíamos olvidarnos del caso cuando queremos que el personaje se adhiera a la pendiente. Esto significa que, aunque el personaje abandone la pendiente, queremos que se comporte como si estuviera en la pendiente de todos modos. Para esto, necesitamos agregar una nueva constante que contendrá el valor de qué tan pronunciada debe ser una pendiente para poder ser considerada una pared vertical en lugar de una pendiente.
1 |
public const int cSlopeWallHeight = 4; |
Si el desplazamiento está por debajo de esta constante, debería ser posible que el objeto avance suavemente a lo largo de la curva de la pendiente. Si es igual o mayor, debe tratarse como una pared, y saltar sería necesario para subir.
Ahora tenemos que agregar otra condición a nuestra declaración. Esta condición verificará si se supone que el personaje se adhiere a pendientes, ya sea en el último cuadro de una pendiente, y si debe presionarse hacia abajo o hacia arriba en menos píxeles que nuestra constante cSlopeWallHeight.
1 |
if (sf.freeUp >= 0 || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight)) |
2 |
{
|
3 |
}
|
Si la condición es verdadera, debemos guardar este mosaico como posible colisión con el objeto. Todavía necesitaremos iterar a través de todos los otros mosaicos a lo largo del eje X. Primero, crea las variables que mantendrán la coordenada X y el valor de compensación para el mosaico que colisiona.
1 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); |
2 |
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); |
3 |
int collidingBottom = int.MinValue; |
4 |
int slopeX = -1; |
Ahora guarde los valores, si la condición definida es verdadera. Si ya encontramos un mosaico que colisiona, debemos comparar los desplazamientos, y el mosaico que colisionará será el que más necesita compensar al personaje.
1 |
if ((sf.freeUp >= 0 || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight)) |
2 |
&& sf.collidingBottom >= collidingBottom) |
3 |
{
|
4 |
collidingBottom = sf.collidingBottom; |
5 |
slopeX = x; |
6 |
}
|
Finalmente, después de iterar a través de todos los mosaicos y encontrar un mosaico con el que colisiona el objeto, necesitamos compensar el objeto.
1 |
if (slopeX != -1) |
2 |
{
|
3 |
state.pushesBottomTile = true; |
4 |
state.bottomTile = new Vector2i(slopeX, bottomleftTile.y); |
5 |
position.y += collidingBottom; |
6 |
topRight.y += collidingBottom; |
7 |
bottomLeft.y += collidingBottom; |
8 |
return true; |
9 |
}
|
10 |
|
11 |
return false; |
Eso es más o menos para el control de fondo, así que ahora vamos a hacer el más alto. Este será un poco más simple, ya que ni siquiera necesitamos manejar la adherencia.
1 |
public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); |
4 |
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
5 |
int freeDown = int.MaxValue; |
6 |
int slopeX = -1; |
7 |
|
8 |
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) |
9 |
{
|
10 |
var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); |
11 |
|
12 |
if (Slopes.IsOneWay(tileCollisionType)) |
13 |
continue; |
14 |
|
15 |
switch (tileCollisionType) |
16 |
{
|
17 |
default://slope |
18 |
|
19 |
Vector2 tileCenter = mMap.GetMapTilePosition(x, topRightTile.y); |
20 |
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y + 0.5f, tileCollisionType); |
21 |
sf.freeDown += 1; |
22 |
sf.collidingTop += 1; |
23 |
|
24 |
if (sf.freeDown < freeDown && sf.freeDown <= 0 && sf.freeDown == sf.collidingTop) |
25 |
{
|
26 |
freeDown = sf.freeDown; |
27 |
slopeX = x; |
28 |
}
|
29 |
|
30 |
break; |
31 |
case TileCollisionType.Empty: |
32 |
break; |
33 |
case TileCollisionType.Full: |
34 |
state.pushesTopTile = true; |
35 |
state.topTile = new Vector2i(x, topRightTile.y); |
36 |
return true; |
37 |
}
|
38 |
}
|
39 |
|
40 |
if (slopeX != -1) |
41 |
{
|
42 |
state.pushesTopTile = true; |
43 |
state.topTile = new Vector2i(slopeX, topRightTile.y); |
44 |
position.y += freeDown; |
45 |
topRight.y += freeDown; |
46 |
bottomLeft.y += freeDown; |
47 |
return true; |
48 |
}
|
49 |
|
50 |
return false; |
51 |
}
|
Eso es todo.
Control de pendiente horizontal
La verificación horizontal será un poco más complicada, ya que es aquí donde manejaremos los casos más problemáticos.
Comencemos con el manejo de las pendientes a la derecha. Hay un par de cosas que tendremos que tener en cuenta, sobre todo en cuanto a subir las pendientes. Consideremos las siguientes situaciones.



Tendremos que manejar esos casos con especial cuidado porque en algún momento, cuando avancemos por la pendiente, llegaremos al techo. Para evitar eso, necesitaremos hacer más controles en caso de que el personaje se mueva horizontalmente.
Tendremos que manejar estos casos con especial cuidado porque en algún momento, cuando lleguemos por la pendiente, lleguemos al techo. Para evitar eso, necesitaremos hacer más controles en caso de que el personaje se mueva horizontalmente. Para las verificaciones horizontales, es un poco diferente, porque este es el lugar donde manejaremos el movimiento a lo largo de la pendiente, por lo que, naturalmente, el ajuste de la altura tendrá lugar principalmente aquí.
Para hacer la respuesta de colisión adecuada para los casos ilustrados arriba, será más fácil verificar si podemos entrar en un espacio horizontalmente, y si eso es posible, entonces verifique si el objeto no se superpone con ningún píxel sólido si tuviera que ser movido verticalmente debido a moverse a lo largo de una pendiente. Si no logramos encontrar el espacio, sabemos que es imposible avanzar hacia la dirección marcada, y podemos establecer el indicador de pared horizontal.
Pasemos a la función CollidesWithTileRight, a la parte donde manejamos las pendientes.
1 |
default://slope |
2 |
|
3 |
Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y); |
4 |
float leftTileEdge = (tileCenter.x - Map.cTileSize / 2); |
5 |
float rightTileEdge = (leftTileEdge + Map.cTileSize); |
6 |
float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2); |
Obtenemos la compensación de forma similar a la de los controles verticales, pero la compensación que nos importa es la que es más grande.
1 |
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); |
2 |
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; |
Ahora, veamos si nuestro personaje debe tratar el mosaico marcado como una pared. Hacemos esto si el desplazamiento de la pendiente es mayor o igual a nuestra constante cSlopeWallHeight o para salir de la colisión necesitamos compensar el personaje hacia arriba o hacia abajo mientras estamos colisionando con una ficha en la misma dirección, lo que significa que nuestro el objeto se aprieta entre los azulejos superior e inferior.
1 |
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); |
2 |
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; |
3 |
|
4 |
if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)) |
5 |
{
|
6 |
state.pushesRightTile = true; |
7 |
state.rightTile = new Vector2i(topRightTile.x, y); |
8 |
return true; |
9 |
}
|
Si ese no es el caso y el desplazamiento es mayor que 0, entonces golpeamos una pendiente. Un problema aquí es que no sabemos si topamos con una pared en otras teselas que aún no hemos comprobado, de modo que por ahora solo guardaremos el desplazamiento de talud y el tipo de colisión de teselas en caso de que necesitemos utilizarlas más adelante.
1 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); |
2 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
3 |
float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; |
4 |
TileCollisionType slopeCollisionType = TileCollisionType.Empty; |
Ahora, en lugar de ver si el desplazamiento de la pendiente es mayor que cero, vamos a compararlo con el desplazamiento de la pendiente de otro mosaico, en caso de que ya hayamos encontrado una pendiente colisionante en iteraciones previas.
1 |
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); |
2 |
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; |
3 |
|
4 |
if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)) |
5 |
{
|
6 |
state.pushesRightTile = true; |
7 |
state.rightTile = new Vector2i(topRightTile.x, y); |
8 |
return true; |
9 |
}
|
10 |
else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset)) |
11 |
{
|
12 |
slopeCollisionType = tileCollisionType; |
13 |
state.rightTile = new Vector2i(topRightTile.x, y); |
14 |
}
|
15 |
else
|
16 |
slopeOffset = oldSlopeOffset; |
Manejar el apretar entre las baldosas
Después de terminar de recorrer todas las fichas de interés, veamos si necesitamos mover el objeto. Manejemos el caso donde el desplazamiento de la pendiente terminó siendo distinto de cero.
1 |
if (slopeOffset != 0.0f) |
2 |
{
|
3 |
}
|
Tendremos que manejar dos casos aquí, y tenemos que hacer cosas ligeramente diferentes dependiendo de si tenemos que compensar nuestro objeto hacia arriba o hacia abajo.
1 |
if (slopeOffset != 0.0f) |
2 |
{
|
3 |
if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight) |
4 |
{
|
5 |
}
|
6 |
}
|
En primer lugar, debemos verificar si podemos encajar en el espacio después de compensar el objeto. Si ese es el caso, entonces estamos manejando uno de los casos ilustrados arriba. Donde el personaje está tratando de moverse hacia la derecha, el desplazamiento es positivo, pero si desplazamos el objeto, entonces será empujado hacia la pared superior, así que en su lugar solo marcaremos que está colisionando con la pared del lado derecho para bloquear el movimiento en esa dirección.
1 |
if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight) |
2 |
{
|
3 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
4 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
5 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
6 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
7 |
PositionState s = new PositionState(); |
8 |
|
9 |
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) |
10 |
{
|
11 |
state.pushesRightTile = true; |
12 |
state.pushesRightSlope = true; |
13 |
return true; |
14 |
}
|
15 |
}
|
Si encajamos en el espacio, marcaremos que chocamos con la loseta inferior y compensamos la posición del objeto de manera apropiada.
1 |
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) |
2 |
{
|
3 |
state.pushesRightTile = true; |
4 |
state.pushesRightSlope = true; |
5 |
return true; |
6 |
}
|
7 |
else
|
8 |
{
|
9 |
position.y += slopeOffset; |
10 |
bottomLeft.y += slopeOffset; |
11 |
topRight.y += slopeOffset; |
12 |
state.pushesBottomTile = true; |
13 |
state.pushesBottomSlope = true; |
14 |
}
|
Manejamos el caso en el cual el objeto necesita ser compensado de manera similar.
1 |
if (slopeOffset != 0.0f) |
2 |
{
|
3 |
if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight) |
4 |
{
|
5 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
6 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
7 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
8 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
9 |
PositionState s = new PositionState(); |
10 |
|
11 |
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) |
12 |
{
|
13 |
state.pushesRightTile = true; |
14 |
state.pushesRightSlope = true; |
15 |
return true; |
16 |
}
|
17 |
else
|
18 |
{
|
19 |
position.y += slopeOffset; |
20 |
bottomLeft.y += slopeOffset; |
21 |
topRight.y += slopeOffset; |
22 |
state.pushesBottomTile = true; |
23 |
state.pushesBottomSlope = true; |
24 |
}
|
25 |
}
|
26 |
else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight) |
27 |
{
|
28 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
29 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
30 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
31 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
32 |
PositionState s = new PositionState(); |
33 |
|
34 |
if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
35 |
{
|
36 |
state.pushesRightTile = true; |
37 |
state.pushesRightSlope = true; |
38 |
return true; |
39 |
}
|
40 |
else
|
41 |
{
|
42 |
position.y += slopeOffset; |
43 |
bottomLeft.y += slopeOffset; |
44 |
topRight.y += slopeOffset; |
45 |
state.pushesTopTile = true; |
46 |
state.pushesTopSlope = true; |
47 |
}
|
48 |
}
|
49 |
}
|
Objeto en movimiento en el control de colisión
Ahora esta función desplazará el objeto hacia arriba o hacia abajo según sea necesario si queremos pisar el mosaico a la derecha, pero ¿qué ocurre si queremos usar esta función solo como una comprobación, y realmente no queremos mover el personaje? llamándolo? Para resolver este problema, agreguemos una variable adicional llamada 'mover' para marcar si la función puede mover el objeto o no.
1 |
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) |
2 |
{
|
3 |
}
|
Y mueva el objeto solo si este nuevo indicador está establecido en verdadero.
1 |
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) |
2 |
{
|
3 |
state.pushesRightTile = true; |
4 |
state.pushesRightSlope = true; |
5 |
return true; |
6 |
}
|
7 |
else if (move) |
8 |
{
|
9 |
position.y += slopeOffset; |
10 |
bottomLeft.y += slopeOffset; |
11 |
topRight.y += slopeOffset; |
12 |
state.pushesBottomTile = true; |
13 |
state.pushesBottomSlope = true; |
14 |
}
|
15 |
|
16 |
//...
|
17 |
|
18 |
if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
19 |
{
|
20 |
state.pushesRightTile = true; |
21 |
state.pushesRightSlope = true; |
22 |
return true; |
23 |
}
|
24 |
else if (move) |
25 |
{
|
26 |
position.y += slopeOffset; |
27 |
bottomLeft.y += slopeOffset; |
28 |
topRight.y += slopeOffset; |
29 |
state.pushesTopTile = true; |
30 |
state.pushesTopSlope = true; |
31 |
}
|
32 |
|
33 |
|
34 |
Controlar la inclinación de pendientes
Ahora manejemos pegados a las pendientes. Es bastante sencillo, pero necesitaremos manejar todas las casillas de las esquinas correctamente, para que el personaje se quede pegado a la pendiente sin ningún contratiempo en el camino.
Sin embargo, antes de manejar las cajas de esquina, podemos manejar fácilmente la inclinación de pendiente dentro de una sola loseta en la verificación de colisión vertical. Será suficiente si agregamos la siguiente condición en la función CollidesWithTileBottom.
1 |
if ((sf.freeUp >= 0 && sf.collidingBottom == sf.freeUp) |
2 |
|| (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight && sf.freeUp >= sf.collidingBottom)) |
3 |
{
|
4 |
state.onOneWay = isOneWay; |
5 |
state.oneWayY = bottomleftTile.y; |
6 |
state.pushesBottomTile = true; |
7 |
state.bottomTile = new Vector2i(x, bottomleftTile.y); |
8 |
position.y += sf.collidingBottom; |
9 |
topRight.y += sf.collidingBottom; |
10 |
bottomLeft.y += sf.collidingBottom; |
11 |
return true; |
12 |
} |
Esta condición lo hace de modo que si la distancia entre la posición del objeto y el terreno más cercano está entre 0 y cSlopeWallHeight, el personaje será empujado hacia abajo también, además de la condición original. Desafortunadamente, esto funciona solo dentro de un solo mosaico; la siguiente ilustración identifica el problema que debemos resolver.



El caso límite que estamos hablando es sólo esto: el personaje se mueve hacia abajo ya la izquierda de baldosas número uno al número de baldosas de dos. Baldosas número dos está vacía, por lo que necesitamos para comprobar la baldosa debajo de ella y ver si el desplazamiento desde el carácter de baldosa número 3 es adecuada para seguir caminando a lo largo de la pendiente allí.
Maneje los casos de esquina
Será más fácil manejar estos casos de esquina en las comprobaciones de colisión horizontal, así que volvamos a la función CollidesWithTileRight. Vayamos al final de la función y manejemos los casos problemáticos aquí.
En primer lugar, para manejar la inclinación de la pendiente, se necesita establecer el indicador mSticksToSlope, el objeto debe haber estado en el suelo del cuadro anterior, y el indicador de movimiento debe estar activado.
1 |
if (mSticksToSlope && state.pushedBottomTile && move) |
2 |
{
|
3 |
}
|
Ahora tenemos que encontrar la baldosa a la que debemos pegarnos. Como esta función verifica la colisión en el borde derecho del objeto, manejaremos la inclinación de la pendiente para la esquina inferior izquierda del personaje.
1 |
var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f); |
2 |
var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1; |
3 |
|
4 |
var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y)); |
5 |
var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY)); |
6 |
|
7 |
var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y)); |
8 |
var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); |
Ahora tenemos que encontrar una forma de comparar la altura en la que se encuentra el objeto actualmente al que quiere pisar. Si la siguiente altura es más baja que la actual, pero aún más alta que nuestra constante cSlopeWallHeight, empujaremos nuestro objeto hacia abajo en el suelo.
Obtener la altura de la pendiente
Volvamos a nuestra clase Slope para hacer una función que devuelva la altura de una pendiente en una posición particular.
1 |
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) |
2 |
{
|
3 |
switch (type) |
4 |
{
|
5 |
case TileCollisionType.Empty: |
6 |
return 0; |
7 |
case TileCollisionType.Full: |
8 |
case TileCollisionType.OneWayPlatform: |
9 |
return Map.cTileSize; |
10 |
}
|
11 |
}
|
Los parámetros para la función son el valor de x en la pendiente y el tipo de pendiente. Si la pendiente está vacía, podemos devolver inmediatamente 0, y si está lleno, devolveremos el tamaño del mosaico.
Podemos obtener fácilmente la altura de una pendiente mediante el uso de nuestras compensaciones en caché. Si el mosaico no se transforma de ninguna manera, obtenemos un desplazamiento para un objeto que tiene un píxel de ancho en la posición x, y su altura es igual al tamaño del mosaico.
1 |
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) |
2 |
{
|
3 |
switch (type) |
4 |
{
|
5 |
case TileCollisionType.Empty: |
6 |
return 0; |
7 |
case TileCollisionType.Full: |
8 |
case TileCollisionType.OneWayPlatform: |
9 |
return Map.cTileSize; |
10 |
}
|
11 |
|
12 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]); |
13 |
return offset.collidingBottom; |
14 |
}
|
Manejemos esto para diferentes transformaciones. Si se inclina una pendiente en el eje X, solo tenemos que duplicar el argumento x.
1 |
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) |
2 |
{
|
3 |
switch (type) |
4 |
{
|
5 |
case TileCollisionType.Empty: |
6 |
return 0; |
7 |
case TileCollisionType.Full: |
8 |
case TileCollisionType.OneWayPlatform: |
9 |
return Map.cTileSize; |
10 |
}
|
11 |
|
12 |
if (IsFlippedX(type)) |
13 |
x = Map.cTileSize - 1 - x; |
14 |
|
15 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]); |
16 |
return offset.collidingBottom; |
17 |
}
|
Si la pendiente se voltea en el eje Y, necesitamos devolver la CollidingTop en lugar de collidingBottom de la parte inferior. Como collidingTop en este caso será negativo, también tendremos que darle la vuelta al signo.
1 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]); |
2 |
return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom; |
Finalmente, si la baldosa se gira 90 grados, necesitaremos regresar collidingLeft o collidingRight. Además de eso, para obtener una compensación en caché adecuada, tendremos que cambiar las posiciones y el tamaño x e y.
1 |
if (!IsFlipped90(type)) |
2 |
{
|
3 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]); |
4 |
return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom; |
5 |
}
|
6 |
else
|
7 |
{
|
8 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][0][x][Map.cTileSize - 1][0]); |
9 |
return IsFlippedY(type) ? offset.collidingLeft : -offset.collidingRight; |
10 |
}
|
Esa es la función final.
1 |
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type) |
2 |
{
|
3 |
switch (type) |
4 |
{
|
5 |
case TileCollisionType.Empty: |
6 |
return 0; |
7 |
case TileCollisionType.Full: |
8 |
case TileCollisionType.OneWayPlatform: |
9 |
return Map.cTileSize; |
10 |
}
|
11 |
|
12 |
if (IsFlippedX(type)) |
13 |
x = Map.cTileSize - 1 - x; |
14 |
|
15 |
if (!IsFlipped90(type)) |
16 |
{
|
17 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]); |
18 |
return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom; |
19 |
}
|
20 |
else
|
21 |
{
|
22 |
var offset = new SlopeOffsetI(slopeOffsets[(int)type][0][x][Map.cTileSize - 1][0]); |
23 |
return IsFlippedY(type) ? offset.collidingLeft : -offset.collidingRight; |
24 |
}
|
25 |
}
|
Volver a los casos de esquina
Volvamos a la función CollidesWithTileRight, justo donde terminamos de determinar los tipos de pendiente para las fichas en las que se mueve el personaje.
Para usar la función que acabamos de crear, necesitamos determinar la posición en la que queremos obtener la altura de un mosaico.
1 |
var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y)); |
2 |
var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); |
3 |
|
4 |
int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); |
5 |
int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); |
Ahora calculemos la altura entre esos dos puntos.
1 |
int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType); |
2 |
int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType); |
3 |
|
4 |
var offset = slopeHeight + Map.cTileSize - nextSlopeHeight; |
Si el desplazamiento está entre 0 y la constante cSlopeWallHeight, vamos a empujar el objeto hacia abajo, pero primero debemos verificar si realmente podemos empujar el objeto hacia abajo. Esta es exactamente la misma rutina que hicimos antes.
1 |
if (offset < Constants.cSlopeWallHeight && offset > 0) |
2 |
{
|
3 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
4 |
pos.y -= offset - Mathf.Sign(offset); |
5 |
tr.y -= offset - Mathf.Sign(offset); |
6 |
bl.y -= offset - Mathf.Sign(offset); |
7 |
bl.x += 1.0f; |
8 |
tr.x += 1.0f; |
9 |
PositionState s = new PositionState(); |
10 |
|
11 |
if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
12 |
{
|
13 |
position.y -= offset; |
14 |
bottomLeft.y -= offset; |
15 |
topRight.y -= offset; |
16 |
state.pushesBottomTile = true; |
17 |
state.pushesBottomSlope = true; |
18 |
}
|
19 |
}
|
En general, la función debería verse así.
1 |
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
5 |
float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; |
6 |
TileCollisionType slopeCollisionType = TileCollisionType.Empty; |
7 |
|
8 |
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) |
9 |
{
|
10 |
var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); |
11 |
|
12 |
switch (tileCollisionType) |
13 |
{
|
14 |
default://slope |
15 |
|
16 |
Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y); |
17 |
float leftTileEdge = (tileCenter.x - Map.cTileSize / 2); |
18 |
float rightTileEdge = (leftTileEdge + Map.cTileSize); |
19 |
float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2); |
20 |
|
21 |
oldSlopeOffset = slopeOffset; |
22 |
|
23 |
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); |
24 |
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; |
25 |
|
26 |
if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)) |
27 |
{
|
28 |
state.pushesRightTile = true; |
29 |
state.rightTile = new Vector2i(topRightTile.x, y); |
30 |
return true; |
31 |
}
|
32 |
else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset)) |
33 |
{
|
34 |
slopeCollisionType = tileCollisionType; |
35 |
state.rightTile = new Vector2i(topRightTile.x, y); |
36 |
}
|
37 |
else
|
38 |
slopeOffset = oldSlopeOffset; |
39 |
|
40 |
break; |
41 |
case TileCollisionType.Empty: |
42 |
break; |
43 |
}
|
44 |
}
|
45 |
|
46 |
if (slopeOffset != 0.0f) |
47 |
{
|
48 |
if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight) |
49 |
{
|
50 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
51 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
52 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
53 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
54 |
PositionState s = new PositionState(); |
55 |
|
56 |
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) |
57 |
{
|
58 |
state.pushesRightTile = true; |
59 |
state.pushesRightSlope = true; |
60 |
return true; |
61 |
}
|
62 |
else if (move) |
63 |
{
|
64 |
position.y += slopeOffset; |
65 |
bottomLeft.y += slopeOffset; |
66 |
topRight.y += slopeOffset; |
67 |
state.pushesBottomTile = true; |
68 |
state.pushesBottomSlope = true; |
69 |
}
|
70 |
}
|
71 |
else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight) |
72 |
{
|
73 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
74 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
75 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
76 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
77 |
PositionState s = new PositionState(); |
78 |
|
79 |
if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
80 |
{
|
81 |
state.pushesRightTile = true; |
82 |
state.pushesRightSlope = true; |
83 |
return true; |
84 |
}
|
85 |
else if (move) |
86 |
{
|
87 |
position.y += slopeOffset; |
88 |
bottomLeft.y += slopeOffset; |
89 |
topRight.y += slopeOffset; |
90 |
state.pushesTopTile = true; |
91 |
state.pushesTopSlope = true; |
92 |
}
|
93 |
}
|
94 |
}
|
95 |
|
96 |
if (mSticksToSlope && state.pushedBottomTile && move) |
97 |
{
|
98 |
var nextX = mMap.GetMapTileXAtPoint(bottomLeft.x + 1.0f); |
99 |
var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1; |
100 |
|
101 |
var prevPos = mMap.GetMapTilePosition(new Vector2i(bottomLeftTile.x, bottomLeftTile.y)); |
102 |
var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY)); |
103 |
|
104 |
var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y)); |
105 |
var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); |
106 |
|
107 |
int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); |
108 |
int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); |
109 |
|
110 |
int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType); |
111 |
int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType); |
112 |
|
113 |
var offset = slopeHeight + Map.cTileSize - nextSlopeHeight; |
114 |
|
115 |
if (offset < Constants.cSlopeWallHeight && offset > 0) |
116 |
{
|
117 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
118 |
pos.y -= offset - Mathf.Sign(offset); |
119 |
tr.y -= offset - Mathf.Sign(offset); |
120 |
bl.y -= offset - Mathf.Sign(offset); |
121 |
bl.x += 1.0f; |
122 |
tr.x += 1.0f; |
123 |
PositionState s = new PositionState(); |
124 |
|
125 |
if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
126 |
{
|
127 |
position.y -= offset; |
128 |
bottomLeft.y -= offset; |
129 |
topRight.y -= offset; |
130 |
state.pushesBottomTile = true; |
131 |
state.pushesBottomSlope = true; |
132 |
}
|
133 |
}
|
134 |
}
|
135 |
|
136 |
return false; |
137 |
}
|
Ahora tenemos que hacer todo analógicamente para la función CollidesWithTileLeft. La versión final de la misma debe tomar la siguiente forma.
1 |
public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f)); |
5 |
float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; |
6 |
TileCollisionType slopeCollisionType = TileCollisionType.Empty; |
7 |
|
8 |
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) |
9 |
{
|
10 |
var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y); |
11 |
|
12 |
switch (tileCollisionType) |
13 |
{
|
14 |
default://slope |
15 |
|
16 |
Vector2 tileCenter = mMap.GetMapTilePosition(bottomLeftTile.x, y); |
17 |
float leftTileEdge = (tileCenter.x - Map.cTileSize / 2); |
18 |
float rightTileEdge = (leftTileEdge + Map.cTileSize); |
19 |
float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2); |
20 |
|
21 |
oldSlopeOffset = slopeOffset; |
22 |
|
23 |
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x - 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType); |
24 |
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown; |
25 |
|
26 |
if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)) |
27 |
{
|
28 |
state.pushesLeftTile = true; |
29 |
state.leftTile = new Vector2i(bottomLeftTile.x, y); |
30 |
return true; |
31 |
}
|
32 |
else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset)) |
33 |
{
|
34 |
slopeCollisionType = tileCollisionType; |
35 |
state.leftTile = new Vector2i(bottomLeftTile.x, y); |
36 |
}
|
37 |
else
|
38 |
slopeOffset = oldSlopeOffset; |
39 |
|
40 |
|
41 |
break; |
42 |
case TileCollisionType.Empty: |
43 |
break; |
44 |
}
|
45 |
}
|
46 |
|
47 |
if (slopeCollisionType != TileCollisionType.Empty && slopeOffset != 0) |
48 |
{
|
49 |
if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight) |
50 |
{
|
51 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
52 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
53 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
54 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
55 |
PositionState s = new PositionState(); |
56 |
|
57 |
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s)) |
58 |
{
|
59 |
state.pushesLeftTile = true; |
60 |
state.pushesLeftSlope = true; |
61 |
return true; |
62 |
}
|
63 |
else if (move) |
64 |
{
|
65 |
position.y += slopeOffset; |
66 |
bottomLeft.y += slopeOffset; |
67 |
topRight.y += slopeOffset; |
68 |
state.pushesBottomTile = true; |
69 |
state.pushesBottomSlope = true; |
70 |
}
|
71 |
}
|
72 |
else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight) |
73 |
{
|
74 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
75 |
pos.y += slopeOffset - Mathf.Sign(slopeOffset); |
76 |
tr.y += slopeOffset - Mathf.Sign(slopeOffset); |
77 |
bl.y += slopeOffset - Mathf.Sign(slopeOffset); |
78 |
PositionState s = new PositionState(); |
79 |
|
80 |
if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
81 |
{
|
82 |
state.pushesLeftTile = true; |
83 |
state.pushesLeftSlope = true; |
84 |
return true; |
85 |
}
|
86 |
else if (move) |
87 |
{
|
88 |
position.y += slopeOffset; |
89 |
bottomLeft.y += slopeOffset; |
90 |
topRight.y += slopeOffset; |
91 |
state.pushesTopTile = true; |
92 |
state.pushesTopSlope = true; |
93 |
}
|
94 |
}
|
95 |
}
|
96 |
|
97 |
if (mSticksToSlope && state.pushedBottomTile && move) |
98 |
{
|
99 |
var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f); |
100 |
var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1; |
101 |
|
102 |
var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y)); |
103 |
var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY)); |
104 |
|
105 |
var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y)); |
106 |
var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY)); |
107 |
|
108 |
int x1 = (int)Mathf.Clamp((topRight.x - 1.0f - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); |
109 |
int x2 = (int)Mathf.Clamp((topRight.x - 1.5f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f); |
110 |
|
111 |
int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType); |
112 |
int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType); |
113 |
|
114 |
var offset = slopeHeight + Map.cTileSize - nextSlopeHeight; |
115 |
|
116 |
if (offset < Constants.cSlopeWallHeight && offset > 0) |
117 |
{
|
118 |
Vector2 pos = position, tr = topRight, bl = bottomLeft; |
119 |
pos.y -= offset - Mathf.Sign(offset); |
120 |
tr.y -= offset - Mathf.Sign(offset); |
121 |
bl.y -= offset - Mathf.Sign(offset); |
122 |
bl.x -= 1.0f; |
123 |
tr.x -= 1.0f; |
124 |
PositionState s = new PositionState(); |
125 |
|
126 |
if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s)) |
127 |
{
|
128 |
position.y -= offset; |
129 |
bottomLeft.y -= offset; |
130 |
topRight.y -= offset; |
131 |
state.pushesBottomTile = true; |
132 |
state.pushesBottomSlope = true; |
133 |
}
|
134 |
}
|
135 |
}
|
136 |
|
137 |
return false; |
138 |
}
|
Eso es. El código debería ser capaz de manejar todas las formas de pendientes no traducidas.



Manejar tipos de traslación
Antes de
comenzar a manipular los mosaicos trasladables, hagamos algunas funciones
que devolverán si un TileCollisionType particular se traduce de una
manera particular. Nuestra enumeración de tipo colisión está estructurada de esta manera:
1 |
public enum TileCollisionType |
2 |
{
|
3 |
Empty = 0, //normal tiles |
4 |
Full, |
5 |
OneWayPlatform, |
6 |
|
7 |
SlopesStart, //starting point for slopes |
8 |
|
9 |
Slope45, //basic version of the slope |
10 |
Slope45FX, //slope flipped on the X axis |
11 |
Slope45FY, //slope flipped on the Y axis |
12 |
Slope45FXY, //slope flipped on the X and Y axes |
13 |
Slope45F90, //slope rotated 90 degrees |
14 |
Slope45F90X, //slope rotated and flipped on X axis |
15 |
Slope45F90Y, //slope rotated and flipped on Y axis |
16 |
Slope45F90XY, //slope rotated and flipped on both axes |
17 |
|
18 |
... |
19 |
} |
Podemos usar estos patrones para decir solo por el valor de la enumeración de cómo se traduce un tipo de colisión particular. Comencemos identificando el giro en el eje X.
1 |
public static bool IsFlippedX(TileCollisionType type) |
2 |
{
|
3 |
}
|
Primero, obtengamos la id de la pendiente. Lo haremos calculando el desplazamiento desde la primera losa de pendiente definida hasta la que queremos identificar.
1 |
public static bool IsFlippedX(TileCollisionType type) |
2 |
{
|
3 |
int typeId = (int)type - (int)TileCollisionType.SlopesStart + 1; |
4 |
}
|
Tenemos ocho tipos de traducciones, así que ahora todo lo que necesitamos es obtener el resto de dividir el tipeId por 8.
1 |
public static bool IsFlippedX(TileCollisionType type) |
2 |
{
|
3 |
int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; |
4 |
} |
Entonces ahora las traslaciones tienen un número asignado para ellos.
1 |
Slope45, //0 |
2 |
Slope45FX, //1 |
3 |
Slope45FY, //2 |
4 |
Slope45FXY, //3 |
5 |
Slope45F90, //4 |
6 |
Slope45F90X, //5 |
7 |
Slope45F90Y, //6 |
8 |
Slope45F90XY, //7 |
El giro en el eje X está presente en los tipos igual a 1, 3, 5 y 7, por lo que si es igual a uno de ellos, entonces la función debería devolver verdadero, de lo contrario devolvería falso.
1 |
public static bool IsFlippedX(TileCollisionType type) |
2 |
{
|
3 |
int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; |
4 |
|
5 |
switch (typeId) |
6 |
{
|
7 |
case 1: |
8 |
case 3: |
9 |
case 5: |
10 |
case 7: |
11 |
return true; |
12 |
}
|
13 |
|
14 |
return false; |
15 |
}
|
De la misma manera, creemos una función que indique si un tipo está volteado en el eje Y.
1 |
public static bool IsFlippedY(TileCollisionType type) |
2 |
{
|
3 |
int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; |
4 |
|
5 |
switch (typeId) |
6 |
{
|
7 |
case 2: |
8 |
case 3: |
9 |
case 6: |
10 |
case 7: |
11 |
return true; |
12 |
}
|
13 |
|
14 |
return false; |
15 |
}
|
Y finalmente, si se gira el tipo de colisión.
1 |
public static bool IsFlipped90(TileCollisionType type) |
2 |
{
|
3 |
int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8; |
4 |
|
5 |
return (typeId > 3); |
6 |
}
|
Eso es todo lo que necesitamos.
Transformar el desplazamiento
Regresemos a la clase Slopes y hagamos que nuestra función GetOffset soporte los mosaicos traducidos.
1 |
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) |
2 |
{
|
3 |
int posX, posY, sizeX, sizeY; |
4 |
|
5 |
float leftTileEdge = tileCenter.x - Map.cTileSize / 2; |
6 |
float rightTileEdge = leftTileEdge + Map.cTileSize; |
7 |
float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; |
8 |
float topTileEdge = bottomTileEdge + Map.cTileSize; |
9 |
SlopeOffsetI offset; |
10 |
|
11 |
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
12 |
sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); |
13 |
|
14 |
posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
15 |
sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); |
16 |
|
17 |
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); |
18 |
|
19 |
if (topTileEdge < topY) |
20 |
{
|
21 |
if (offset.freeDown < 0) |
22 |
offset.freeDown -= (int)(topY - topTileEdge); |
23 |
offset.collidingTop = offset.freeDown; |
24 |
}
|
25 |
if (bottomTileEdge > bottomY) |
26 |
{
|
27 |
if (offset.freeUp > 0) |
28 |
offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); |
29 |
offset.collidingBottom = offset.freeUp; |
30 |
}
|
31 |
|
32 |
return offset; |
33 |
}
|
Como es habitual, dado que no tenemos datos almacenados en caché para las pendientes traducidas, traduciremos la posición y el tamaño del objeto para que el resultado sea idéntico al del traductor traducido. Comencemos con el giro en el eje X. Todo lo que tenemos que hacer aquí es voltear el objeto a lo largo del centro del mosaico.
1 |
if (IsFlippedX(tileCollisionType)) |
2 |
{
|
3 |
posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); |
4 |
sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1); |
5 |
}
|
6 |
else
|
7 |
{
|
8 |
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
9 |
sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); |
10 |
}
|
Del mismo modo para el giro en el eje Y.
1 |
if (IsFlippedY(tileCollisionType)) |
2 |
{
|
3 |
posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); |
4 |
sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1); |
5 |
}
|
6 |
else
|
7 |
{
|
8 |
posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
9 |
sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); |
10 |
}
|
Ahora, en caso de que volteemos el mosaico en el eje y, las compensaciones que recibimos se intercambian en realidad. Vamos a traducirlos para que funcionen de la misma manera que los desplazamientos de la tesela no traducida, lo que significa que arriba está arriba y abajo.
1 |
if (IsFlippedY(tileCollisionType)) |
2 |
{
|
3 |
int tmp = offset.freeDown; |
4 |
offset.freeDown = -offset.freeUp; |
5 |
offset.freeUp = -tmp; |
6 |
tmp = offset.collidingTop; |
7 |
offset.collidingTop = -offset.collidingBottom; |
8 |
offset.collidingBottom = -tmp; |
9 |
}
|
Ahora manejemos la rotación de 90 grados.
1 |
if (!IsFlipped90(tileCollisionType)) |
2 |
{
|
3 |
if (IsFlippedX(tileCollisionType)) |
4 |
{
|
5 |
posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); |
6 |
sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1); |
7 |
} |
8 |
else |
9 |
{
|
10 |
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
11 |
sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); |
12 |
} |
13 |
|
14 |
if (IsFlippedY(tileCollisionType)) |
15 |
{
|
16 |
posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); |
17 |
sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1); |
18 |
} |
19 |
else |
20 |
{
|
21 |
posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
22 |
sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); |
23 |
} |
24 |
|
25 |
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); |
26 |
|
27 |
if (IsFlippedY(tileCollisionType)) |
28 |
{
|
29 |
int tmp = offset.freeDown; |
30 |
offset.freeDown = -offset.freeUp; |
31 |
offset.freeUp = -tmp; |
32 |
tmp = offset.collidingTop; |
33 |
offset.collidingTop = -offset.collidingBottom; |
34 |
offset.collidingBottom = -tmp; |
35 |
} |
36 |
} |
37 |
else |
38 |
{
|
39 |
} |
Aquí todo debe girarse 90 grados, por lo que en lugar de basar nuestra posX y sizeX en los bordes izquierdo y derecho del objeto, los basaremos en la parte superior e inferior.
1 |
if (IsFlippedY(tileCollisionType)) |
2 |
{
|
3 |
posX = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
4 |
sizeX = (int)Mathf.Clamp(topY - (bottomTileEdge + posX), 0.0f, Map.cTileSize - 1); |
5 |
}
|
6 |
else
|
7 |
{
|
8 |
posX = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); |
9 |
sizeX = (int)Mathf.Clamp((topTileEdge - posX) - bottomY, 0.0f, Map.cTileSize - 1); |
10 |
}
|
11 |
|
12 |
if (IsFlippedX(tileCollisionType)) |
13 |
{
|
14 |
posY = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); |
15 |
sizeY = (int)Mathf.Clamp((rightTileEdge - posY) - leftX, 0.0f, Map.cTileSize - 1); |
16 |
}
|
17 |
else
|
18 |
{
|
19 |
posY = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
20 |
sizeY = (int)Mathf.Clamp(rightX - (leftTileEdge + posY), 0.0f, Map.cTileSize - 1); |
21 |
}
|
Ahora tenemos que hacer algo similar a lo que hicimos anteriormente si el mosaico se volteó en el eje Y, pero esta vez tenemos que hacerlo tanto para la rotación de 90 grados como para el giro Y.
1 |
if (IsFlippedY(tileCollisionType)) |
2 |
{
|
3 |
offset.collidingBottom = offset.collidingLeft; |
4 |
offset.freeDown = offset.freeLeft; |
5 |
offset.collidingTop = offset.collidingRight; |
6 |
offset.freeUp = offset.freeRight; |
7 |
}
|
8 |
else
|
9 |
{
|
10 |
offset.collidingBottom = -offset.collidingRight; |
11 |
offset.freeDown = -offset.freeRight; |
12 |
offset.collidingTop = -offset.collidingLeft; |
13 |
offset.freeUp = -offset.freeLeft; |
14 |
}
|
Eso es todo. Dado que nuestros desplazamientos finales hacia arriba y hacia abajo se ajustan para que tengan sentido en el espacio mundial, nuestros ajustes fuera de los límites de las teselas siguen funcionando correctamente.
1 |
public static SlopeOffsetI GetOffsetHeight(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) |
2 |
{
|
3 |
int posX, posY, sizeX, sizeY; |
4 |
|
5 |
float leftTileEdge = tileCenter.x - Map.cTileSize / 2; |
6 |
float rightTileEdge = leftTileEdge + Map.cTileSize; |
7 |
float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; |
8 |
float topTileEdge = bottomTileEdge + Map.cTileSize; |
9 |
SlopeOffsetI offset; |
10 |
|
11 |
if (!IsFlipped90(tileCollisionType)) |
12 |
{
|
13 |
if (IsFlippedX(tileCollisionType)) |
14 |
{
|
15 |
posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); |
16 |
sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1); |
17 |
}
|
18 |
else
|
19 |
{
|
20 |
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
21 |
sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); |
22 |
}
|
23 |
|
24 |
if (IsFlippedY(tileCollisionType)) |
25 |
{
|
26 |
posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); |
27 |
sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1); |
28 |
}
|
29 |
else
|
30 |
{
|
31 |
posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
32 |
sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); |
33 |
}
|
34 |
|
35 |
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); |
36 |
|
37 |
if (IsFlippedY(tileCollisionType)) |
38 |
{
|
39 |
int tmp = offset.freeDown; |
40 |
offset.freeDown = -offset.freeUp; |
41 |
offset.freeUp = -tmp; |
42 |
tmp = offset.collidingTop; |
43 |
offset.collidingTop = -offset.collidingBottom; |
44 |
offset.collidingBottom = -tmp; |
45 |
}
|
46 |
}
|
47 |
else
|
48 |
{
|
49 |
if (IsFlippedY(tileCollisionType)) |
50 |
{
|
51 |
posX = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
52 |
sizeX = (int)Mathf.Clamp(topY - (bottomTileEdge + posX), 0.0f, Map.cTileSize - 1); |
53 |
}
|
54 |
else
|
55 |
{
|
56 |
posX = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1); |
57 |
sizeX = (int)Mathf.Clamp((topTileEdge - posX) - bottomY, 0.0f, Map.cTileSize - 1); |
58 |
}
|
59 |
|
60 |
if (IsFlippedX(tileCollisionType)) |
61 |
{
|
62 |
posY = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1); |
63 |
sizeY = (int)Mathf.Clamp((rightTileEdge - posY) - leftX, 0.0f, Map.cTileSize - 1); |
64 |
}
|
65 |
else
|
66 |
{
|
67 |
posY = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
68 |
sizeY = (int)Mathf.Clamp(rightX - (leftTileEdge + posY), 0.0f, Map.cTileSize - 1); |
69 |
}
|
70 |
|
71 |
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); |
72 |
|
73 |
if (IsFlippedY(tileCollisionType)) |
74 |
{
|
75 |
offset.collidingBottom = offset.collidingLeft; |
76 |
offset.freeDown = offset.freeLeft; |
77 |
offset.collidingTop = offset.collidingRight; |
78 |
offset.freeUp = offset.freeRight; |
79 |
}
|
80 |
else
|
81 |
{
|
82 |
offset.collidingBottom = -offset.collidingRight; |
83 |
offset.freeDown = -offset.freeRight; |
84 |
offset.collidingTop = -offset.collidingLeft; |
85 |
offset.freeUp = -offset.freeLeft; |
86 |
}
|
87 |
}
|
88 |
|
89 |
if (topTileEdge < topY) |
90 |
{
|
91 |
if (offset.freeDown < 0) |
92 |
offset.freeDown -= (int)(topY - topTileEdge); |
93 |
offset.collidingTop = offset.freeDown; |
94 |
}
|
95 |
if (bottomTileEdge > bottomY) |
96 |
{
|
97 |
if (offset.freeUp > 0) |
98 |
offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); |
99 |
offset.collidingBottom = offset.freeUp; |
100 |
}
|
101 |
|
102 |
return offset; |
103 |
}
|
Eso es todo; ahora también podemos usar pendientes trasladadas.



En la animación anterior, hay pendientes de 45, 22, 15 y 11 grados. Gracias a las rotaciones de 90 grados, también podemos obtener pendientes de 79, 75 y 68 grados sin definir losetas de pendiente adicionales. También puede
ver que la pendiente de 79 grados es demasiado empinada para avanzar sin
problemas con nuestro valor de cSlopeWallHeight.
Maneje plataformas de una vía
En todo este problema, hemos roto nuestro soporte para plataformas de una vía. Necesitamos arreglar eso, y extender la funcionalidad a las pendientes también. Las plataformas unidireccionales son tan importantes o incluso más importantes que las fichas sólidas, por lo que no podemos permitirnos perderlas.
Agregue los tipos de One-Way
Lo primero que debemos hacer es agregar nuevos tipos de colisión para plataformas de una vía. Los agregaremos más allá de los tipos de colisión no unidireccional y también marcaremos dónde comienzan, por lo que más adelante nos resultará fácil determinar si un tipo de colisión en particular es unidireccional o no.
1 |
public enum TileCollisionType |
2 |
{
|
3 |
Empty = 0, |
4 |
Full, |
5 |
|
6 |
SlopesStart, |
7 |
|
8 |
...
|
9 |
|
10 |
Slope45, |
11 |
Slope45FX, |
12 |
Slope45FY, |
13 |
Slope45FXY, |
14 |
Slope45F90, |
15 |
Slope45F90X, |
16 |
Slope45F90Y, |
17 |
Slope45F90XY, |
18 |
|
19 |
//...
|
20 |
|
21 |
OneWayStart, |
22 |
|
23 |
OneWaySlope45, |
24 |
OneWaySlope45FX, |
25 |
OneWaySlope45FY, |
26 |
OneWaySlope45FXY, |
27 |
OneWaySlope45F90, |
28 |
OneWaySlope45F90X, |
29 |
OneWaySlope45F90Y, |
30 |
OneWaySlope45F90XY, |
31 |
|
32 |
//...
|
33 |
|
34 |
SlopeEnd = OneWaySlopeMid4RevF90XY, |
35 |
|
36 |
OneWayFull, |
37 |
|
38 |
OneWayEnd, |
39 |
|
40 |
Count, |
41 |
}
|
Ahora todas las plataformas de una vía se encuentran entre las encripciones OneWayStart y OneWayEnd, por lo que podemos crear fácilmente una función que devuelva esta información.
1 |
public static bool IsOneWay(TileCollisionType type) |
2 |
{
|
3 |
return ((int)type > (int)TileCollisionType.OneWayStart && (int)type < (int)TileCollisionType.OneWayEnd); |
4 |
}
|
Las variantes unidireccionales de las pendientes deben apuntar a los mismos datos que las plataformas que no son de una sola vía, por lo que no se preocupe por ampliar aún más los requisitos de memoria.
1 |
case TileCollisionType.Slope45: |
2 |
slopesHeights[i] = slope45; |
3 |
slopesExtended[i] = Extend(slopesHeights[i]); |
4 |
posByHeightCaches[i] = CachePosByHeight(slopesHeights[i]); |
5 |
slopeHeightByPosAndSizeCaches[i] = CacheSlopeHeightByPosAndLength(slopesHeights[i]); |
6 |
slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]); |
7 |
break; |
8 |
case TileCollisionType.Slope45FX: |
9 |
case TileCollisionType.Slope45FY: |
10 |
case TileCollisionType.Slope45FXY: |
11 |
case TileCollisionType.Slope45F90: |
12 |
case TileCollisionType.Slope45F90X: |
13 |
case TileCollisionType.Slope45F90XY: |
14 |
case TileCollisionType.Slope45F90Y: |
15 |
case TileCollisionType.OneWaySlope45: |
16 |
case TileCollisionType.OneWaySlope45FX: |
17 |
case TileCollisionType.OneWaySlope45FY: |
18 |
case TileCollisionType.OneWaySlope45FXY: |
19 |
case TileCollisionType.OneWaySlope45F90: |
20 |
case TileCollisionType.OneWaySlope45F90X: |
21 |
case TileCollisionType.OneWaySlope45F90XY: |
22 |
case TileCollisionType.OneWaySlope45F90Y: |
23 |
slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; |
24 |
slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; |
25 |
posByHeightCaches[i] = posByHeightCaches[(int)TileCollisionType.Slope45]; |
26 |
slopeHeightByPosAndSizeCaches[i] = slopeHeightByPosAndSizeCaches[(int)TileCollisionType.Slope45]; |
27 |
slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45]; |
28 |
break; |
Cubra los Datos Adicionales
Ahora agreguemos variables que nos permitan hacer que un objeto ignore las plataformas unidireccionales. Una será una bandera de objeto, que básicamente será para establecer ignorar permanentemente las plataformas de una sola vía; esto será útil para volar monstruos y otros objetos que no necesitan utilizar las plataformas, y otra bandera para desactivar temporalmente la colisión con plataformas de una vía, solo por el hecho de caer a través de ellas.
La primera variable estará dentro de la clase MovingObject.
1 |
public bool mIgnoresOneWay = false; |
2 |
public bool mOnOneWayPlatform = false; |
3 |
public bool mSticksToSlope = true; |
4 |
public bool mIsKinematic = false; |
El segundo está dentro de la estructura PositionState.
1 |
public bool onOneWay; |
2 |
public bool tmpIgnoresOneWay; |
También agregaremos otra variable aquí que mantendrá la coordenada Y de la plataforma que queremos omitir.
1 |
public bool onOneWay; |
2 |
public bool tmpIgnoresOneWay; |
3 |
public int oneWayY; |
Para hacer que las plataformas de una vía funcionen, simplemente ignoraremos una sola capa horizontal de plataformas. Cuando ingresamos a otra capa, es decir, la posición Y de nuestro personaje ha cambiado en las coordenadas del mapa, luego configuramos el personaje para colisionar nuevamente con las plataformas de una vía.
Modificar los controles de colisión
Vayamos a nuestra función CollidesWithTileBottom. Antes que nada, mientras iteramos a través de las teselas, veamos si es una plataforma de un solo sentido, y si es así, si debemos considerar colisionar con este mosaico o no.
1 |
public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); |
5 |
bool isOneWay; |
6 |
|
7 |
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) |
8 |
{
|
9 |
var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y); |
10 |
|
11 |
isOneWay = Slopes.IsOneWay(tileCollisionType); |
12 |
|
13 |
if ((mIgnoresOneWay || state.tmpIgnoresOneWay) && isOneWay) |
14 |
continue; |
Deberíamos colisionar con plataformas unidireccionales solo si la distancia hasta la parte superior de la plataforma es menor que cSlopeWallHeightConstant, por lo que podemos llegar a la cima. Agreguemos esto a la condición ya establecida, y también debemos asignar los valores adecuados a state.onOneWay y state.oneWayY.
1 |
if (((sf.freeUp >= 0 && sf.collidingBottom == sf.freeUp) |
2 |
|| (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight && sf.freeUp >= sf.collidingBottom)) |
3 |
&& !(isOneWay && Mathf.Abs(sf.collidingBottom) >= Constants.cSlopeWallHeight)) |
4 |
{
|
5 |
state.onOneWay = isOneWay; |
6 |
state.oneWayY = bottomleftTile.y; |
7 |
state.pushesBottomTile = true; |
8 |
state.bottomTile = new Vector2i(x, bottomleftTile.y); |
9 |
position.y += sf.collidingBottom; |
10 |
topRight.y += sf.collidingBottom; |
11 |
bottomLeft.y += sf.collidingBottom; |
12 |
return true; |
13 |
}
|
Para la función CollidesWithTileTop, simplemente ignoramos las plataformas unidireccionales.
1 |
public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); |
4 |
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
5 |
|
6 |
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) |
7 |
{
|
8 |
var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); |
9 |
|
10 |
if (Slopes.IsOneWay(tileCollisionType)) |
11 |
continue; |
Para la verificación de colisión horizontal, habrá un poco más de trabajo. En primer lugar, creemos dos booleanos adicionales al principio, que servirán como información sobre si el mosaico actualmente procesado es unidireccional, y si el mosaico de la iteración anterior ha sido una plataforma unidireccional.
1 |
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
5 |
float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; |
6 |
bool wasOneWay = false, isOneWay; |
7 |
TileCollisionType slopeCollisionType = TileCollisionType.Empty; |
Ahora estamos interesados en iterar a través de una plataforma unidireccional si nos movemos a lo largo de ella. Realmente no podemos colisionar con plataformas de una vía desde la derecha o la izquierda, pero si el personaje se mueve a lo largo de una pendiente que también es una plataforma de una vía, entonces debe manejarse de la misma manera que lo haría una pendiente normal.
1 |
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
5 |
float slopeOffset = 0.0f, oldSlopeOffset = 0.0f; |
6 |
bool wasOneWay = false, isOneWay; |
7 |
TileCollisionType slopeCollisionType = TileCollisionType.Empty; |
8 |
|
9 |
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) |
10 |
{
|
11 |
var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); |
12 |
isOneWay = Slopes.IsOneWay(tileCollisionType); |
13 |
|
14 |
if (isOneWay && (!move || mIgnoresOneWay || state.tmpIgnoresOneWay || y != bottomLeftTile.y)) |
15 |
continue; |
Ahora asegúrate de que no podamos colisionar con una pendiente como si fuera una pared.
1 |
if (!isOneWay && (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile))) |
2 |
{
|
3 |
state.pushesRightTile = true; |
4 |
state.rightTile = new Vector2i(topRightTile.x, y); |
5 |
return true; |
6 |
}
|
Y si ese no es el caso y el desplazamiento es lo suficientemente pequeño como para escalarlo, entonces recuerde que ahora nos estamos moviendo a lo largo de una plataforma unidireccional.
1 |
if (!isOneWay && (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile))) |
2 |
{
|
3 |
state.pushesRightTile = true; |
4 |
state.rightTile = new Vector2i(topRightTile.x, y); |
5 |
return true; |
6 |
}
|
7 |
else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset)) |
8 |
{
|
9 |
wasOneWay = isOneWay; |
10 |
slopeCollisionType = tileCollisionType; |
11 |
state.rightTile = new Vector2i(topRightTile.x, y); |
12 |
}
|
Ahora lo que
queda aquí es asegurarnos de que cada vez que cambiemos el estado de la
posición también necesitemos actualizar la variable onOneWay.
1 |
state.onOneWay = wasOneWay; |
Saltando hacia abajo
Tenemos que dejar de ignorar las plataformas de dirección única una vez que cambiemos la posición Y en las coordenadas del mapa. Vamos a configurar nuestra condición después del movimiento en el eje Y en la función Mover. Necesitamos agregarlo al final del segundo caso.
1 |
else if (move.y != 0.0f && move.x == 0.0f) |
2 |
{
|
3 |
MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); |
4 |
|
5 |
if (step.y > 0.0f) |
6 |
state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); |
7 |
else
|
8 |
state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); |
9 |
|
10 |
if (!mIgnoresOneWay && state.tmpIgnoresOneWay && mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY) |
11 |
state.tmpIgnoresOneWay = false; |
12 |
}
|
Y también al final del tercer caso.
1 |
else
|
2 |
{
|
3 |
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); |
4 |
float vertAccum = 0.0f; |
5 |
|
6 |
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) |
7 |
{
|
8 |
vertAccum += Mathf.Sign(step.y) * speedRatio; |
9 |
|
10 |
MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); |
11 |
move.x -= step.x; |
12 |
|
13 |
while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f)) |
14 |
{
|
15 |
move.y -= step.y; |
16 |
vertAccum -= step.y; |
17 |
|
18 |
MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); |
19 |
}
|
20 |
}
|
21 |
|
22 |
if (step.x > 0.0f) |
23 |
state.pushesLeftTile = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); |
24 |
else
|
25 |
state.pushesRightTile = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); |
26 |
|
27 |
if (step.y > 0.0f) |
28 |
state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); |
29 |
else
|
30 |
state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); |
31 |
|
32 |
if (!mIgnoresOneWay && state.tmpIgnoresOneWay && mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY) |
33 |
state.tmpIgnoresOneWay = false; |
34 |
}
|
Deberias hacer eso. Ahora, lo único que debemos hacer para que un personaje caiga desde una plataforma unidireccional es establecer su tmpIgnoresOneWay en verdadero.
1 |
if (KeyState(KeyInput.GoDown)) |
2 |
mPS.tmpIgnoresOneWay = true; |
Veamos cómo se ve esto en acción.



Resumen
Vaya, eso fue mucho trabajo, pero valió la pena. El resultado es muy flexible y robusto. Podemos definir cualquier tipo de pendiente gracias a nuestro manejo de mapas de bits de colisión, traducir los mosaicos y convertirlos en plataformas de una vía.
Esta implementación aún no está optimizada, y estoy seguro de haber perdido muchas oportunidades para nuestro nuevo método de integración de un píxel. También estoy bastante seguro de que se podrían omitir muchos controles de colisión adicionales, así que si mejoras esta implementación, ¡házmelo saber en la sección de comentarios!
Gracias por quedarte conmigo hasta ahora, y espero que este tutorial te sea útil.



