Comment pourrais-je contraindre le mouvement du joueur à la surface d'un objet 3D en utilisant Unity?

16

J'essaie de créer un effet similaire à celui de Mario Galaxy ou de Geometry Wars 3 où lorsque le joueur contourne la "planète", la gravité semble s'ajuster et ils ne tombent pas du bord de l'objet comme ils le feraient si la gravité a été fixé dans une seule direction.

entrez la description de l'image ici
(source: gameskinny.com )

Geometry Wars 3

J'ai réussi à implémenter quelque chose de proche de ce que je recherche en utilisant une approche où l'objet qui devrait avoir la gravité attire d'autres corps rigides vers lui, mais en utilisant le moteur physique intégré pour Unity, en appliquant le mouvement avec AddForce et autres, Je n'arrivais pas à faire en sorte que le mouvement se sente bien. Je ne pouvais pas faire bouger le joueur assez rapidement sans que le joueur ne commence à voler de la surface de l'objet et je ne pouvais pas trouver un bon équilibre de force et de gravité appliquées pour s'adapter à cela. Mon implémentation actuelle est une adaptation de ce qui a été trouvé ici

J'ai l'impression que la solution utiliserait probablement encore la physique pour mettre le joueur à la terre sur l'objet s'il devait quitter la surface, mais une fois que le joueur a été mis à la terre, il y aurait un moyen de mettre le joueur à la surface et de désactiver la physique et contrôler le joueur par d'autres moyens mais je ne suis vraiment pas sûr.

Quel type d'approche dois-je adopter pour accrocher le lecteur à la surface d'objets? Notez que la solution devrait fonctionner dans l'espace 3D (par opposition à 2D) et devrait pouvoir être implémentée à l'aide de la version gratuite d'Unity.

SpartanDonut
la source
Je n'ai même pas pensé à chercher à marcher sur les murs. Je vais jeter un oeil et voir si ces aides.
SpartanDonut
1
Si cette question concerne spécifiquement cette opération en 3D dans Unity, elle devrait être clarifiée avec les modifications. (Ce ne serait pas un double exact de celui existant alors.)
Anko
C'est aussi mon sentiment général - je vais voir si je peux adapter cette solution à la 3D et la poster comme réponse (ou si quelqu'un d'autre peut me battre pour le coup, ça me va aussi). Je vais essayer de mettre à jour ma question pour être plus clair à ce sujet.
SpartanDonut
Potentiellement utile: youtube.com/watch?v=JMWnufriQx4
ssb

Réponses:

7

J'ai réussi à accomplir ce dont j'avais besoin, principalement avec l'aide de ce billet de blog pour la pièce du puzzle en surface et j'ai trouvé mes propres idées pour le mouvement des joueurs et la caméra.

Accrochage du lecteur à la surface d'un objet

La configuration de base se compose d'une grande sphère (le monde) et d'une sphère plus petite (le joueur), toutes deux avec des collisionneurs de sphères qui leur sont attachés.

La majeure partie du travail effectué se faisait selon les deux méthodes suivantes:

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;
}

le Vector3 movementDirection paramètre est juste tel qu'il semble, la direction dans laquelle nous allons déplacer notre lecteur dans cette image, et le calcul de ce vecteur, bien que relativement simple dans cet exemple, était un peu difficile à comprendre au début. Plus à ce sujet plus tard, mais gardez à l'esprit qu'il s'agit d'un vecteur normalisé dans la direction dans laquelle le joueur déplace cette image.

En progressant, la première chose que nous faisons est de vérifier si un rayon, provenant de la position future hypothétique dirigée vers le vecteur des joueurs vers le bas (-transform.up), frappe le monde en utilisant WorldLayerMask qui est une propriété LayerMask publique du script. Si vous souhaitez des collisions plus complexes ou plusieurs calques, vous devrez créer votre propre masque de calque. Si le raycast frappe avec succès quelque chose, hitInfo est utilisé pour récupérer le point normal et le point de coup pour calculer la nouvelle position et rotation du joueur qui devrait se trouver juste sur l'objet. Un décalage de la position du joueur peut être nécessaire en fonction de la taille et de l'origine de l'objet joueur en question.

Enfin, cela n'a vraiment été testé et ne fonctionne probablement bien que sur des objets simples tels que des sphères. Comme le suggère le billet de blog sur lequel j'ai basé ma solution, vous souhaiterez probablement effectuer plusieurs diffusions et les faire une moyenne pour votre position et votre rotation afin d'obtenir une transition beaucoup plus agréable lorsque vous vous déplacez sur un terrain plus complexe. Il peut également y avoir d'autres pièges auxquels je n'ai pas pensé à ce stade.

Caméra et mouvement

Une fois que le joueur collait à la surface de l'objet, la prochaine tâche à accomplir était le mouvement. J'avais à l'origine commencé par le mouvement par rapport au joueur, mais j'ai commencé à rencontrer des problèmes aux pôles de la sphère où les directions ont soudainement changé, ce qui a fait que mon joueur changeait rapidement de direction encore et encore ne me permettant jamais de passer les pôles. J'ai fini par faire bouger mes joueurs par rapport à la caméra.

Ce qui fonctionnait bien pour mes besoins était d'avoir une caméra qui suivait strictement le joueur uniquement en fonction de la position des joueurs. En conséquence, même si la caméra était techniquement en rotation, une pression vers le haut déplaçait toujours le lecteur vers le haut de l'écran, vers le bas vers le bas, etc. avec la gauche et la droite.

Pour ce faire, ce qui suit a été exécuté sur la caméra où l'objet cible était le joueur:

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);
}

Enfin, pour déplacer le joueur, nous avons exploité la transformation de la caméra principale de sorte que nos commandes montent, descendent, etc. Et c'est ici que nous appelons UpdatePlayerTransform qui obtiendra notre position accrochée à l'objet monde.

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);
}

Pour implémenter une caméra plus intéressante mais les commandes étant à peu près les mêmes que celles que nous avons ici, vous pouvez facilement implémenter une caméra qui n'est pas rendue ou juste un autre objet factice pour baser le mouvement, puis utiliser la caméra la plus intéressante pour rendre ce vous voulez que le jeu ressemble. Cela permettra de belles transitions de caméra lorsque vous contournerez des objets sans casser les commandes.

SpartanDonut
la source
3

Je pense que cette idée fonctionnera:

Conservez une copie côté CPU du maillage de la planète. Avoir des sommets maillés signifie également que vous avez des vecteurs normaux pour chaque point de la planète. Désactivez ensuite complètement la gravité pour toutes les entités, appliquant plutôt une force dans la direction exactement opposée d'un vecteur normal.

Maintenant, sur la base de quel point ce vecteur normal de la planète devrait-il être calculé?

La réponse la plus simple (qui, j'en suis sûr, fonctionnera bien) est de se rapprocher de la méthode de Newton : lorsque les objets apparaissent pour la première fois, vous connaissez toutes leurs positions initiales sur la planète. Utilisez cette position initiale pour déterminer le upvecteur de chaque objet . De toute évidence, la gravité sera dans la direction opposée (versdown ). Dans l'image suivante, avant d'appliquer la gravité, lancez un rayon de la nouvelle position de l'objet vers son ancien downvecteur. Utilisez l'intersection de ce rayon avec la planète comme nouvelle référence pour déterminer le upvecteur. Le rare cas où le rayon ne frappe rien signifie que quelque chose s'est horriblement mal passé et vous devriez ramener votre objet à l'endroit où il était dans l'image précédente.

Notez également qu'en utilisant cette méthode, plus le joueur est originaire de la planète, plus l'approximation est mauvaise. Il est donc préférable d'utiliser quelque part autour des pieds de chaque joueur comme origine. Je suppose, mais je pense que l'utilisation des pieds comme origine entraînera également une manipulation et une navigation plus faciles du joueur.


Une dernière note: pour de meilleurs résultats, vous pouvez même faire ce qui suit: suivre le mouvement du joueur dans chaque image (par exemple en utilisant current_position - last_position). Ensuite, fixez-le de movement_vectorsorte que sa longueur vers l'objet upsoit nulle. Appelons ce nouveau vecteur reference_movement. Déplacez le précédent reference_pointpar reference_movementet utilisez ce nouveau point comme origine du lancer de rayons. Après (et si) le rayon frappe la planète, déplacez-vous reference_pointvers ce point d' impact . Enfin, calculez le nouveau upvecteur, à partir de ce nouveaureference_point .

Un pseudo code pour le résumer:

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
la source
2

Ce post pourrait être utile. Son essence est que vous n'utilisez pas les contrôleurs de personnage, mais créez le vôtre en utilisant le moteur physique. Ensuite, vous utilisez les normales détectées sous le lecteur pour les orienter vers la surface du maillage.

Voici un bon aperçu de la technique. Il y a beaucoup plus de ressources avec des termes de recherche Web comme "promenade d'unité sur des objets 3d mario galaxy".

Il y a aussi un projet de démonstration de l'unité 3.x qui avait un moteur qui marche, avec un soldat et un chien et dans l'une des scènes. Il a montré la marche sur des objets 3D de style Galaxy . C'est ce qu'on appelle le système de locomotion par runevision.

SpidermanSpidermanDWASC
la source
1
À première vue, la démo ne fonctionne pas très bien et le package Unity contient des erreurs de construction. Je vais voir si je peux faire ce travail mais une réponse plus complète serait appréciée.
SpartanDonut
2

Rotation

Consultez la réponse à cette question sur answers.unity3d.com (effectivement posée par moi-même). Citation:

La première tâche consiste à obtenir un vecteur qui définit up. À partir de votre dessin, vous pouvez le faire de deux manières. Vous pouvez traiter la planète comme une sphère et l'utiliser (object.position - planet.position). La deuxième façon consiste à utiliser Collider.Raycast () et à utiliser le 'hit.normal' qu'il renvoie.

Voici le code qu'il m'a proposé:

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

Appelez cela à chaque mise à jour, et vous devriez le faire fonctionner. (notez que le code est en UnityScript ).
Le même code en C # :

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

La gravité

Pour la gravité, vous pouvez utiliser ma "technique de physique des planètes", que j'ai décrite dans ma question, mais elle n'est pas vraiment optimisée. C'était juste quelque chose que j'avais en tête.
Je vous suggère de créer votre propre système pour la gravité.

Voici un tutoriel Youtube de Sebastian Lague . C'est une solution très efficace.

EDIT: À l'unité, allez dans Edition > Paramètres du projet > Physique et définissez toutes les valeurs de Gravity sur 0 (ou supprimez le Rigidbody de tous les objets), pour éviter que la gravité intégrée (qui tire simplement le joueur vers le bas) entre en conflit avec la solution personnalisée. (éteignez-le)

Daniel Kvist
la source
Graviter le joueur au centre d'une planète (et les faire tourner en conséquence) fonctionne bien pour les planètes presque sphériques, mais je peux imaginer que ça va vraiment mal pour des objets moins sphériques, comme cette forme de fusée sur laquelle Mario saute dans le premier GIF.
Anko
Vous pouvez créer un cube qui est limité pour se déplacer uniquement autour de l'axe X par exemple, à l'intérieur de l'objet non sphérique, et le déplacer lorsque le joueur se déplace, afin qu'il soit toujours droit sous le joueur, puis tirer le joueur jusqu'au cube. Essayez-le.
Daniel Kvist