Como construir um sistema de volta no tempo como em Prince Of Persia, parte 1
() translation by (you can also view the original English article)



Neste tutorial, vamos construir um jogo simples na Unity, onde o jogador pode rebobinar o progresso (que também pode ser adaptado para funcionar em outros sistemas). Nesta primeira parte veremos o básico do sistema, e a próxima parte vai esclarecer tudo e tornar o sistema muito mais versátil.
Em primeiro lugar, vamos dar uma olhada nos jogos que usam isto. Então, veremos outros usos para esta técnica, antes de, finalmente, criar um jogo pequeno que pode rebobinar, que deve dar-lhe uma base para o seu próprio jogo.



Você precisará da versão mais recente da Unity para isso e deve ter alguma experiência. O código-fonte também está disponível para download, se você quiser comparar o seu progresso com ele.
Pronto? Vamos lá!
Como isso foi usado antes?
Prince Of Persia: The Sands of Time é um dos primeiros jogos a realmente integrar uma mecânica de voltar no tempo em sua jogabilidade. Quando você morre não precisa recarregar, mas sim voltar um pouco no passado durante alguns segundos para onde o personagem estava vivo e imediatamente, tentar novamente.



Essa mecânica não é apenas integrada à jogabilidade, mas a narrativa e o universo também, e isso é mencionado ao longo da história.
Outros jogos que utilizam esses sistemas são Braid, por exemplo, que também funciona com voltas no tempo. A heroína Tracer em Overwatch tem um poder que muda a sua posição para uma posição de alguns segundos atrás, isso é essencialmente voltar no tempo, mesmo em um jogo multiplayer. A série de jogos de corrida GRID também tem um mecanismo de retorno, onde você tem uma pequeno número de retrocessos durante uma corrida, que pode ser usado quando você comete uma falha crítica. Isso inibe a frustração causada por deslizes perto do final da corrida, o que pode ser muito irritante.



Outros usos
Mas este sistema não só pode ser usado para substituir salvamentos automáticos. Outra maneira de se aplicar isso é o "carro fantasma" em jogos de corrida e multiplayer assíncrono.
Replays
Replays são outra forma divertida de outra de empregar esse recurso. Isto pode ser visto em jogos como SUPERHOT, a série Worms e praticamente a maioria dos jogos esportivos.
Replays de esportes funcionam da mesma maneira que eles são apresentados na TV, onde uma ação é mostrada mais uma vez, possivelmente de um ângulo diferente. Para isso não é gravado um vídeo, mas sim as ações do usuário, permitindo que a repetição aconteça de ângulos e câmera diferentes. Jogos Worms usam isto de uma forma humorística, onde uma morte muito engraçada ou eficaz é mostrada em um Replay instantâneo.
SUPERHOT também registra seus movimentos. Quando você termina e fase seu progresso inteiro é então repetido, mostrando tudo o que aconteceu.
Super Meat Boy usa isso de uma maneira divertida. Quando você termina uma fase você vê um replay de todas as suas tentativas anteriores uma seguida da outra, terminando com a última performance.



Fantasmas em modos contra-relógio
Fantasmas de corrida é uma técnica onde você corre pelo melhor tempo em uma pista vazia. Mas ao mesmo tempo, você disputa contra um fantasma, que é um carro transparente que dirige exatamente como você dirigiu no melhor tempo. Você não pode colidir com ele, o que significa que você ainda pode se concentrar em obter o melhor tempo.
Em vez de dirigir sozinho você pode competir contra si mesmo, o que torna tudo mais divertido. Esse recurso aparece na maioria dos jogos de corrida, desde a série Need for Speed até Diddy Kong Racing.



Fantasmas Multiplayer
Fantasmas multiplayer assíncronos são outra maneira de usar essa funcionalidade. Neste recurso raramente utilizado, partidas multiplayer são realizadas para gravar os dados de um jogador, que então envia sua partida para outro jogador, que posteriormente pode batalhar contra o primeiro jogador. Os dados são aplicados da mesma forma que seriam em um fantasma em modos contra-relógio, só que você está correndo contra outro jogador.
Isso é usado nos jogos de Trackmania, onde é possível jogar contra certas dificuldades. Estes pilotos gravados lhe darão um adversário para vencer por uma certa recompensa.
Edição de filme
Alguns jogos oferecem isso desde o início, mas se for bem usado pode ser uma ferramenta divertida. Team Fortress 2 oferece um editor de repetição embutido, com o qual você pode criar seus próprios clipes.



Uma vez que o recurso é ativado, você pode gravar e assistir jogos anteriores. O elemento vital é que tudo é gravado, não só o que você vê. Isto significa que você pode se mover no mundo do jogo gravado, ver onde todo mundo está e ter controle sobre o tempo.
Como construí-lo
Para testar este sistema, precisamos de um jogo simples onde podemos testá-lo. Vamos criar um!
O jogador
Crie um cubo em sua cena, este será nosso personagem. Em seguida, crie um novo script C# chamado Player.cs
e escreva a função Update()
desta forma:
1 |
void Update() |
2 |
{
|
3 |
transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical")); |
4 |
transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal")); |
5 |
}
|
Isto vai ler os movimentos nas setas do teclado. Anexe este script no cubo. Agora se você apertar Play será capaz de se movimentar.
Ajuste o ângulo da câmera para que possa ver o cubo de cima. Por fim, crie um plano para servir como chão e atribua alguns materiais diferentes para cada objeto, para que não caiamos no vazio. Deve ficar desta forma:



Experimente. Você deve ser capaz de mover o cubo usando WSAD e as setas.
O TimeController
Agora crie um novo script C# chamado TimeController.cs
e adicione-o em um novo GameObject vazio. Isto irá lidar com a gravação real e a função rebobinar do jogo.
Para fazer isso, registraremos o movimento do personagem. Quando apertarmos o botão de voltar no tempo, nós vamos adaptar as coordenadas do jogador. Para isso comece criando uma variável para armazenar o jogador, como esta:
1 |
public GameObject player; |
Atribuia o objeto jogador para o slot criado no TimeController, para que ele possa acessar o jogador e seus dados.



Agora, precisamos criar uma matriz para armazenar os dados do jogador:
1 |
public ArrayList playerPositions; |
2 |
|
3 |
void Start() |
4 |
{
|
5 |
playerPositions = new ArrayList(); |
6 |
}
|
O que faremos é gravar continuamente a posição do jogador. Teremos a posição de onde o jogador estava no último quadro, a posição onde o jogador estava 6 frames atrás e a posição onde o jogador estava há 8 segundos (ou o quanto você deseja gravar). Mais tarde quando apertarmos o botão, vamos buscar nossa matriz de posições e atribuí-la quadro a quadro, causando a impressão de voltar no tempo.
Primeiro, vamos salvar os dados:
1 |
void FixedUpdate() |
2 |
{
|
3 |
playerPositions.Add (player.transform.position); |
4 |
}
|
Na função FixedUpdate()
gravamos os dados. FixedUpdate()
rodará 50 ciclos por segundo (ou o que achar melhor), o que permite um intervalo fixo para gravar e definir os dados. A função Update()
roda dependendo de quantos frames o CPU suporta, o que faz as coisas mais difíceis.
Este código irá armazenar a posição do jogador em cada frame na matriz. Agora precisamos aplicar!
Vamos adicionar uma verificação para ver se o botão foi pressionado. Para isso, precisamos de uma variável booleana:
1 |
public bool isReversing = false; |
e também de um controle na função Update()
para marcá-la quando quisermos rebobinar os passos:
1 |
void Update() |
2 |
{
|
3 |
if(Input.GetKey(KeyCode.Space)) |
4 |
{
|
5 |
isReversing = true; |
6 |
}
|
7 |
else
|
8 |
{
|
9 |
isReversing = false; |
10 |
}
|
11 |
}
|
Para fazer o jogo funcionar de trás pra frente, vamos aplicar os dados em vez de gravá-los. O novo código para registrar e aplicar a posição do jogador deve ficar assim:
1 |
void FixedUpdate() |
2 |
{
|
3 |
if(!isReversing) |
4 |
{
|
5 |
playerPositions.Add (player.transform.position); |
6 |
}
|
7 |
else
|
8 |
{
|
9 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
10 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
11 |
}
|
12 |
}
|
E todo o script TimeController
ficará assim:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class TimeController: MonoBehaviour |
5 |
{
|
6 |
public GameObject player; |
7 |
public ArrayList playerPositions; |
8 |
public bool isReversing = false; |
9 |
|
10 |
void Start() |
11 |
{
|
12 |
playerPositions = new ArrayList(); |
13 |
}
|
14 |
|
15 |
void Update() |
16 |
{
|
17 |
if(Input.GetKey(KeyCode.Space)) |
18 |
{
|
19 |
isReversing = true; |
20 |
}
|
21 |
else
|
22 |
{
|
23 |
isReversing = false; |
24 |
}
|
25 |
}
|
26 |
|
27 |
void FixedUpdate() |
28 |
{
|
29 |
if(!isReversing) |
30 |
{
|
31 |
playerPositions.Add (player.transform.position); |
32 |
}
|
33 |
else
|
34 |
{
|
35 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
36 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
37 |
}
|
38 |
}
|
39 |
}
|
Também, não se esqueça de adicionar uma checagem na classe Player
para ver se o TimeController
atualmente está rebobinando ou não, e só mover-se quando ele não está voltando. Caso contrário, ele pode criar um comportamento inesperado:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class Player: MonoBehaviour |
5 |
{
|
6 |
private TimeController timeController; |
7 |
|
8 |
void Start() |
9 |
{
|
10 |
timeController = FindObjectOfType(typeof(TimeController)) as TimeController; |
11 |
}
|
12 |
|
13 |
void Update() |
14 |
{
|
15 |
if(!timeController.isReversing) |
16 |
{
|
17 |
transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical")); |
18 |
transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal")); |
19 |
}
|
20 |
}
|
21 |
}
|
Essas novas linhas automaticamente localizarão o objeto TimeController
na inicialização e verificarão durante tempo de execução para ver se estamos atualmente jogando ou rebobinando. Só podemos controlar o personagem quando nós não estamos voltando no tempo.
Agora você deve ser capaz de se mover pelo mundo e rebobinar seu movimento pressionando espaço. Se você baixar o pacote de compilação anexado a este artigo e abrir o TimeRewindingFunctionality01 você pode experimentar!
Mas espere, por que é que nosso cubo continua olhando para a última posição que nós o deixamos? Porque não gravamos também sua rotação!
Para isso você precisa de outra matriz para manter seus valores de rotação, instanciá-lo no início, salvar e aplicar os dados da mesma maneira que lidamos com dados de posição.
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class TimeController: MonoBehaviour |
5 |
{
|
6 |
public GameObject player; |
7 |
public ArrayList playerPositions; |
8 |
public ArrayList playerRotations; |
9 |
public bool isReversing = false; |
10 |
|
11 |
void Start() |
12 |
{
|
13 |
playerPositions = new ArrayList(); |
14 |
playerRotations = new ArrayList(); |
15 |
}
|
16 |
|
17 |
void Update() |
18 |
{
|
19 |
if(Input.GetKey(KeyCode.Space)) |
20 |
{
|
21 |
isReversing = true; |
22 |
}
|
23 |
else
|
24 |
{
|
25 |
isReversing = false; |
26 |
}
|
27 |
}
|
28 |
|
29 |
void FixedUpdate() |
30 |
{
|
31 |
if(!isReversing) |
32 |
{
|
33 |
playerPositions.Add (player.transform.position); |
34 |
playerRotations.Add (player.transform.localEulerAngles); |
35 |
}
|
36 |
else
|
37 |
{
|
38 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
39 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
40 |
|
41 |
player.transform.localEulerAngles = (Vector3) playerRotations[playerRotations.Count - 1]; |
42 |
playerRotations.RemoveAt(playerRotations.Count - 1); |
43 |
}
|
44 |
}
|
45 |
}
|
Experimente! TimeRewindingFunctionality02 é a versão melhorada. Agora nosso cubo pode voltar no tempo e ele vai parecer do mesmo jeito que estava naquele momento.
Conclusão
Nós construímos um protótipo simples com um sistema de voltar no tempo já utilizável, mas ele ainda está longe de pronto. Na próxima parte desta série vamos torná-lo muito mais estável e versátil e adicionar alguns efeitos.
Aqui está o que ainda precisamos fazer:
- gravar apenas a cada ~12 frames e interpolar entre o que foi gravado para salvar uma carga enorme de dados;
- apenas gravar as últimas posições ~75 posições e rotações do jogador para certificar-se de que a matriz fique muito grande para que o jogo não tenha problemas.
Nós também daremos uma olhada em como estender este sistema para o jogador:
- gravar mais do que apenas o jogador;
- adicionar um efeito para indicar que o rebobinamento está acontecendo (como um efeito de blur);
- usar uma classe personalizada para manter a posição do jogador e a rotação em vez de matrizes.