Comment construire un système de retour rapide dans le style Prince-of-Persia, 2e partie

Ce que vous allez créer

La dernière fois, nous avons créé un jeu simple dans lequel nous pouvons revenir à un point précédent. Maintenant, nous allons consolider cette fonctionnalité et la rendre beaucoup plus amusante à utiliser.

Tout ce que nous faisons ici va construire sur la partie précédente, alors allez y faire un tour! Comme auparavant, vous aurez besoin d'Unity et d'une compréhension de base.

Prêt? Allons-y!

Enregistrer moins de données et interpoler

En ce moment, nous enregistrons les positions et les rotations du joueur. 50 fois par seconde. Cette quantité de données deviendra rapidement intenable, et cela deviendra particulièrement visible avec des configurations de jeu plus complexes et des appareils mobiles avec moins de puissance de traitement.

Mais ce que nous pouvons faire à la place, c’est enregistrer seulement 4 fois par seconde et interpoler entre ces images clés. De cette façon, nous économisons 92% de la bande passante de traitement et obtenons des résultats impossibles à distinguer des enregistrements de 50 images, car ils se déroulent en quelques fractions de seconde..

Nous commencerons par enregistrer uniquement une image clé toutes les x images. Pour ce faire, nous avons d’abord besoin de ces nouvelles variables:

public int keyframe = 5; private int frameCounter = 0;

La variable image clé est le cadre dans le FixedUpdate méthode à laquelle nous allons enregistrer les données du joueur. Actuellement, il est réglé sur 5, ce qui signifie toutes les cinq fois la FixedUpdate la méthode défile, les données sont enregistrées. Comme FixedUpdate fonctionne 50 fois par seconde, cela signifie que 10 images seront enregistrées par seconde, contre 50 auparavant. La variable frameCounter sera utilisé pour compter les images jusqu'à la prochaine image clé.

Maintenant, adaptez le bloc d'enregistrement dans le FixedUpdate fonction à ressembler à ceci:

if (! isReversing) if (frameCounter < keyframe)  frameCounter += 1;  else  frameCounter = 0; playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles);  

Si vous l'essayez maintenant, vous verrez que le rembobinage prend beaucoup moins de temps qu'avant. En effet, nous avons enregistré moins de données, mais les avons lues à une vitesse normale. Maintenant, nous devons changer cela.

Tout d'abord, nous avons besoin d'un autre frameCounter variable pour garder une trace non pas de l'enregistrement des données, mais de leur lecture.

private int reverseCounter = 0;

Adaptez le code qui restaure la position du lecteur pour l'utiliser de la même manière que nous enregistrons les données. le FixedUpdate la fonction devrait alors ressembler à ceci:

void FixedUpdate () if (! isReversing) if (frameCounter < keyframe)  frameCounter += 1;  else  frameCounter = 0; playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles);   else  if(reverseCounter > 0) reverseCounter - = 1;  else player.transform.position = (Vector3) playerPositions [playerPositions.Count - 1]; playerPositions.RemoveAt (playerPositions.Count - 1); player.transform.localEulerAngles = (Vector3) playerRotations [playerRotations.Count - 1]; playerRotations.RemoveAt (playerRotations.Count - 1); reverseCounter = image clé; 

Lorsque vous rembobinez l'heure, le joueur reviendra à ses positions précédentes, en temps réel!

Ce n'est pas tout à fait ce que nous voulons, cependant. Nous devons interpoler entre ces images clés, ce qui sera un peu plus compliqué. Premièrement, nous avons besoin de ces quatre variables:

privé Vector3 currentPosition; private Vector3 previousPosition; privé Vector3 currentRotation; private Vector3 previousRotation;

Ceux-ci sauvegarderont les données du joueur actuel et celles de l’image clé enregistrée précédemment afin que nous puissions interpoler entre ces deux images..

Ensuite, nous avons besoin de cette fonction:

void RestorePositions () int lastIndex = images clés. Nombre - 1; int secondToLastIndex = keyframes.Count - 2; if (secondToLastIndex> = 0) currentPosition = (Vector3) playerPositions [lastIndex]; previousPosition = (Vector3) playerPositions [secondToLastIndex]; playerPositions.RemoveAt (lastIndex); currentRotation = (Vector3) playerRotations [lastIndex]; previousRotation = (Vector3) playerRotations [secondToLastIndex]; playerRotations.RemoveAt (lastIndex); 

Cela affectera les informations correspondantes aux variables de position et de rotation entre lesquelles nous interpolerons. Nous avons besoin de cela dans une fonction distincte, comme nous l'appelons à deux endroits différents.

Notre bloc de restauration de données devrait ressembler à ceci:

si (reverseCounter> 0) reverseCounter - = 1;  else reverseCounter = keyframe; RestorePositions ();  if (firstRun) firstRun = false; RestorePositions ();  float interpolation = (float) reverseCounter / (float) image clé; player.transform.position = Vector3.Lerp (previousPosition, currentPosition, interpolation); player.transform.localEulerAngles = Vector3.Lerp (previousRotation, currentRotation, interpolation);

Nous appelons la fonction pour obtenir le dernier et l’avant-dernier ensembles d’informations de nos tableaux lorsque le compteur atteint l’intervalle d’images clé défini (dans notre cas, 5), mais nous devons également l'appeler au premier cycle lorsque la restauration est en cours. C'est pourquoi nous avons ce bloc:

if (firstRun) firstRun = false; RestorePositions (); 

Pour que cela fonctionne, vous avez également besoin de la première exécution variable:

private bool firstRun = true;

Et pour le réinitialiser lorsque le bouton de l'espace est levé:

if (Input.GetKey (KeyCode.Space)) isReversing = true;  else isReversing = false; firstRun = true; 

Voici comment fonctionne l'interpolation:

Au lieu d'utiliser uniquement la dernière image clé que nous avons enregistrée, ce système récupère la dernière et l'avant-dernière et interpole entre elles. La quantité d'interpolation est basée sur la distance entre les images que nous avons actuellement.. 

Tout cela se passe via la fonction Lerp, où nous ajoutons la position actuelle (ou rotation) et la précédente. Ensuite, la fraction de l'interpolation est calculée, ce qui peut aller de 0 à 1. Ensuite, le joueur est placé à la place équivalente entre ces deux points sauvegardés, par exemple, 40% du parcours de la dernière image clé..

Lorsque vous le ralentissez et que vous le lisez image par image, vous pouvez réellement voir le personnage du joueur bouger entre ces images clés, mais en mode de jeu, cela ne se remarque pas..

Nous avons ainsi considérablement réduit la complexité de la configuration du rembobinage temporel et l'avons rendue beaucoup plus stable..

Enregistrer uniquement un nombre fixe d'images clés

Maintenant que nous avons considérablement réduit le nombre d'images enregistrées, nous pouvons nous assurer de ne pas enregistrer trop de données..

À l'heure actuelle, nous ne faisons qu'empiler les données enregistrées dans le tableau, ce qui ne fonctionnera pas à long terme. Au fur et à mesure que le tableau s'agrandit, il deviendra de plus en plus difficile à manier, l'accès prendra plus de temps et l'ensemble de la configuration deviendra plus instable..

Afin de résoudre ce problème, nous pouvons instituer un code qui vérifie si le tableau a dépassé une certaine taille. Si nous savons combien d'images nous économisons par seconde, nous pouvons déterminer le nombre de secondes de temps qu'il faut sauvegarder, et ce qui convient à notre jeu et à sa complexité. Le peu complexe Prince de Perse permet peut-être 15 secondes de temps de rembobinage, tandis que la configuration plus simple de Tresser permet un rembobinage illimité.

if (playerPositions.Count> 128) playerPositions.RemoveAt (0); playerRotations.RemoveAt (0); 

Ce qui se passe, c'est qu'une fois que le tableau dépasse une certaine taille, nous en supprimons la première entrée. Ainsi, il ne reste que tant que nous voulons que le lecteur rembobine, et il n’existe aucun risque qu’il devienne trop volumineux pour une utilisation efficace. Mettez ceci dans le FixedUpdate fonction après l'enregistrement et la lecture du code.

Utiliser un cours personnalisé pour conserver les données du joueur

À l'heure actuelle, nous enregistrons les positions et les rotations des joueurs dans deux tableaux distincts. Bien que cela fonctionne, nous devons nous rappeler de toujours enregistrer et accéder aux données à deux endroits en même temps, ce qui peut entraîner de futurs problèmes..

Ce que nous pouvons faire, cependant. est de créer une classe séparée pour contenir ces deux choses, et peut-être même plus (si cela est nécessaire dans votre projet).

Le code pour une classe personnalisée agissant en tant que conteneur pour les données ressemble à ceci:

classe publique Keyframe position publique sur Vector3; rotation publique de Vector3; image clé publique (position Vector3, rotation Vector3) this.position = position; this.rotation = rotation; 

Vous pouvez l'ajouter au fichier TimeController.cs juste avant le début de la déclaration de classe. Ce qu'il fait est de fournir un conteneur pour enregistrer à la fois la position et la rotation du joueur. La méthode constructeur permet de la créer directement avec les informations nécessaires.

Le reste de l'algorithme devra être adapté pour fonctionner avec le nouveau système. Dans la méthode Start, le tableau doit être initialisé:

images clés = new ArrayList ();

Et au lieu de dire:

playerPositions.Add (player.transform.position); playerRotations.Add (player.transform.localEulerAngles);

Nous pouvons l'enregistrer directement dans un objet Keyframe:

keyframes.Add (nouvelle image clé (player.transform.position, player.transform.localEulerAngles));

Ce que nous faisons ici est d’ajouter la position et la rotation du joueur dans le même objet, qui est ensuite ajouté dans un tableau unique, ce qui réduit considérablement la complexité de cette configuration..

Ajouter un effet de flou pour indiquer que le rembobinage se produit

Nous avons absolument besoin d'une sorte de signifiant nous informant que le jeu est en train d'être rembobiné. Maintenant, nous sachez cela, mais un joueur peut être confus. Dans de telles situations, il est bon d’avoir plusieurs choses indiquant au lecteur que le rembobinage est en cours, comme visuellement (via l’écran un peu flou) et l’audio (lorsque la musique ralentit et s’inverse)..

Faisons quelque chose de semblable à comment Prince de Perse le fait et ajoute un peu de flou.

Temps de rembobinage de Prince de Perse, les sables oubliés

Unity vous permet d'ajouter plusieurs effets de caméra les uns sur les autres et, avec quelques expériences, vous pouvez en créer un qui convient parfaitement à votre projet..

Avant de pouvoir utiliser les effets de base, nous devons les importer. Pour ce faire, allez à Biens> Package d'importation> Effets, et importer tout ce qui vous est offert.

Des effets visuels peuvent être ajoutés directement à la caméra principale. Aller à Composants> Effets d'image et ajouter un Brouiller et un Floraison effet. La combinaison de ces deux devrait donner un bel effet sur ce que nous visons.

Ce sont les paramètres de base. Vous pouvez les ajuster pour mieux correspondre à votre projet.

Lorsque vous l'essayez maintenant, le jeu aura cet effet tout le temps.

Maintenant, nous devons l'activer et le désactiver respectivement. Pour cela, le TimeController doit importer les effets d'image. Ajoutez cette ligne au tout début:

using UnityStandardAssets.ImageEffects;

Pour accéder à la caméra depuis le TimeController, ajoutez cette variable:

caméra privée;

Et l'assigner dans le Début une fonction:

camera = Camera.main;

Ajoutez ensuite ce code pour activer les effets pendant le temps de rembobinage, et activez-les sinon:

void Update () if (Input.GetKey (KeyCode.Space)) isReversing = true; camera.GetComponent() .enabled = true; camera.GetComponent() .enabled = true;  else isReversing = false; firstRun = true; camera.GetComponent() .enabled = false; camera.GetComponent() .enabled = false; 

Lorsque vous appuyez sur la touche Espace, vous rembobinez à présent la scène, mais vous activez également l'effet de rembobinage sur l'appareil photo en informant le lecteur qu'il se passe quelque chose..

Le code entier de la TimeController devrait ressembler à ceci:

utiliser UnityEngine; using System.Collections; using UnityStandardAssets.ImageEffects; classe publique Keyframe position publique sur Vector3; rotation publique de Vector3; image clé publique (position Vector3, rotation Vector3) this.position = position; this.rotation = rotation;  public class TimeController: MonoBehaviour public GameObject player; images clés ArrayList publiques; public bool isReversing = false; public int keyframe = 5; private int frameCounter = 0; private int reverseCounter = 0; privé Vector3 currentPosition; private Vector3 previousPosition; privé Vector3 currentRotation; private Vector3 previousRotation; caméra privée; private bool firstRun = true; void Start () keyframes = new ArrayList (); camera = Camera.main;  void Update () if (Input.GetKey (KeyCode.Space)) isReversing = true; camera.GetComponent() .enabled = true; camera.GetComponent() .enabled = true;  else isReversing = false; firstRun = true; camera.GetComponent() .enabled = false; camera.GetComponent() .enabled = false;  void FixedUpdate () if (! isReversing) if (frameCounter < keyframe)  frameCounter += 1;  else  frameCounter = 0; keyframes.Add(new Keyframe(player.transform.position, player.transform.localEulerAngles));   else  if(reverseCounter > 0) reverseCounter - = 1;  else reverseCounter = keyframe; RestorePositions ();  if (firstRun) firstRun = false; RestorePositions ();  float interpolation = (float) reverseCounter / (float) image clé; player.transform.position = Vector3.Lerp (previousPosition, currentPosition, interpolation); player.transform.localEulerAngles = Vector3.Lerp (previousRotation, currentRotation, interpolation);  if (keyframes.Count> 128) keyframes.RemoveAt (0);  void RestorePositions () int lastIndex = images clés. Nombre - 1; int secondToLastIndex = keyframes.Count - 2; if (secondToLastIndex> = 0) currentPosition = (images clés [lastIndex] en tant qu'images clés) .position; previousPosition = (images clés [secondToLastIndex] en tant qu'image clé) .position; currentRotation = (images clés [lastIndex] en tant qu'image clé) .rotation; previousRotation = (images clés [secondToLastIndex] en tant qu'image clé) .rotation; images clés.RemoveAt (lastIndex); 

Téléchargez le package de construction ci-joint et essayez-le!

Conclusion

Notre temps de rembobinage est maintenant beaucoup mieux qu'avant. L'algorithme est sensiblement amélioré et utilise 90% de puissance de calcul en moins, il est beaucoup plus stable et nous avons un gentil signifiant nous informant que nous sommes en train de rembobiner.

Maintenant, allez faire un jeu en utilisant cette!