Salvando dados no Unity3D usando o PlayPrefs
Avalie esse Tutorial
(Parte 1 da série de comparação de persistência)
A persistência de dados é uma parte importante da maioria dos jogos. A Unity oferece apenas um conjunto limitado de soluções, o que significa que também precisamos procurar outras opções.
Nesta série de tutoriais, exploraremos as opções oferecidas pela Unity e por bibliotecas de terceiros. Cada parte examinará mais profundamente uma delas, com a parte final sendo uma comparação:
- Parte 1: PlayerPrefs (este tutorial)
- Parte 2: Arquivos
- Parte 3: BinaryReader e BinaryWriter (em breve)
- Parte 4: SQL
- Parte 5: Realm Unity SDK
- Parte 6: Comparação de todas essas opções
Para facilitar o acompanhamento, preparamos um repositório de exemplo para você. Todos esses exemplos podem ser encontrados no mesmo projeto Unity, pois todos usam o mesmo jogo de exemplo, então você pode ver melhor as diferenças entre essas abordagens de persistência.
O repositório pode ser encontrado em https://github.com/realm/unity-examples, com este tutorial estando na ramificaçãopersistence-comparison ao lado de outros tutoriais que preparamos para você.
Observe que, se já tiver trabalhado em qualquer um dos outros tutoriais desta série, você pode pular esta seção, pois estamos usando o mesmo exemplo em todas as partes da série, para que seja mais fácil ver as diferenças entre as abordagens.
O objetivo desta série de tutoriais é mostrar a você uma maneira rápida e fácil de dar os primeiros passos nas várias formas de persistir os dados em seu jogo.
Portanto, o exemplo que usaremos será o mais simples possível no próprio editor para que possamos nos concentrar totalmente no código real que precisamos escrever.
Uma cápsula simples na cena será usada para que possamos interagir com um objeto de jogo. Em seguida, registramos os cliques na cápsula e mantemos a contagem de ocorrências.
Quando você abre um modelo de 3D limpo, basta escolher
GameObject
-> 3D Object
-> Capsule
.Em seguida, você pode adicionar scripts à cápsula ativando-a na hierarquia e usando
Add Component
no inspetor.Os scripts que adicionaremos a esta cápsula, apresentando os diferentes métodos, terão a mesma estrutura básica que pode ser encontrada em
HitCountExample.cs
.1 using UnityEngine; 2 3 /// <summary> 4 /// This script shows the basic structure of all other scripts. 5 /// </summary> 6 public class HitCountExample : MonoBehaviour 7 { 8 // Keep count of the clicks. 9 [private int hitCount; // 1 ] 10 11 private void Start() // 2 12 { 13 // Read the persisted data and set the initial hit count. 14 hitCount = 0; // 3 15 } 16 17 private void OnMouseDown() // 4 18 { 19 // Increment the hit count on each click and save the data. 20 hitCount++; // 5 21 } 22 }
A primeira coisa que precisamos adicionar é um contador para os cliques na cápsula (1). Adicione um
[SerializeField]
aqui para que você possa observá-lo ao clicar na cápsula no editor Unity.Sempre que o jogo for iniciado (2), queremos ler a contagem atual de acertos da persistência e inicializar
hitCount
de acordo (3). Isso é feito no método Start()
que é chamado sempre que uma cena é carregada para cada objeto de jogo ao qual esse script está anexado.A segunda parte disso é salvar as alterações, o que queremos fazer sempre que registrarmos um clique do mouse. A mensagem do Unity para isso é
OnMouseDown()
(4). Esse método é chamado toda vez que o GameObject
ao qual esse script está anexado é clicado (com um clique do botão esquerdo do mouse). Nesse caso, incrementamos o hitCount
(5) que, por fim, será salvo pelas várias opções mostradas nesta série de tutoriais.(Consulte
PlayerPrefsExampleSimple.cs
no repositório para ver a versão finalizada).A maneira mais fácil e provavelmente a mais direta de salvar dados no Unity é usando o
PlayerPrefs
integrado. A desvantagem, no entanto, é a usabilidade limitada, pois apenas três tipos de dados são suportados:- string
- float
- inteiro
Outro fato importante sobre eles é que eles salvam dados em texto sem formatação, o que significa que um jogador pode alterar facilmente seu conteúdo. Portanto,
PlayerPrefs
só deve ser usado para coisas como configurações gráficos, nomes de usuário e outros dados que podem ser alterados no jogo e, portanto, não precisam ser seguros.Dependendo do sistema operacional em que o jogo está sendo executado, o
PlayerPrefs
será salvo em locais diferentes. Todos eles estão listados na documentação. O Windows, por exemplo, usa o registro para salvar os dados em HKCU\Software\ExampleCompanyName\ExampleProductName
.O uso do
PlayerPrefs
é principalmente o mesmo que um dicionário. Eles são acessados como key
value
pares / , em que key
é do tipo string
. Cada tipo de dados suportado tem sua própria função:- SetString(chave, valor)
- GetString(key)
- SetFloat(key, value)
- GetFloat(key)
- SetInt(chave, valor)
- GetInt(key)
1 using UnityEngine; 2 3 public class PlayerPrefsExampleSimple : MonoBehaviour 4 { 5 // Resources: 6 // https://docs.unity3d.com/ScriptReference/PlayerPrefs.html 7 8 [private int hitCount = 0; ] 9 10 private const string HitCountKey = "HitCountKey"; // 1 11 12 private void Start() 13 { 14 // Check if the key exists. If not, we never saved the hit count before. 15 if (PlayerPrefs.HasKey(HitCountKey)) // 2 16 { 17 // Read the hit count from the PlayerPrefs. 18 hitCount = PlayerPrefs.GetInt(HitCountKey); // 3 19 } 20 } 21 22 private void OnMouseDown() 23 { 24 hitCount++; 25 26 // Set and save the hit count before ending the game. 27 PlayerPrefs.SetInt(HitCountKey, hitCount); // 4 28 PlayerPrefs.Save(); // 5 29 } 30 31 }
Para o exemplo
PlayerPrefs
, criamos um script denominado PlayerPrefsExampleSimple
com base no HitCountExample
mostrado anteriormente.Além da estrutura básica, também precisamos definir uma chave (1) que será usada para salvar o
hitCount
no PlayerPrefs
. Vamos chamá-lo "HitCountKey"
.Quando o jogo começa, primeiro queremos verificar se já havia uma contagem de acertos salva. O
PlayerPrefs
tem uma função embutida HasKey(hitCountKey)
(2) que nos permite fazer exatamente isso. Se a chave existir, nós a lemos usando GetInt(hitCountKey)
(3) e a salvamos no contador.A segunda parte é salvar dados sempre que eles mudarem. Em cada clique depois de incrementarmos o
hitCount
, temos que chamar SetInt(key, value)
em PlayerPrefs
(4) para definir os novos dados. Observe que isso não salva os dados em disco. Isso só acontece durante OnApplicationQuit()
implicitamente. Podemos escrever explicitamente os dados no disco a qualquer momento para evitar a perda de dados caso o jogo falhe e OnApplicationQuit()
nunca seja chamado. Para escrever os dados no disco, chamamos Save()
(5).(Consulte
PlayerPrefsExampleExtended.cs
no repositório para ver a versão finalizada).Na segunda parte deste tutorial, estenderemos esta versão muito simples para ver maneiras de salvar dados mais complexos dentro
PlayerPrefs
.Em vez de apenas detectar um clique do mouse, o script estendido detectará
Shift+Click
e Ctrl+Click
também.Novamente, para visualizar isso no editor, adicionaremos mais alguns
[SerializeFields]
(1). Substitua o atual (hitCount
) pelo seguinte:1 // 1 2 [private int hitCountUnmodified = 0; ] 3 [private int hitCountShift = 0; ] 4 [private int hitCountControl = 0; ]
Cada tipo de clique será mostrado em seu próprio elemento
Inspector
.O mesmo deve ser feito para as teclas
PlayerPrefs
. Remova oHitCountKey
e adicione três novos elementos (2).1 // 2 2 private const string HitCountKeyUnmodified = "HitCountKeyUnmodified"; 3 private const string HitCountKeyShift = "HitCountKeyShift"; 4 private const string HitCountKeyControl = "HitCountKeyControl";
Há muitas maneiras diferentes de salvar dados mais complexos. Aqui, usaremos três entradas diferentes em
PlayerPrefs
como primeira etapa. Mais tarde, veremos também como salvar dados estruturados que pertencem uns aos outros de uma maneira diferente.Mais um campo que precisamos salvar é o
KeyCode
da tecla que foi pressionada:1 // 3 2 private KeyCode modifier = default;
Ao iniciar a cena, o carregamento dos dados é semelhante ao exemplo anterior, apenas estendido por mais duas chamadas:
1 private void Start() 2 { 3 // Check if the key exists. If not, we never saved the hit count before. 4 if (PlayerPrefs.HasKey(HitCountKeyUnmodified)) // 4 5 { 6 // Read the hit count from the PlayerPrefs. 7 hitCountUnmodified = PlayerPrefs.GetInt(HitCountKeyUnmodified); // 5 8 } 9 if (PlayerPrefs.HasKey(HitCountKeyShift)) // 4 10 { 11 // Read the hit count from the PlayerPrefs. 12 hitCountShift = PlayerPrefs.GetInt(HitCountKeyShift); // 5 13 } 14 if (PlayerPrefs.HasKey(HitCountKeyControl)) // 4 15 { 16 // Read the hit count from the PlayerPrefs. 17 hitCountControl = PlayerPrefs.GetInt(HitCountKeyControl); // 5 18 } 19 }
Como antes, primeiro verificamos se a chave existe no
PlayerPrefs
(4) e, em caso afirmativo, definimos o contador correspondente (5) para seu valor. Isso é bom para um exemplo simples, mas aqui você já pode ver que salvar dados mais complexos levará PlayerPrefs
muito em breve aos seus limites se você não quiser escrever muitos códigos padrão.O Unity oferece uma detecção para cliques do teclado e outras entradas, como um controlador ou o mouse, por meio de uma classe chamada
Input
. Usando GetKey
, podemos verificar se uma tecla específica foi mantida pressionada no momento em que registramos um clique do mouse.No entanto, a documentação nos informa sobre um fato importante:
Nota: os sinalizadores de entrada não são redefinidos até a atualização. Você deve fazer todas as chamadas de entrada no loop de atualização.
Portanto, também precisamos implementar a função
Update()
(6) em que verificamos a chave e a salvamos no modifier
definido anteriormente.As chaves podem ser endereçadas por seu nome como string, mas a maneira segura de fazer isso é usar a classe
KeyCode
, que define todas as chaves necessárias. No nosso caso, seria KeyCode.LeftShift
e KeyCode.LeftControl
.Essas verificações usam
Input.GetKey()
(7) e, se uma das duas for encontrada, ela será salva como modifier
(8). Se nenhum deles tiver sido pressionado (9), acabamos de redefinir modifier
para o default
(10) que usaremos como marcador para um clique do mouse não modificado.1 private void Update() // 6 2 { 3 // Check if a key was pressed. 4 if (Input.GetKey(KeyCode.LeftShift)) // 7 5 { 6 // Set the LeftShift key. 7 modifier = KeyCode.LeftShift; // 8 8 } 9 else if (Input.GetKey(KeyCode.LeftControl)) // 7 10 { 11 // Set the LeftControl key. 12 modifier = KeyCode.LeftControl; // 8 13 } 14 else // 9 15 { 16 // In any other case reset to default and consider it unmodified. 17 modifier = default; // 10 18 } 19 }
O mesmo trio também pode ser encontrado na detecção de cliques:
1 private void OnMouseDown() 2 { 3 // Check if a key was pressed. 4 switch (modifier) 5 { 6 case KeyCode.LeftShift: // 11 7 // Increment the hit count and set it to PlayerPrefs. 8 hitCountShift++; // 12 9 PlayerPrefs.SetInt(HitCountKeyShift, hitCountShift); // 15 10 break; 11 case KeyCode.LeftControl: // 11 12 // Increment the hit count and set it to PlayerPrefs. 13 hitCountControl++; // 14 PlayerPrefs.SetInt(HitCountKeyControl, hitCountControl); // 15 15 break; 16 default: // 13 17 // Increment the hit count and set it to PlayerPrefs. 18 hitCountUnmodified++; // 14 19 PlayerPrefs.SetInt(HitCountKeyUnmodified, hitCountUnmodified); // 15 20 break; 21 } 22 23 // Persist the data to disk. 24 PlayerPrefs.Save(); // 16 25 }
Primeiro, verificamos se um desses dois foi mantido pressionado enquanto o clique aconteceu (11) e, em caso afirmativo, incrementamos o contador de acertos correspondente (12). Caso contrário,13, o contador
unmodfied
deverá ser incrementado (14).Finalmente, precisamos definir cada um desses três contadores individualmente (15) via
PlayerPrefs.SetInt()
usando as três chaves que definimos anteriormente.Como no exemplo anterior, também chamamos
Save()
(16) no final para garantir que os dados não sejam perdidos se o jogo não terminar normalmente.Ao voltar para o editor do Unity, o script na cápsula agora deve ficar assim:
(Consulte
PlayerPrefsExampleJson.cs
no repositório para ver a versão finalizada).Nas duas seções anteriores, vimos como lidar com dois exemplos simples de persistência de dados em
PlayerPrefs
. E se eles se tornarem mais complexos do que isso? E se você quiser estruturar e agrupar dados?Uma abordagem possível seria usar o fato de que
PlayerPrefs
pode conter um string
e salvar um JSON
lá.Primeiro, precisamos descobrir como realmente transformar nossos dados em JSON. A estrutura .NET e a estrutura
UnityEngine
oferecem um serializador e um desserializador JSON para fazer esse trabalho para nós. Ambos se comportam de forma muito semelhante, mas usaremos o próprio JsonUtility
do Unity, que temmelhor desempenho no Unity do que outras soluções JSON semelhantes.Para transformar dados em JSON, primeiro precisamos criar um objeto container. Isso tem algumas restrições:
Internamente, esse método usa o serializador Unity. Portanto, o objeto que você passa deve ser suportado pelo serializador. Deve ser um MonoBehaviour, ScriptableObject ou classe/estrutura simples com o atributo Serializable aplicado. Os tipos de campos que você deseja incluir devem ser suportados pelo serializador; campos sem suporte serão ignorados, assim como campos privados, campos estáticos e campos com o atributo NonSerialized aplicado.
Em nosso caso, como estamos salvando apenas tipos de dados simples (int) por enquanto, tudo bem. Podemos definir uma nova classe (1) e chamá-la
HitCount
:1 // 1 2 private class HitCount 3 { 4 public int Unmodified; 5 public int Shift; 6 public int Control; 7 }
Manteremos as lojas do editor Unity iguais (2):
1 // 2 2 [private int hitCountUnmodified = 0; ] 3 [private int hitCountShift = 0; ] 4 [private int hitCountControl = 0; ]
Todos esses serão eventualmente salvos no mesmo campo
PlayerPrefs
, o que significa que só precisamos de uma chave (3):1 // 3 2 private const string HitCountKey = "HitCountKeyJson";
Como antes, o
modifier
indicará qual modificador foi usado:1 // 4 2 private KeyCode modifier = default;
Em
Start()
, precisamos então ler o JSON. Como antes, verificamos se a chavePlayerPrefs
existe (5) e depois lemos os dados, desta vez usando GetString()
(em vez de GetInt()
antes).A transformação desse JSON no objeto real é então feita usando
JsonUtility.FromJson()
(6), que usa a string como um argumento. É uma função genérica e precisamos fornecer as informações sobre qual objeto esse JSON deve representar — neste caso, HitCount
.Se o JSON puder ser lido e transformado com sucesso, podemos definir os campos de contagem de acertos (7) para seus três valores.
1 private void Start() 2 { 3 // 5 4 // Check if the key exists. If not, we never saved to it. 5 if (PlayerPrefs.HasKey(HitCountKey)) 6 { 7 // 6 8 var jsonString = PlayerPrefs.GetString(HitCountKey); 9 var hitCount = JsonUtility.FromJson<HitCount>(jsonString); 10 11 // 7 12 if (hitCount != null) 13 { 14 hitCountUnmodified = hitCount.Unmodified; 15 hitCountShift = hitCount.Shift; 16 hitCountControl = hitCount.Control; 17 } 18 } 19 }
A detecção da tecla que foi pressionada é idêntica ao exemplo estendido, pois não envolve carregar ou salvar nenhum dado, mas é apenas uma verificação da tecla durante
Update()
:1 private void Update() // 8 2 { 3 // Check if a key was pressed. 4 if (Input.GetKey(KeyCode.LeftShift)) // 9 5 { 6 // Set the LeftShift key. 7 modifier = KeyCode.LeftShift; // 10 8 } 9 else if (Input.GetKey(KeyCode.LeftControl)) // 9 10 { 11 // Set the LeftControl key. 12 modifier = KeyCode.LeftControl; // 10 13 } 14 else // 11 15 { 16 // In any other case reset to default and consider it unmodified. 17 modifier = default; // 12 18 } 19 }
De maneira muito semelhante,
OnMouseDown()
precisa salvar os dados sempre que forem alterados.1 private void OnMouseDown() 2 { 3 // Check if a key was pressed. 4 switch (modifier) 5 { 6 case KeyCode.LeftShift: // 13 7 // Increment the hit count and set it to PlayerPrefs. 8 hitCountShift++; // 14 9 break; 10 case KeyCode.LeftControl: // 13 11 // Increment the hit count and set it to PlayerPrefs. 12 hitCountControl++; // 14 13 break; 14 default: // 15 15 // Increment the hit count and set it to PlayerPrefs. 16 hitCountUnmodified++; // 16 17 break; 18 } 19 20 // 17 21 var updatedCount = new HitCount 22 { 23 Unmodified = hitCountUnmodified, 24 Shift = hitCountShift, 25 Control = hitCountControl, 26 }; 27 28 // 18 29 var jsonString = JsonUtility.ToJson(updatedCount); 30 PlayerPrefs.SetString(HitCountKey, jsonString); 31 PlayerPrefs.Save(); 32 }
Em comparação com antes, você vê que verificar a chave e aumentar o contador (13 - 16) permanece praticamente inalterado, exceto pela parte de salvar que agora está um pouco diferente.
Primeiro, precisamos criar um novo 17
HitCount
( ) e atribuir as três contagens. UsandoJsonUtility.ToJson()
, podemos (18) criar uma string JSON a partir desse objeto e defini-la usando PlayerPrefs
.Lembre-se de também chamar
Save()
aqui para garantir que os dados não possam ser perdidos caso o jogo falhe sem que seja possível chamar OnApplicationQuit()
.Execute o jogo e, depois de clicar na capsula algumas vezes com ou sem Shift e Controle, dê uma olhada no resultado. A captura de tela a seguir mostra o registro do Windows, que é onde os
PlayerPrefs
são salvos.O local ao usar nosso projeto de exemplo é
HKEY_CURRENT_USER\SOFTWARE\Unity\UnityEditor\MongoDB Inc.\UnityPersistenceExample
e, como você pode ver, nosso JSON está bem aqui, salvo em texto sem formatação. Essa também é uma das grandes desvantagens a serem lembradas ao usar PlayerPrefs
: os dados não estão seguros e podem ser editados facilmente quando salvos em texto sem formatação. Fique atento ao nosso futuro tutorial sobre criptografia, que é uma opção para melhorar a segurança dos seus dados.Neste tutorial, vimos como salvar e carregar dados usando
PlayerPrefs
. Eles são muito simples e fáceis de usar e são uma ótima opção para alguns pontos de dados simples. Se ficar um pouco mais complexo, você poderá salvar dados usando vários campos ou agrupando-os em um objeto que pode ser serializado usando JSON
.O que acontece se você quiser persistir vários objetos da mesma classe? Ou várias turmas? Talvez com relacionamentos entre eles? E se a estrutura desses objetos mudar?
Como você pode ver,
PlayerPrefs
atinge seus limites muito rápido - por mais fáceis de usar e limitados que sejam.Em tutoriais futuros, exploraremos outras opções para persistir dados no Unity e como elas podem resolver algumas ou todas as questões acima.