Vue et déplacement TPS sous Unity

Commençant un nouveau projet, j’ai regardé rapidement s’il y avait des scripts simples pour faire une vue TPS gratuit. J’ai été assez déçu d’où cet article. Déjà TPS ou third personne shooter, est traduit par vue à la 3ème personne… c’est à dire ce que vous voyez sur l’image ci-dessus. On le traduit parfois par vue à l’épaule même si ce n’est pas toujours au niveau de l’épaule. Le but étant de voir notre avatar… après tout on c’est cassez le culs à faire un modèle 3D autant le mettre en avant !

 

Je vous propose donc un script assez simple pour gérer la caméra et le déplacement de l’avatar. Je cherche un script très simple qui permet seulement de se déplacer avant/arrière, gauche/droite, et tourner la caméra avec la souris. Après il est possible d’utiliser des assets plus complet, mais le but est déjà de comprendre les bases avant de partir dans plus complexe.

Lors de mes recherches, je suis tombé sur cet asset. Par contre il était hors de question de réutiliser le script fourni. Étant inutilement compliqué et surtout entièrement en portugais. J’ai cependant gardé le modèle et le principe. Mais si vous voulez utiliser votre propre modèle, vous pouvez regarder l’article concernant l’animation d’un modèle 3D.

 

Télécharger la mise à jour

Vous pouvez télécharger l’asset et l’importer.

Vous trouverez une scène avec une démo pour tester et comprendre comment est utiliser le script TPS Controller.cs

 

Le script prend en paramètre la caméra et un game object (ici OrbitalCamera) permettant de tourner la caméra.

 

Il a aussi besoin qu’un des éléments enfants possède un Animator. Mais ce point peut être changé dans le script.

 

Les paramètres Angular Velocity et Speed étant en paramètre pour améliorer selon vos besoins.

 

Explication du script « TPS Controller »

Je vous propose de passer en revue le script en expliquant étape par étape.

Pour l’explication rapide, lorsque le joueur interagit, on va changer d’état, pour empêcher de faire un pas de côté et d’avancer en même temps. C’est une machine à état très simplifié et marchant à chaque appel de la méthode Update.

Start

    void Start ()
    {
        animator = GetComponentInChildren<Animator>();
        state = MovementState.Idle;
        aiming = false;
        horizontal = transform.eulerAngles.y;        
        vertical = transform.eulerAngles.x;

        // Lock the cursor on the screen and hide it
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

Initialisation des différentes variables utiles.

  • On récupère l’animator du personnage
  • transform.eulerAngles permettant de récupérer l’angle en degré et non en Quarternion.
  • Cursor.lockState permet de bloquer le curseur dans la fenêtre puis de cacher le curseur.

 

Update

<span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span>
    void Update ()
    {
        UpdateMovement();
        animator.SetInteger("State", (int)state);
        UpdateCamera();
    }

La fonction update s’occupe de tout, mais si vous voulez de meilleures performances il faudra regarder pour placer une partie dans la fonction FixedUpdate(). On s’occupe ici de 3 parties, la détection des commandes (a, w, s, d, click gauche), la mise à jour de l’état du personnage et la mise à jour du zoom de la caméra.

Contrôles

        // Check if the user press shift
        if(Input.GetKeyDown(KeyCode.LeftShift)) {
            shiftPressed = true; // Remember that the key is pressed
        }
        // Check if he release the button
        else if(Input.GetKeyUp(KeyCode.LeftShift)) {
            shiftPressed = false;
        }

On pourrait être n’importe où dans Update() ou FixedUpdate() pour faire cette manip, en l’occurrence on est dans UpdateMovement(). C’est une astuce pour détecter l’appui d’une touche, la retenir dans une variable, et remettre à 0 la variable quand la touche est relâchée.

        Vector3 moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        
        // Moving forward is priorize
        bool isStepMovement = Mathf.Abs(moveDirection.x) > Mathf.Abs(moveDirection.z);
        bool isMove = Mathf.Abs(moveDirection.x) > 0 || Mathf.Abs(moveDirection.z) > 0;
        bool isBackMove = moveDirection.z < 0;

Partie un peu plus intéressante, j’utilise ces variables pour déterminer si le personnage doit changer d’état.

  • moveDirection : On récupère ici sous la forme d’un vecteur3 le déplacement directement via les commandes par défaut d’unity. (Par défaut malheureusement W, A, S D) L’avantage étant qu’il gère autant le clavier que la manette. De plus on peut utilise une accélération, c’est à dire qu’au lieu d’avoir une vitesse de 0 à 100 en instantanée, on va accélérer sur quelques frames pour lisser le tout.
  • isStepMovement : Exprime si l’utilisateur demande un déplacement latéral (axe X) si celui-ci est plus demandé qu’un déplacement en avant ou arrière (axe Z). L’utilisation de Mathf.Abs() étant pour récupérer la valeur absolue, et donc vérifier si on est strictement différent de 0.
  • isMove : Exprime si l’utilisateur demande un déplacement, qu’il soit latérale ou avant/arrière.
  • isBackMove : Pour vérifier si le joueur souhaite plutôt reculer qu’avancer.

On a tout ce qu’il faut pour déterminer ce que l’utilisateur souhaite, aller plutôt à gauche, plutôt à droite ?

Remarque : C’est une très mauvaise habitude de vérifier si votre float est égal à 0 ou différent de 0 (float == 0 ou float != 0). Le float est une valeur flottante, c’est-à-dire qu’il sera qu’une approximation de la valeur souhaitée et presque jamais exactement ce que vous souhaitez. Si vous voulez plus d’explication plus complète => https://msdn.microsoft.com/en-us/library/system.single.epsilon.aspx ou https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

        // Detect if the state must be changed
        if(isMove && !isStepMovement && !isBackMove && !shiftPressed) {
            state = MovementState.Walking;
        }
        else if(isMove && !isStepMovement && !isBackMove && shiftPressed) {
            state = MovementState.Running;
        }
        else if(isMove && isBackMove && !isStepMovement) {
            state = MovementState.WalkingBack;
        }
        else if(isMove && isStepMovement && moveDirection.x > 0) {
            state = MovementState.StepRight;
        }
        else if(isMove && isStepMovement && moveDirection.x < 0) {
            state = MovementState.StepLeft;
        }
        else if(!isMove) {
            state = MovementState.Idle;
        }

J’ai fait un cas relativement simple, pour déterminer ce que le joueur veut faire. A noter que si vous demandez à allez vers la gauche moveDirection.x sera négatif.

        // If the user want to run but is aiming, force walking state
        if(aiming && state == MovementState.Running) {
            state = MovementState.Walking;
        }

Petite correction sur l’état, dans le cas où le joueur chercherait à courir et à viser en même temps. On aurait pu le faire directement dans les conditions au-dessus, mais ainsi on pourra ajouter d’autres cas spécifiques et optionnels.

 

Déplacement

        // Fix the movement via the state, apply mask
        moveDirection = Vector3.Scale(moveDirection, MOVEMENT_MASK_VECTOR[(int)state]);
        moveDirection *= speed;

        // Apply movement
        transform.Translate(moveDirection * Time.deltaTime);

Avant d’appliquer le mouvement, on déduit de notre état un mask MOVEMENT_MASK_VECTOR. Pourquoi faire une multiplication entre le vecteur de déplacement et ce mask? On aurait pu directement utiliser le mask comme déplacement, permettant de donner la direction uniquement via notre état et non via toutes les touches de l’utilisateur. Sauf que, comme mentionné plus haut, dans notre vecteur moveDirection, on n’a pas une valeur qui passe de 0 à 1 en une frame, mais justement quelques frames/secondes pour accélérer et ralentir. On applique donc le mask pour n’utilise QUE la valeur de l’axe utile.

Si vous préférez pouvoir courir en diagonale, n’hésitez pas à enlever ce mask.

La vitesse speed est multipliée pour pouvoir ponctuer de manière statique ou dynamique selon vos besoins.

 

Enfin on ajoute le Time.deltaTime, il est très important parce qu’il permet de pondérer selon le temps passé depuis le dernier appel à la méthode Update ! Sans lui vous pouvez être sûr que votre vitesse ne sera en rien stable. Quand vous affichez le FPS (frame per second) d’un jeu, ça affiche le nombre d’images calculées par seconde. Pour faire simple, si vous avez une baisse critique à 1fps au lieu du 30 (ou 60) fps auquel vous vous attendiez. Sans cette pondération, vous avanceriez durant ce seul et uniquement calcul que de 0,03m et non les 2m auquel vous vous attendiez durant cette seconde.

Enfin on applique le mouvement, au transform de l’ensemble de l’objet. C’est pour cette raison que le script est sur la racine, comprenant la caméra et le modèle 3d. On veut que tout reste ensemble.

 

Si vous préférez utiliser le rigidBody avec les fonctions de AddForce(), vous aurez besoin de la méthode TransformDirection().

        Vector3 dir = transform.TransformDirection(movement);
        myRigidbody.AddForce(dir * speed);

Gestion de la souris

        // When the player move his mouse horizontally, the whole character move
        horizontal = (horizontal + angularVelocity * Input.GetAxis("Mouse X")) % 360f;
        transform.rotation = Quaternion.AngleAxis(horizontal, Vector3.up);
        
        // Update camera vertical axis
        vertical = (vertical - angularVelocity * Input.GetAxis("Mouse Y")) % 360f;
        vertical = Mathf.Clamp(vertical, -30, 60); // Min and max 
        OrbitalCamera.transform.localRotation = Quaternion.AngleAxis(vertical, Vector3.right);

Concernant les axes, on utilise comme pour la vitesse une variable angularVelocity pour plus ou moins tourner l’écran rapidement. Ici je ne suis pas sûr que le module 360° soit obligatoire, sauf pour le 2nd cas qui permet de faire un Mathf.Clamp() qui revient à mettre une limite basse et haute comme le ferait un Math.min() et Math.max().

A noter qu’on sépare l’impacte, tourner de gauche à droite, est appliqué à tout l’objet (personnage + caméra) alors que l’axe Y n’agit que sur la caméra. Mais pour améliorer le script, on pourrait impacter la tête du modèle pour que le regarde sur personnage reste vers le centre de la caméra.

Si vous ne connaissez pas le mot clef Quaternion n’ayez pas peur, c’est une manière très utilisée en 3D pour gérer les problèmes de rotation. Si vous voulez en savoir plus n’hésitez pas à regarde la vidéo https://jeux.developpez.com/videos/tutoriels/unity/17-quaternions/ ou la doc d’unity https://unity3d.com/fr/learn/tutorials/topics/scripting/quaternions

Zoom

    private void UpdateCamera()
    {
        // Zoom if the user is aiming
        if (aiming == true && camera.fieldOfView > 37) {
            camera.fieldOfView = camera.fieldOfView - 65.0f * Time.deltaTime;
        }
        if (aiming == false && camera.fieldOfView <span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span>< 60) {
            camera.fieldOfView = camera.fieldOfView + 65.0f * Time.deltaTime;
        }
    }

Petit plaisir de s’amuser avec le fieldOfView qui correspond à l’angle de votre point de vue… dur de l’expliquer en quelques mots, pour que ce soit plus parlant je vous ai mis 3 angles différents 10°, 30° et 120°. Mais pour les connaisseurs, c’est en jouant sur cet angle que vous aurez une vision comme dans Quake.

La caméra ne bouge pas d’un pouce, seule la taille du champs de vision change, ce qu’y peut donner des choses assez abstraites, nous supprimant toute perspective comme on la connait.

 

Le code en entier

using UnityEngine;
using System.Collections;

public class TPSController : MonoBehaviour {
    private enum MovementState { Idle, Walking, Running, WalkingBack, StepRight, StepLeft };
    private readonly Vector3[] MOVEMENT_MASK_VECTOR = {
        new Vector3(0, 0, 0), // Idle
        new Vector3(0, 0, 1f), // Walking
        new Vector3(0, 0, 5f), // Running
        new Vector3(0, 0, 1f), // WalkingBack
        new Vector3(1f, 0, 0), // StepRight
        new Vector3(1f, 0, 0) // StepLeft
    };

    //This variable indicates how is the current state of character.
    private MovementState state;

    //Define the turning speed.
    public float angularVelocity = 4.0f;
    public float speed = 1.0f;
    
    // To command camera
    private float horizontal;
    private float vertical;
    private Animator animator;

    //This variable indicates if the player is aiming or not.
    private bool aiming; 
    private bool shiftPressed = false;

    //Get the camera properties.
    public Camera camera;
    public GameObject OrbitalCamera;

    void Start ()
    {
        animator = GetComponentInChildren<Animator>();
        state = MovementState.Idle;
        aiming = false;
        horizontal = transform.eulerAngles.y;        
        vertical = transform.eulerAngles.x;

        // Lock the cursor on the screen and hide it
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    void Update ()
    {
        UpdateMovement();
        animator.SetInteger("State", (int)state);
        UpdateCamera();
    }

    private void UpdateMovement()
    {
        Vector3 moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        
        // Moving forward is priorize
        bool isStepMovement = Mathf.Abs(moveDirection.x) > Mathf.Abs(moveDirection.z);
        bool isMove = Mathf.Abs(moveDirection.x) > 0 || Mathf.Abs(moveDirection.z) > 0;
        bool isBackMove = moveDirection.z < 0; // Check if the user press shift if(Input.GetKeyDown(KeyCode.LeftShift)) { shiftPressed = true; // Remember that the key is pressed } // Check if he release the button else if(Input.GetKeyUp(KeyCode.LeftShift)) { shiftPressed = false; } // Detect if the state must be changed if(isMove && !isStepMovement && !isBackMove && !shiftPressed) { state = MovementState.Walking; } else if(isMove && !isStepMovement && !isBackMove && shiftPressed) { state = MovementState.Running; } else if(isMove && isBackMove && !isStepMovement) { state = MovementState.WalkingBack; } else if(isMove && isStepMovement && moveDirection.x > 0) {
            state = MovementState.StepRight;
        }
        else if(isMove && isStepMovement && moveDirection.x < 0) { state = MovementState.StepLeft; } else if(!isMove) { state = MovementState.Idle; } // Check if the user click on the left click if (Input.GetKeyDown(KeyCode.Mouse1)) { aiming = true; // Remember that the key is pressed // Check if he release the button } else if (Input.GetKeyUp(KeyCode.Mouse1)) { aiming = false; } // If the user want to run but is aiming, force walking state if(aiming && state == MovementState.Running) { state = MovementState.Walking; } // Fix the movement via the state, apply mask moveDirection = Vector3.Scale(moveDirection, MOVEMENT_MASK_VECTOR[(int)state]); moveDirection *= speed; // Apply movement transform.Translate(moveDirection * Time.deltaTime); // When the player move his mouse horizontally, the whole character move horizontal = (horizontal + angularVelocity * Input.GetAxis("Mouse X")) % 360f; transform.rotation = Quaternion.AngleAxis(horizontal, Vector3.up); // Update camera vertical axis vertical = (vertical - angularVelocity * Input.GetAxis("Mouse Y")) % 360f; vertical = Mathf.Clamp(vertical, -30, 60); // Min and max OrbitalCamera.transform.localRotation = Quaternion.AngleAxis(vertical, Vector3.right); } private void UpdateCamera() { // Zoom if the user is aiming if (aiming == true && camera.fieldOfView > 37) {
            camera.fieldOfView = camera.fieldOfView - 65.0f * Time.deltaTime;
        }
        if (aiming == false && camera.fieldOfView <span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span>< 60) {
            camera.fieldOfView = camera.fieldOfView + 65.0f * Time.deltaTime;
        }
    }
}

Pour aller plus loin, vous pourriez

  • Permettre de regarder de gauche à droite sans bouger le corps mais juste la tête, et bouger le corps que s’il souhaite réèlement se déplacer.
  • Ajouter la gestion de la molette, pour avoir une vu plus éloigner ou plus rapprocher de notre personnage.

 

J’ai trouvé d’autre script intéressant après coups


Une version payante existe, ajoutant des boutons sur l’écran pour une version mobile.

Un dernier que je n’ai pas testé mais qui pour une version gratuite me semble très intéressant

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *