Hoe een Prince-of-Persia-stijl Time-Rewind-systeem te bouwen, deel 2

Wat je gaat creëren

De laatste keer hebben we een eenvoudig spel gemaakt waarin we de tijd kunnen terugspoelen naar een vorig punt. Nu zullen we deze functie stollen en maken het veel leuker om te gebruiken.

Alles wat we hier doen, zal voortbouwen op het vorige deel, dus ga het eens bekijken! Net als voorheen heb je Unity nodig en een basisbegrip ervan.

Klaar? Laten we gaan!

Neem minder gegevens op en interpoleer

Op dit moment nemen we de posities en rotaties van de speler op 50 keer per seconde. Deze hoeveelheid gegevens zal snel onhoudbaar worden, en dit zal vooral merkbaar worden met complexere spelopstellingen en mobiele apparaten met minder verwerkingskracht.

Maar wat we in plaats daarvan kunnen doen, is slechts 4 keer per seconde opnemen en interpoleren tussen die keyframes. Op die manier besparen we 92% van de verwerkingsbandbreedte en krijgen we resultaten die niet te onderscheiden zijn van de 50-frame opnames, omdat ze binnen fracties van een seconde uitkomen.

We beginnen met het opnemen van een hoofdframe bij elke x-opname. Om dit te doen, hebben we eerst deze nieuwe variabelen nodig:

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

De variabele keyframe is het frame in de FixedUpdate methode waarop we de spelersgegevens zullen opnemen. Momenteel is het ingesteld op 5, wat betekent dat elke vijfde keer de FixedUpdate methode doorloopt, worden de gegevens vastgelegd. Zoals FixedUpdate werkt 50 keer per seconde, dit betekent dat er 10 frames per seconde worden opgenomen, vergeleken met 50 eerder. De variabele frameteller wordt gebruikt om de frames te tellen tot het volgende keyframe.

Pas nu het opnameblok aan in de FixedUpdate functie om er zo uit te zien:

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

Als je het nu uitprobeert, zul je zien dat het terugspoelen deel heeft aan een veel kortere tijd dan daarvoor. Dit komt omdat we minder gegevens hebben opgenomen, maar deze op normale snelheid hebben afgespeeld. Nu moeten we dat veranderen.

Ten eerste hebben we een andere nodig frameteller variabele om bij te houden niet van het opnemen van de gegevens, maar van het afspelen ervan.

private int reverseCounter = 0;

Pas de code aan die de positie van de speler herstelt om deze te gebruiken op dezelfde manier waarop we de gegevens vastleggen. De FixedUpdate functie zou er dan als volgt uit moeten zien:

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 = hoofdframe; 

Wanneer u nu de tijd terugspoelt, springt de speler in realtime terug naar zijn vorige posities!

Dat is niet helemaal wat we willen. We moeten interpoleren tussen die keyframes, wat een beetje lastiger zal zijn. Ten eerste hebben we deze vier variabelen nodig:

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

Die zullen de huidige speler data opslaan en die van het opgenomen keyframe ervoor, zodat we tussen die twee kunnen interpoleren.

Dan hebben we deze functie nodig:

void RestorePositions () int lastIndex = keyframes.Count - 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); 

Dit zal de corresponderende informatie toewijzen aan de positie- en rotatievariabelen die we zullen interpoleren tussen. We hebben dit nodig in een aparte functie, zoals we het op twee verschillende plaatsen noemen.

Ons gegevensherstelblok zou er als volgt uit moeten zien:

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

We roepen de functie op om de laatste en de tweede voorlaatste informatiesets uit onze arrays te krijgen telkens wanneer de teller het keyframe-interval bereikt dat we hebben ingesteld (in ons geval 5), maar we moeten het ook in de eerste cyclus noemen wanneer het herstel plaatsvindt. Dit is waarom we dit blok hebben:

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

Om dit te laten werken, hebt u ook de eerste loop variabele:

private bool firstRun = true;

En om het opnieuw in te stellen als de spatiebalk wordt opgeheven:

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

Hier is hoe de interpolatie werkt:

In plaats van alleen het laatste keyframe te gebruiken dat we hebben opgeslagen, krijgt dit systeem de laatste en de voorlaatste en interpoleert het tussen de twee. De hoeveelheid interpolatie is gebaseerd op hoe ver tussen de frames die we momenteel zijn. 

Dit gebeurt allemaal via de Lerp-functie, waar we de huidige positie (of rotatie) en de vorige positie toevoegen. Vervolgens wordt de fractie van de interpolatie berekend, die kan afwijken 0 naar 1. Vervolgens wordt de speler op de equivalente plaats tussen die twee opgeslagen punten geplaatst, bijvoorbeeld 40% op de route naar het laatste hoofdframe.

Wanneer je het vertraagt ​​en het frame voor frame afspeelt, kun je het spelerpersonage daadwerkelijk zien bewegen tussen die keyframes, maar in gameplay is het niet merkbaar.

En daardoor hebben we de complexiteit van de tijdherwikkelingsset-up aanzienlijk verminderd en stabieler gemaakt.

Neem alleen een vast aantal keyframes op

Nu we het aantal frames dat we daadwerkelijk opslaan aanzienlijk hebben verkleind, kunnen we ervoor zorgen dat we niet te veel gegevens opslaan.

Op dit moment stapelen we de opgenomen gegevens in de array, wat niet op de lange termijn zal werken. Naarmate de array groeit, wordt deze lastiger, neemt de toegang langer in beslag en wordt de gehele installatie onstabieler.

Om dit te verhelpen, kunnen we een code instellen die controleert of de array over een bepaalde grootte is gegroeid. Als we weten hoeveel frames per seconde we besparen, kunnen we bepalen hoeveel seconden herwikkelbare tijd we zouden moeten besparen, en wat zou passen bij onze game en de complexiteit ervan. Het enigszins complexe prins van Perzië zorgt voor misschien 15 seconden van herwikkelbare tijd, terwijl de eenvoudigere instelling van Vlecht zorgt voor onbeperkt terugspoelen.

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

Wat er gebeurt, is dat wanneer de array over een bepaalde grootte groeit, we de eerste invoer ervan verwijderen. Dus het blijft maar zo lang als we willen dat de speler terugspoelt, en er is geen gevaar dat het te groot wordt om efficiënt te gebruiken. Zet dit in de FixedUpdate functie na de opname en het afspelen van code.

Gebruik een aangepaste klasse om spelergegevens te bewaren

Op dit moment nemen we de posities en rotaties van de speler op in twee afzonderlijke matrices. Hoewel dit werkt, moeten we onthouden dat we altijd op twee plaatsen tegelijkertijd gegevens moeten registreren en openen, wat de mogelijkheid biedt voor toekomstige problemen.

Wat we kunnen doen, echter. is een aparte klasse maken om beide dingen vast te houden, en mogelijk zelfs meer (als dat in uw project noodzakelijk zou zijn).

De code voor een aangepaste klasse om op te treden als container voor de gegevens ziet er als volgt uit:

public class Keyframe public Vector3 position; openbare Vector3-rotatie; openbaar sleutelframe (Vector3-positie, Vector3-rotatie) this.position = position; this.rotation = rotatie; 

U kunt het toevoegen aan het bestand TimeController.cs, vlak voordat de klasseverklaring begint. Wat het doet is een container bieden om zowel de positie als de rotatie van de speler te bewaren. Met de constructormethode kan deze direct worden gemaakt met de benodigde informatie.

De rest van het algoritme moet worden aangepast om met het nieuwe systeem te werken. In de Start-methode moet de array worden geïnitialiseerd:

keyframes = nieuwe ArrayList ();

En in plaats van te zeggen:

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

We kunnen het direct opslaan in een Keyframe-object:

keyframes.Add (nieuw keyframe (player.transform.position, player.transform.localEulerAngles));

Wat we hier doen is de positie en rotatie van de speler toevoegen aan hetzelfde object, dat vervolgens wordt toegevoegd aan een enkele array, wat de complexiteit van deze set aanzienlijk vermindert.

Voeg een vervagingseffect toe om aan te geven dat terugspoelen gebeurt

We hebben drastisch een soort betekenaar nodig die ons vertelt dat de game momenteel wordt teruggespoeld. Nu, wij weet dit, maar een speler kan in de war zijn. In dergelijke situaties is het goed om meerdere dingen te vertellen aan de speler dat het terugspoelen gebeurt, zoals visueel (via het hele scherm vervaging een beetje) en audio (door de muziek te vertragen en om te keren).

Laten we iets doen dat lijkt op hoe prins van Perzië doet het, en voeg wat vervaging toe.

Time-rewinding van Prince of Persia: The Forgotten Sands

Met Unity kun je meerdere camera-effecten op elkaar leggen en met wat experimenteren kun je er een maken die perfect bij je project past.

Voordat we de basiseffecten kunnen gebruiken, moeten we ze importeren. Ga hiervoor naar Activa> Pakket importeren> Effecten, en importeer alles wat u wordt aangeboden.

Visuele effecten kunnen direct aan de hoofdcamera worden toegevoegd. Ga naar Componenten> Afbeeldingseffecten en voeg een toe vervagen en een Bloeien effect. De combinatie van die twee zou een mooi effect moeten hebben voor waar we voor gaan.

Dit zijn de basisinstellingen. U kunt ze aanpassen om beter bij uw project te passen.

Wanneer je het nu uitprobeert, zal de game dit effect de hele tijd hebben.

Nu moeten we het activeren en desactiveren. Daarvoor, de TimeController moet de afbeeldingseffecten importeren. Voeg deze regel toe aan het begin:

gebruikmakend van UnityStandardAssets.ImageEffects;

Om toegang te krijgen tot de camera vanaf de TimeController, voeg deze variabele toe:

privécamera camera;

En wijs het toe in de Begin functie:

camera = Camera.main;

Voeg vervolgens deze code toe om de effecten te activeren tijdens het terugspoelen en laat ze anders activeren:

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; 

Wanneer u op de spatiebalk drukt, wordt niet alleen de scène teruggespoeld, maar wordt ook het terugspoeleffect op de camera geactiveerd, zodat de speler zegt dat er iets gebeurt.

De volledige code van de TimeController zou er als volgt uit moeten zien:

gebruikmakend van UnityEngine; met behulp van System.Collections; gebruikmakend van UnityStandardAssets.ImageEffects; public class Keyframe public Vector3 position; openbare Vector3-rotatie; openbaar sleutelframe (Vector3-positie, Vector3-rotatie) this.position = position; this.rotation = rotatie;  public class TimeController: MonoBehaviour public GameObject player; openbare ArrayList-hoofdframes; public bool isReversing = false; public int keyframe = 5; private int frameCounter = 0; private int reverseCounter = 0; private Vector3 currentPosition; private Vector3 previousPosition; privé Vector3 currentRotation; private Vector3 vorigeRotation; privécamera camera; 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) keyframe; 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 = keyframes.Count - 1; int secondToLastIndex = keyframes.Count - 2; if (secondToLastIndex> = 0) currentPosition = (hoofdframes [lastIndex] als sleutelframe) .positie; previousPosition = (hoofdframes [secondToLastIndex] als sleutelframe) .positie; currentRotation = (hoofdframes [lastIndex] als hoofdframe) .rotatie; previousRotation = (hoofdframes [secondToLastIndex] als hoofdframe) .rotatie; keyframes.RemoveAt (lastIndex); 

Download het bijgevoegde build-pakket en probeer het uit!

Conclusie

Onze tijdwinst-game is nu veel beter dan voorheen. Het algoritme is merkbaar verbeterd en gebruikt 90% minder verwerkingskracht, het is veel stabieler en we hebben een leuke betekenaar die ons vertelt dat we momenteel terugspoelen.

Ga nu een spel maken met dit!