Salvando dados no Unity3D usando arquivos
Avalie esse Tutorial
(Parte 2 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.
Na parte 1 desta série, exploramos a solução da própria Unity:
PlayerPrefs
. Desta vez, examinamos uma das maneiras de usar a estrutura .NET subjacente salvando arquivos. Aqui está uma visão geral da série completa:- Parte 1: PlayerPrefs
- Parte 2: Arquivos (este tutorial)
- Parte 3: BinaryReader e BinaryWriter (em breve)
- Parte 4: SQL
- Parte 5: Realm Unity SDK
- Parte 6: Comparação de todas essas opções
Como a Parte 1, este tutorial também pode ser encontrado no https://github.com/realm/unity-examples repositório na comparação de persistência ramificação.
Cada parte é classificada em uma pasta. Os três scripts que veremos neste tutorial estão na subpasta
File
. Mas, primeiro, vamos dar uma olhada no jogo de exemplo em si e no que temos de preparar no Unity antes de começarmos a codificação propriamente dita.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
.A primeira coisa que precisamos adicionar é um contador para os cliques na cápsula (1). Adicione um
[SerilizeField]
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
FileExampleSimple.cs
no repositório para ver a versão finalizada).Fornece métodos estáticos para a criação, cópia, exclusão, movimentação e abertura de um único arquivo e auxilia na criação de objetos FileStream.
Além disso, a classe
File
também é utilizada para manipular o próprio arquivo, lendo e escrevendo dados. Além disso, oferece maneiras de ler metadados de um arquivo, como hora da criação.Ao trabalhar com um arquivo, você também pode usar várias opções para alterar
FileMode
ou FileAccess.
O
FileStream
mencionado na documentação é outra abordagem para trabalhar com esses arquivos, fornecendo opções adicionais. Neste tutorial, usaremos apenas a classeFile
simples.Vamos dar uma olhada no que temos que alterar no exemplo apresentado na seção anterior para salvar os dados usando
File
:Primeiro definimos um nome para o arquivo que conterá os dados (1). Se nenhum caminho adicional for fornecido, o arquivo será salvo apenas na pasta do projeto ao executar o jogo no editor Unity ou na pasta do jogo ao executar uma compilação. Isso é bom para o exemplo.
Sempre que clicarmos na cápsula (2) e aumentarmos a contagem de acertos (3), precisaremos salvar essa alteração. Usando
File.WriteAllText()
(4), o arquivo será aberto, os dados serão salvos e ele será fechado imediatamente. Além do nome do arquivo, essa função espera o conteúdo como uma string. Portanto, temos que transformar hitCount
chamando ToString()
antes de passá-lo adiante.Da próxima vez que iniciarmos o jogo (5), queremos carregar os dados salvos anteriormente. Primeiro, verificamos se o arquivo já existe (6). Se não existir, nunca salvamos antes e podemos apenas manter o valor padrão para
hitCount
. Se o arquivo existir, usaremos ReadAllText()
para obter esses dados (7). Como isso é uma string novamente, precisamos converter aqui também usando Int32.Parse()
(8). Observe que isso significa que precisamos ter certeza do que lemos. Se a estrutura do arquivo for alterada ou o reprodutor edita, isso poderá causar problemas durante a análise do arquivo.Vamos tentar estender esse exemplo simples na próxima seção.
(Consulte
FileExampleExtended.cs
no repositório para ver a versão finalizada).A seção anterior mostrou o exemplo mais simples, usando apenas uma variável que precisa ser salva. E se quisermos economizar mais do que isso?
Dependendo do que precisa ser salvo, há várias abordagens diferentes. Você pode usar vários arquivos ou escrever várias linhas dentro do mesmo arquivo. Este último deve ser mostrado nesta seção, estendendo o jogo para reconhecer as teclas modificadoras. Queremos detectar cliques normais, Shift+Clique e Control+Clique.
Primeiro, atualize as contagens de acertos para que possamos salvar três deles:
Também queremos usar um nome de arquivo diferente para que possamos ver as duas versões uma ao lado da outra:
O último campo que precisamos definir é a tecla pressionada:
A primeira coisa que precisamos fazer é verificar se uma tecla foi pressionada e qual chave era.O Unity oferece uma maneira fácil de fazer isso usando a função
Input
da turma GetKey
. Ele verifica se a chave fornecida foi pressionada ou não. Você pode passar a string para a chave ou, para ter um pouco mais de segurança, é só usar o enum KeyCode
. No entanto , não podemos usar isso noOnMouseClick()
ao detectar o clique do mouse: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.
Adicione um novo método chamado
Update()
(1) que é chamado em cada quadro. Aqui precisamos verificar se a teclaShift
ou Control
foi pressionada (2) e, em caso afirmativo, salvar a tecla correspondente em modifier
(3). Caso nenhuma dessas teclas tenha sido pressionada (4), nós a consideramos não modificada e redefinimos modifier
ao seu default
(5).Agora, para salvar os dados quando um clique acontece:
Sempre que um clique do mouse é detectado na capsula (6), podemos realizar uma verificação semelhante ao que aconteceu em
Update()
, exceto que usamos modifier
em vez de Input.GetKey()
aqui.Verifique se
modifier
foi definido como KeyCode.LeftShift
ou KeyCode.LeftControl
(7) e, em caso afirmativo, aumente a contagem de ocorrências correspondente (8). Se nenhum modificador foi usado (9), aumente o hitCountUnmodified
.Como visto na última seção, precisamos criar uma string que possa ser salva no arquivo. Há uma segunda função em
File
que aceita uma array de string e então salva cada entrada em uma linha: WriteAllLines()
.Sabendo disso, criamos um array contendo as três contagens de acertos (11) e passamos este para
File.WriteAllLines()
.Inicie o jogo e clique na cápsula usando Shift e Control. Você deve ver os três contadores no Inspetor.
Depois de interromper o jogo e, portanto, salvar os dados, um novo arquivo
hitCountFileExtended.txt
deve existir na pasta do seu projeto. Dê uma olhada nele. Deve parecer algo assim:Por último, mas não menos importante, vamos ver como carregar o arquivo novamente ao iniciar o jogo:
Primeiro, verificamos se o arquivo existe (12). Se já salvamos dados antes, esse deve ser o caso. Se existir, lemos os dados. Semelhante a escrever com
WriteAllLines()
, usamos ReadAllLines
(13) para criar uma array de string onde cada entrada representa uma linha no arquivo.Esperamos que haja três linhas, então devemos esperar que a matriz de string tenha três entradas (14).
Usando esse conhecimento, podemos então atribuir as três entradas da matriz às contagens de acertos correspondentes (15).
Desde que todos os dados salvos nessas linhas estejam juntos, o arquivo pode ser uma opção. Se você tiver várias propriedades diferentes, poderá criar vários arquivos. Como alternativa, você pode salvar todos os dados no mesmo arquivo usando um pouco de estrutura. Observe, no entanto, que os números não serão associados às propriedades. Se a estrutura do objeto mudar, precisaremos migrar o arquivo também e levar isso em consideração na próxima vez que abrirmos e lermos o arquivo.
Outra abordagem possível para estruturar seus dados será mostrada na próxima seção usando JSON.
(Consulte
FileExampleJson.cs
no repositório para ver a versão finalizada).JSON é uma abordagem muito comum ao salvar dados estruturados. É fácil de usar e existem frameworks para quase todas as linguagens. O framework .NET fornece um
JsonSerializer
. O Unity tem sua própria versão dele: JsonUtility
.Como você pode ver na documentação, a funcionalidade se resume a esses três métodos:
- FromJson: Crie um objeto a partir de sua representação JSON.
- FromJsonOverwrite: Substitui dados em um objeto lendo a partir de sua representação JSON.
- ToJson: Gere uma representação JSON dos campos públicos de um objeto.
O
JsonUtility
transforma JSON em objetos e de volta. Portanto, nossa primeira alteração na seção anterior é definir tal objeto com campos públicos:A classe em si pode ser
private
e apenas ser adicionada dentro da classeFileExampleJson
, mas seus campos precisam ser públicos.Como antes, usamos um arquivo diferente para salvar esses dados. Atualizar o nome do arquivo para:
Ao salvar os dados, usaremos o mesmo método
Update()
de antes para detectar qual tecla foi pressionada.A primeira parte de
OnMouseDown()
(1) também pode permanecer a mesma, pois essa parte só aumenta a contagem de acertos dependendo do modificador usado.No entanto, precisamos atualizar a segunda parte. Em vez de uma array de string, criamos um novo objeto
HitCount
e definimos os três campos públicos para os valores dos contadores de acessos (2).Usando
JsonUtility.ToJson()
, podemos transformar este objeto em uma string (3). Se você passar em true
para o segundo parâmetro opcional, prettyPrint
, a string será formatada de forma legível.Finalmente, como em
FileExampleSimple.cs
, acabamos de usar WriteAllText()
porque estamos salvando apenas uma string, não uma array (4).Então, quando o jogo começar, precisamos ler os dados novamente na contagem de acertos:
Verificamos se o arquivo existe primeiro (5). Caso isso aconteça, salvamos os dados antes e podemos continuar lendo-os.
Usando
ReadAllText
, lemos a string do arquivo e a transformamos, por meio de JsonUtility.FromJson<>()
, em um objeto do tipo HitCount
(6).Se isso aconteceu com sucesso (7), podemos atribuir as três propriedades à contagem de ocorrências correspondente (8).
Ao executar o jogo, você verá que, no editor, ele parece idêntico à seção anterior, pois estamos usando os mesmos três contadores. Se você abrir o arquivo
hitCountFileJson.txt
, verá os três contadores em um JSON bem formatado.Observe que os dados são salvos em texto simples. Em um tutorial futuro, veremos a criptografia e como melhorar a segurança de seus dados.
Neste tutorial, aprendemos a utilizar
File
para salvar dados. JsonUtility
ajuda a estruturar esses dados. Eles são simples e fáceis de usar, e não é necessário muito código.Quais são as desvantagens, embora?
Em primeiro lugar, abrimos, gravamos e salvamos o arquivo toda vez que a cápsula é clicada. Embora não seja um problema neste caso e certamente seja aplicável a alguns jogos, isso não funcionará muito bem quando muitas operações de salvamento forem feitas.
Além disso, os dados são salvos em texto simples e podem ser facilmente editados pelo jogador.
Quanto mais complexos forem seus dados, mais complexa será a manutenção dessa abordagem. E se a estrutura do objeto
HitCount
mudar? Você terá que alterar a conta para isso ao carregar uma versão mais antiga do JSON. As migrações são necessárias.Nos tutoriais a seguir, veremos (entre outras coisas) como os bancos de dados podem facilitar muito esse trabalho e resolver os problemas que enfrentamos aqui.