¿Cómo podría restringir el movimiento del jugador a la superficie de un objeto 3D usando Unity?

16

Estoy tratando de crear un efecto similar al de Mario Galaxy o Geometry Wars 3 donde el jugador camina alrededor del "planeta", la gravedad parece ajustarse y no se caen del borde del objeto como lo harían si la gravedad fue arreglado en una sola dirección.

ingrese la descripción de la imagen aquí
(fuente: gameskinny.com )

Geometry Wars 3

Logré implementar algo cercano a lo que estoy buscando usando un enfoque donde el objeto que debería tener la gravedad atrae otros cuerpos rígidos hacia él, pero al usar el motor de física incorporado para Unity, aplicando movimiento con AddForce y similares, Simplemente no pude hacer que el movimiento se sintiera bien. No pude lograr que el jugador se moviera lo suficientemente rápido sin que el jugador comenzara a volar fuera de la superficie del objeto y no pude encontrar un buen equilibrio de fuerza aplicada y gravedad para acomodar esto. Mi implementación actual es una adaptación de lo que se encontró aquí

Siento que la solución probablemente aún usaría física para hacer que el jugador se conecte a tierra sobre el objeto si salieran de la superficie, pero una vez que el jugador haya sido conectado a tierra, habría una manera de encajar al jugador en la superficie y apagar la física y controlar al jugador a través de otros medios, pero realmente no estoy seguro.

¿Qué tipo de enfoque debo tomar para encajar al jugador en la superficie de los objetos? Tenga en cuenta que la solución debería funcionar en el espacio 3D (a diferencia de 2D) y debería poder implementarse utilizando la versión gratuita de Unity.

SpartanDonut
fuente
Ni siquiera pensé en buscar caminar en las paredes. Echaré un vistazo y veré si esto ayuda.
SpartanDonut
1
Si esta pregunta se refiere específicamente a hacer esto en 3D en Unity, debería aclararse con las ediciones. (No sería un duplicado exacto de ese existente entonces.)
Anko
Ese es mi sentimiento general también: voy a ver si puedo adaptar esa solución a 3D y publicarla como respuesta (o si alguien más puede ganarme el golpe, también estoy bien con eso). Intentaré actualizar mi pregunta para que quede más claro al respecto.
SpartanDonut
Potencialmente útil: youtube.com/watch?v=JMWnufriQx4
ssb

Respuestas:

7

Logré lograr lo que necesitaba, principalmente con la ayuda de esta publicación blog para la pieza de la superficie del rompecabezas y se me ocurrieron mis propias ideas para el movimiento del jugador y la cámara.

Ajustar jugador a la superficie de un objeto

La configuración básica consiste en una esfera grande (el mundo) y una esfera más pequeña (el jugador), ambas con colisionadores de esferas unidas a ellas.

La mayor parte del trabajo realizado se realizó en los siguientes dos métodos:

private void UpdatePlayerTransform(Vector3 movementDirection)
{                
    RaycastHit hitInfo;

    if (GetRaycastDownAtNewPosition(movementDirection, out hitInfo))
    {
        Quaternion targetRotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
        Quaternion finalRotation = Quaternion.RotateTowards(transform.rotation, targetRotation, float.PositiveInfinity);

        transform.rotation = finalRotation;
        transform.position = hitInfo.point + hitInfo.normal * .5f;
    }
}

private bool GetRaycastDownAtNewPosition(Vector3 movementDirection, out RaycastHit hitInfo)
{
    Vector3 newPosition = transform.position;
    Ray ray = new Ray(transform.position + movementDirection * Speed, -transform.up);        

    if (Physics.Raycast(ray, out hitInfo, float.PositiveInfinity, WorldLayerMask))
    {
        return true;
    }

    return false;
}

los Vector3 movementDirection parámetro es tal como suena, la dirección en la que vamos a mover a nuestro jugador en este cuadro, y calcular ese vector, aunque terminó siendo relativamente simple en este ejemplo, fue un poco difícil de entender al principio. Más sobre eso más adelante, pero solo tenga en cuenta que es un vector normalizado en la dirección en que el jugador mueve este cuadro.

Al pasar, lo primero que hacemos es verificar si un rayo, que se origina en la hipotética posición futura dirigida hacia el vector hacia abajo del jugador (-transform.up), llega al mundo usando WorldLayerMask, que es una propiedad pública de LayerMask del script. Si desea colisiones más complejas o varias capas, tendrá que construir su propia máscara de capa. Si la emisión de rayos impacta con éxito en algo, hitInfo se utiliza para recuperar el punto normal y el punto de golpe para calcular la nueva posición y rotación del jugador que debe estar justo en el objeto. La compensación de la posición del jugador puede ser necesaria según el tamaño y el origen del objeto del jugador en cuestión.

Finalmente, esto realmente solo se ha probado y probablemente solo funcione bien en objetos simples como esferas. Como sugiere la publicación del blog en la que basé mi solución, es probable que desee realizar múltiples transmisiones de rayos y promediarlas para su posición y rotación para obtener una transición mucho más agradable al moverse sobre terrenos más complejos. También puede haber otras trampas que no he pensado en este momento.

Cámara y movimiento

Una vez que el jugador se pegaba a la superficie del objeto, la siguiente tarea a abordar era el movimiento. Originalmente había comenzado con el movimiento relativo al jugador, pero comencé a encontrar problemas en los polos de la esfera donde las direcciones cambiaron repentinamente, lo que hizo que mi jugador cambiara rápidamente de dirección una y otra vez sin dejarme pasar nunca por los polos. Lo que terminé haciendo fue hacer que mis jugadores se movieran en relación con la cámara.

Lo que funcionó bien para mis necesidades fue tener una cámara que siguiera estrictamente al jugador basándose únicamente en la posición del jugador. Como resultado, a pesar de que la cámara estaba rotando técnicamente, presionar hacia arriba siempre movía al jugador hacia la parte superior de la pantalla, hacia abajo, hacia abajo, y así sucesivamente con la izquierda y la derecha.

Para hacer esto, se ejecutó lo siguiente en la cámara donde el objeto objetivo era el jugador:

private void FixedUpdate()
{
    // Calculate and set camera position
    Vector3 desiredPosition = this.target.TransformPoint(0, this.height, -this.distance);
    this.transform.position = Vector3.Lerp(this.transform.position, desiredPosition, Time.deltaTime * this.damping);

    // Calculate and set camera rotation
    Quaternion desiredRotation = Quaternion.LookRotation(this.target.position - this.transform.position, this.target.up);
    this.transform.rotation = Quaternion.Slerp(this.transform.rotation, desiredRotation, Time.deltaTime * this.rotationDamping);
}

Finalmente, para mover el reproductor, aprovechamos la transformación de la cámara principal para que con nuestros controles hacia arriba, hacia abajo, hacia abajo, etc. Y aquí es donde llamamos UpdatePlayerTransform, que hará que nuestra posición se ajuste al objeto del mundo.

void Update () 
{        
    Vector3 movementDirection = Vector3.zero;
    if (Input.GetAxisRaw("Vertical") > 0)
    {
        movementDirection += cameraTransform.up;
    }
    else if (Input.GetAxisRaw("Vertical") < 0)
    {
        movementDirection += -cameraTransform.up;
    }

    if (Input.GetAxisRaw("Horizontal") > 0)
    {
        movementDirection += cameraTransform.right;
    }
    else if (Input.GetAxisRaw("Horizontal") < 0)
    {
        movementDirection += -cameraTransform.right;
    }

    movementDirection.Normalize();

    UpdatePlayerTransform(movementDirection);
}

Para implementar una cámara más interesante, pero los controles deben ser casi los mismos que los que tenemos aquí, puede implementar fácilmente una cámara que no esté renderizada o simplemente otro objeto ficticio para basar el movimiento y luego usar la cámara más interesante para representar lo que quieres que se vea el juego. Esto permitirá transiciones agradables de la cámara a medida que recorres objetos sin romper los controles.

SpartanDonut
fuente
3

Creo que esta idea funcionará:

Mantenga una copia del lado de la CPU de la malla del planeta. Tener vértices de malla también significa que tienes vectores normales para cada punto del planeta. Luego, desactive completamente la gravedad para todas las entidades, en lugar de aplicar una fuerza en la dirección exactamente opuesta de un vector normal.

Ahora, ¿en base a qué punto debe calcularse ese vector normal del planeta?

La respuesta más fácil (que estoy bastante seguro de que funcionará bien) es aproximarse de manera similar al método de Newton : cuando los objetos aparecen por primera vez, conoces todas sus posiciones iniciales en el planeta. Use esa posición inicial para determinar el upvector de cada objeto . Obviamente la gravedad estará en la dirección opuesta (hacia down). En el siguiente cuadro, antes de aplicar la gravedad, proyecta un rayo desde la nueva posición del objeto hacia su viejo downvector. Use la intersección de ese rayo con el planeta como la nueva referencia para determinar el upvector. El raro caso de que el rayo no golpee nada significa que algo salió terriblemente mal y debe mover su objeto de nuevo a donde estaba en el cuadro anterior.

También tenga en cuenta que al usar este método, cuanto más origen del jugador sea del planeta, peor será la aproximación. Por lo tanto, es mejor usar en algún lugar alrededor de los pies de cada jugador como su origen. Supongo, pero creo que usar los pies como origen también resultará en un manejo y navegación más fácil del jugador.


Una última nota: para obtener mejores resultados, incluso puede hacer lo siguiente: realizar un seguimiento del movimiento del jugador en cada cuadro (por ejemplo, usando current_position - last_position). Luego sujete eso movement_vectorpara que su longitud hacia el objeto upsea ​​cero. Llamemos a este nuevo vector reference_movement. Mueva el anterior reference_pointpor reference_movementy use este nuevo punto como origen de trazado de rayos. Después (y si) el rayo golpea el planeta, muévase reference_pointa ese punto de golpe. Finalmente, calcule el nuevo upvector, a partir de este nuevo reference_point.

Algunos pseudocódigo para resumirlo:

update_up_vector(player:object, planet:mesh)
{
    up_vector        = normalize(planet.up);
    reference_point  = ray_trace (object.old_position, -up_vector, planet);
    movement         = object.position - object.old_position;
    movement_clamped = movement - up_vector * dot (movement, up_vector);

    reference_point  += movement_clamped;
    reference_point  = ray_trace (reference_point, -up_vector, planet);
    player.up        = normal_vector(mesh, reference_point);
}
Ali1S232
fuente
2

Esta publicación podría ser útil. Su esencia es que no usas los controladores de personajes, sino que los tuyos con el motor de física. Luego, usa las normales detectadas debajo del jugador para orientarlas hacia la superficie de la malla.

Aquí hay una buena descripción de la técnica. Hay muchos más recursos con términos de búsqueda en la web como "unidad caminar en objetos 3d mario galaxy".

También hay un proyecto de demostración de unity 3.x que tenía un motor para caminar, con un soldado y un perro y en una de las escenas. Demostró caminar sobre objetos 3d al estilo Galaxy . Runevision lo llama sistema de locomoción.

SpidermanSpidermanDWASC
fuente
1
A primera vista, la demostración no funciona muy bien y el paquete Unity tiene errores de compilación. Veré si puedo hacer que esto funcione, pero agradecería una respuesta más completa.
SpartanDonut
2

Rotación

Verifique la respuesta a esta pregunta en answers.unity3d.com (en realidad la pregunté yo mismo). Citar:

La primera tarea es obtener un vector que defina. Desde su dibujo, puede hacerlo de una de dos maneras. Puede tratar el planeta como una esfera y usar (object.position - planet.position). La segunda forma es usar Collider.Raycast () y usar el 'hit.normal' que devuelve.

Aquí está el código que me sugirió:

var up : Vector3 = transform.position - planet.position;
transform.rotation = Quaternion.FromToRotation(transform.up, up) * transform.rotation;

Llame a eso cada actualización, y debería hacerlo funcionar. (tenga en cuenta que el código está en UnityScript ).
El mismo código en C # :

Vector3 up = transform.position - planet.position;
transform.rotation = Quaternion.FromToRotation(transform.up, up) * transform.rotation;

Gravedad

Para la gravedad, puede usar mi "técnica de física de planetas", que describí en mi pregunta, pero no está realmente optimizada. Era solo algo que tenía en mente.
Le sugiero que cree su propio sistema para la gravedad.

Aquí hay un tutorial de Youtube por Sebastian Lague . Esa es una solución que funciona muy bien.

EDITAR: en la unidad, vaya a Edición > Configuración del proyecto > Física y establezca todos los valores de Gravedad en 0 (o elimine el Cuerpo rígido de todos los objetos), para evitar que la gravedad incorporada (que simplemente tira al jugador hacia abajo) entre en conflicto con La solución personalizada. (apágalo)

Daniel Kvist
fuente
Gravitar al jugador hacia el centro de un planeta (y rotarlos en consecuencia) funciona bien para planetas casi esféricos, pero me imagino que va realmente mal para objetos menos esféricos, como la forma de cohete que Mario está saltando en el primer GIF.
Anko
Podría crear un cubo que esté restringido a moverse solo alrededor del eje X, por ejemplo, dentro del objeto no esférico, y moverlo cuando el jugador se mueva, de modo que siempre esté recto debajo del jugador, y luego tirar del jugador hasta el cubo Intentalo.
Daniel Kvist