Salvando dados no Unity3D usando SQLite
(Parte 4 da série de comparação de persistência)
Nossa jornada de explorar as opções oferecidas para uso quando se trata de persistência no Unity levará, nesta parte, aos bancos de dados. Mais especificamente, ao SQLite.
O SQLite é um banco de dados baseado em C e usado em muitas áreas. Ele existe há muito tempo e também encontrou seu caminho no mundo do Unity. Durante esta série de tutoriais, vimos opções como
PlayerPrefs
no Unity e, por outro lado, File
e BinaryWriter
/BinaryReader
fornecidas pelo framework .NET subjacente.Aqui está uma visão geral da série completa:
- Parte 1: PlayerPrefs
- Parte 2: Arquivos
- Parte 3: BinaryReader e BinaryWriter
- Parte 4: SQL (este tutorial)
- Parte 5: Realm Unity SDK (em breve)
- Parte 6: Comparação de todas essas opções
Semelhante às partes anteriores, este tutorial também pode ser encontrado em nosso repositório de exemplos do Unity na ramificação persistência-comparação.
Cada parte é classificada em uma pasta. Os três scripts que veremos neste tutorial estão na subpasta
SQLite
. 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
.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
[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
SqliteExampleSimple.cs
no repositório para ver a versão finalizada).Agora vamos garantir que nossa contagem de acertos seja persistente para que possamos continuar jogando da próxima vez que iniciarmos o jogo.
O SQLite não está incluído por padrão em um novo projeto Unity e também não está disponível diretamente por meio do gerenciador de pacotes Unity. Temos que instalar dois componentes para começar a usá-lo.
Primeiro, acesse https://sqlite.org/download.html e escolha os
Precompiled Binaries
para seu sistema operacional. Descompacte-o e adicione os dois arquivos —sqlite3.def
e sqlite3.dll
— à pasta Plugin
em seu projeto Unity.Em seguida, abra um explorador de arquivos no diretório de instalação do Unity Hub e vá para o seguinte subdiretório:
1 Unity/Hub/Editor/2021.2.11f1/Editor/Data/MonoBleedingEdge/lib/mono/unity
Lá, você encontrará o arquivo
Mono.Data.Sqlite.dll
que também precisa ser movido para a pasta Plugins
em seu projeto Unity. O resultado ao voltar para o Editor deve ser parecido com este:Agora que os preparativos terminaram, queremos adicionar nosso primeiro script à cápsula. Semelhante ao
HitCountExample.cs
, crie um novo C# script
e nomeie-o SqliteExampleSimple
.Ao abri-lo, a primeira coisa que queremos fazer é importar SQLite adicionando
using Mono.Data.Sqlite;
e using System.Data;
no topo do arquivo (1).A seguir, veremos como salvar sempre que a contagem de ocorrências for alterada, o que acontece durante
OnMouseDown()
. Primeiro precisamos abrir uma conexão com o banco de dados. Isso é oferecido pela biblioteca SQLite por meio da classe IDbConnection
(2), que representa uma conexão aberta com o banco de dados. Como precisaremos de uma conexão para carregar os dados mais tarde, extrairemos a abertura de uma conexão de banco de dados em outra função e a chamaremos private IDbConnection CreateAndOpenDatabase()
(3).Ali, primeiro definimos um nome para nosso arquivo de banco de dados.Por enquanto , vamos chamá-lo
MyDatabase
. Consequentemente, o URI deve ser "URI=file:MyDatabase.sqlite"
(4). Então podemos criar uma conexão com este banco de dados utilizando new SqliteConnection(dbUri)
(5) e abri-lo com dbConnection.Open()
(6).1 using Mono.Data.Sqlite; // 1 2 using System.Data; // 1 3 using UnityEngine; 4 5 public class SqliteExampleSimple : MonoBehaviour 6 { 7 // Resources: 8 // https://www.mono-project.com/docs/database-access/providers/sqlite/ 9 10 [private int hitCount = 0; ] 11 12 void Start() // 13 13 { 14 // Read all values from the table. 15 IDbConnection dbConnection = CreateAndOpenDatabase(); // 14 16 IDbCommand dbCommandReadValues = dbConnection.CreateCommand(); // 15 17 dbCommandReadValues.CommandText = "SELECT * FROM HitCountTableSimple"; // 16 18 IDataReader dataReader = dbCommandReadValues.ExecuteReader(); // 17 19 20 while (dataReader.Read()) // 18 21 { 22 // The `id` has index 0, our `hits` have the index 1. 23 hitCount = dataReader.GetInt32(1); // 19 24 } 25 26 // Remember to always close the connection at the end. 27 dbConnection.Close(); // 20 28 } 29 30 private void OnMouseDown() 31 { 32 hitCount++; 33 34 // Insert hits into the table. 35 IDbConnection dbConnection = CreateAndOpenDatabase(); // 2 36 IDbCommand dbCommandInsertValue = dbConnection.CreateCommand(); // 9 37 dbCommandInsertValue.CommandText = "INSERT OR REPLACE INTO HitCountTableSimple (id, hits) VALUES (0, " + hitCount + ")"; // 10 38 dbCommandInsertValue.ExecuteNonQuery(); // 11 39 40 // Remember to always close the connection at the end. 41 dbConnection.Close(); // 12 42 } 43 44 private IDbConnection CreateAndOpenDatabase() // 3 45 { 46 // Open a connection to the database. 47 string dbUri = "URI=file:MyDatabase.sqlite"; // 4 48 IDbConnection dbConnection = new SqliteConnection(dbUri); // 5 49 dbConnection.Open(); // 6 50 51 // Create a table for the hit count in the database if it does not exist yet. 52 IDbCommand dbCommandCreateTable = dbConnection.CreateCommand(); // 6 53 dbCommandCreateTable.CommandText = "CREATE TABLE IF NOT EXISTS HitCountTableSimple (id INTEGER PRIMARY KEY, hits INTEGER )"; // 7 54 dbCommandCreateTable.ExecuteReader(); // 8 55 56 return dbConnection; 57 } 58 }
Agora podemos trabalhar com este banco de dados SQLite. No entanto, antes de podermos realmente adicionar dados a ele, precisamos configurar uma estrutura. Isso significa criar e definir tabelas, que é a maneira como a maioria dos bancos de dados são organizados. A captura de tela a seguir mostra o estado final que criaremos neste exemplo.
Ao acessar ou modificar o banco de dados, usamos
IDbCommand
(6), que representa uma declaração SQL que pode ser executada em um banco de dados.Vamos criar uma nova tabela e definir algumas colunas usando o seguinte comando (7):
1 "CREATE TABLE IF NOT EXISTS HitCountTableSimple (id INTEGER PRIMARY KEY, hits INTEGER )"
Afinal, o que significa essa declaração? Primeiro, precisamos declarar o que queremos fazer, que é
CREATE TABLE IF NOT EXISTS
. Em seguida, precisamos nomear esta tabela, que será o mesmo script em que estamos trabalhando agora: HitCountTableSimple
.Por último, mas não menos importante, precisamos definir como essa nova tabela deve ficar. Isso é feito nomeando todas as colunas como uma tupla:
(id INTEGER PRIMARY KEY, hits INTEGER )
. A primeira define uma coluna id
do tipo INTEGER
que é a nossa PRIMARY KEY
. A segunda define uma coluna hits
do tipo INTEGER
.Depois de atribuir essa instrução como
CommandText
, precisamos chamar ExecuteReader()
(8) em dbCommandCreateTable
para executá-lo.Agora, de volta a
OnMouseClicked()
. Com o dbConnection
criado, agora podemos definir outro IDbCommand
(9) para modificar a nova tabela que acabamos de criar e adicionar alguns dados. Desta vez, o CommandText
(10) será:1 "INSERT OR REPLACE INTO HitCountTableSimple (id, hits) VALUES (0, " + hitCount + ")"
Vamos decifrar isso também:
INSERT OR REPLACE INTO
adiciona uma nova variável a uma tabela ou a atualiza, se ela já existir. A seguir está o nome da tabela que queremos inserir, HitCountTableSimple
. Isto é seguido por uma tupla de colunas que gostaríamos de alterar, (id, hits)
. A instrução VALUES (0, " + hitCount + ")
define então valores que devem ser inseridos, também como uma tupla. Nesse caso, apenas escolhemos 0
para a chave e usamos qualquer que seja o hitCount
atual como valor.Em vez de criar a tabela, executamos este comando chamando
ExecuteNonQuery()
(11) nela.A diferença pode ser definida da seguinte forma:
O ExecuteReader é usado para qualquer conjunto de resultados com várias linhas/colunas (por exemplo, SELECT col1, col2 de alguma tabela). ExecuteNonQuery normalmente é usado para instruções SQL sem resultados (por exemplo, UPDATE, INSERT etc.).
Tudo o que resta a fazer é
Close()
corretamente (12) o banco de dados.Como podemos realmente verificar se isso funcionou antes de continuarmos a ler os valores do banco de dados novamente? Bem, a maneira mais fácil seria apenas procurar no banco de dados. Existem muitas ferramentas disponíveis para conseguir isso. Uma das opções de código aberto seria https://sqlitebrowser.org/.
Depois de baixá-lo e instalá-lo, tudo que você precisa fazer é
File -> Open Database
, navegar até seu projeto do Unity e selecionar o arquivo MyDatabase.sqlite
. Se você então escolher o Table
HitCountTableSimple
, o resultado deve ser semelhante a este:Vá em frente e execute o jogo. Clique algumas vezes na cápsula e verifique a alteração no Inspector. Quando você voltar ao navegador do banco de dados e clicar em Atualizar, o mesmo número deverá aparecer na coluna
value
da tabela.Na próxima vez que iniciarmos o jogo, queremos carregar essa contagem de visitas do banco de dados novamente. Usamos a função
Start()
(13) já que ela só precisa ser feita quando a cena carrega. Como antes, precisamos obter um controle do banco de dados com um IDbConnection
(14) e criar um novo IDbCommand
(15) para ler os dados. Como há apenas uma tabela e um valor, é bastante simples por enquanto. Podemos apenas ler all data
usando:1 "SELECT * FROM HitCountTableSimple"
Nesse caso,
SELECT
significa read the following values
, seguido por um *
que indica a leitura de todos os dados. A palavra-chave FROM
então especifica a tabela que deve ser lida, que é novamente HitCountTableSimple
. Finalmente, executamos este comando usando ExecuteReader()
(17) pois queremos dados de volta. Esses dados são salvos em um IDataReader
, da documentação:Fornece um meio de ler um ou mais fluxos somente de encaminhamento de conjuntos de resultados obtidos pela execução de um comando em uma fonte de dados e é implementado por provedores de dados .NET que acessam bancos de dados relacionais.
IDataReader
aborda seu conteúdo de forma de índice, onde a ordem corresponde a uma das colunas na tabela SQL. Então, em nosso caso, id
tem índice 0 e hitCount
tem índice 1. A forma como esses dados são lidos é linha por linha. Cada vez que chamamos dataReader.Read()
(18), lemos outra linha da tabela. Como sabemos que há apenas uma linha na tabela, podemos apenas atribuir o value
dessa linha ao hitCount
usando seu índice 1. O value
é do tipo INTEGER
então precisamos usar GetInt32(1)
para lê-lo e especificar o índice do campo que queremos ler como parâmetro, id
sendo 0
e value
sendo 1
.Como antes, no final queremos
Close()
adequadamente o banco de dados (20).Quando você reiniciar o jogo novamente, agora você deve ver um valor inicial para
hitCount
que é lido do banco de dados.(Consulte
SqliteExampleExtended.cs
no repositório para ver a versão finalizada).Na seção anterior, vimos a versão mais simples de um exemplo de banco de dados que você pode imaginar. Uma tabela, uma linha e apenas um valor no qual estamos interessados. Embora um banco de dados como o SQLite possa lidar com qualquer tipo de complexidade, queremos poder compará-lo com as partes anteriores desta série de tutoriais e, portanto, analisaremos o mesmo
Extended example
, usando três contagens de ocorrências em vez de uma e usando chaves modificadoras para identificá-las: Shift
e Control
.Vamos começar criando um novo script
SqliteExampleExtended.cs
e anexá-lo à capsula. Copie o código de SqliteExampleSimple
e aplique as seguintes alterações a ele. Primeiro, defina as três contagens de acertos:1 [private int hitCountUnmodified = 0; ] 2 [private int hitCountShift = 0; ] 3 [private int hitCountControl = 0; ]
A detecção de qual tecla está pressionada (além do clique do mouse) pode ser feita usando a classe
Input
que faz parte do Unity SDK. Chamando Input.GetKey()
, podemos verificar se uma determinada tecla foi pressionada. Isso deve ser feito durante Update()
, que é a função do Unity chamada a cada quadro. A razão para isso é indicada na documentação: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.
A tecla que foi pressionada precisa ser lembrada ao receber o evento
OnMouseDown()
. Portanto, precisamos adicionar um campo privado para salvá-lo assim:1 private KeyCode modifier = default;
Agora, a função
Update()
pode ser assim:1 private void Update() 2 { 3 // Check if a key was pressed. 4 if (Input.GetKey(KeyCode.LeftShift)) // 1 5 { 6 // Set the LeftShift key. 7 modifier = KeyCode.LeftShift; // 2 8 } 9 else if (Input.GetKey(KeyCode.LeftControl)) // 1 10 { 11 // Set the LeftControl key. 12 modifier = KeyCode.LeftControl; // 2 13 } 14 else // 3 15 { 16 // In any other case reset to default and consider it unmodified. 17 modifier = default; // 4 18 } 19 }
Primeiro, verificamos se a tecla
LeftShift
ou LeftControl
foi pressionada (1) e, em caso afirmativo, salvamos o KeyCode
correspondente em modifier
. Observe que você pode usar o nome string
da chave que está procurando ou o enum KeyCode
, mais seguro para digitação.Caso nenhuma dessas duas teclas tenha sido pressionada (3), definimos isso como o estado
unmodified
e apenas configuramos modifier
de volta para seu default
(4).Antes de prosseguirmos para
OnMouseClicked()
, você pode se perguntar que alterações precisamos fazer na estrutura do banco de dados criada por private IDbConnection CreateAndOpenDatabase()
. Acontece que, na verdade, não precisamos mudar nada. Apenas usaremos o id
introduzido na seção anterior e salvaremos o KeyCode
(que é um número inteiro) nele.Para podermos comparar as duas versões posteriormente, mudaremos o nome da tabela e a chamaremos de
HitCountTableExtended
:1 dbCommandCreateTable.CommandText = "CREATE TABLE IF NOT EXISTS HitCountTableExtended (id INTEGER PRIMARY KEY, hits INTEGER)";
Agora, vejamos como a detecção de cliques do mouse precisa ser modificada para levar em conta essas chaves:
1 private void OnMouseDown() 2 { 3 var hitCount = 0; 4 switch (modifier) // 1 5 { 6 case KeyCode.LeftShift: 7 // Increment the hit count and set it to PlayerPrefs. 8 hitCount = ++hitCountShift; // 2 9 break; 10 case KeyCode.LeftControl: 11 // Increment the hit count and set it to PlayerPrefs. 12 hitCount = ++hitCountControl; // 2 13 break; 14 default: 15 // Increment the hit count and set it to PlayerPrefs. 16 hitCount = ++hitCountUnmodified; // 2 17 break; 18 } 19 20 // Insert a value into the table. 21 IDbConnection dbConnection = CreateAndOpenDatabase(); 22 IDbCommand dbCommandInsertValue = dbConnection.CreateCommand(); 23 dbCommandInsertValue.CommandText = "INSERT OR REPLACE INTO HitCountTableExtended (id, hits) VALUES (" + (int)modifier + ", " + hitCount + ")"; 24 dbCommandInsertValue.ExecuteNonQuery(); 25 26 // Remember to always close the connection at the end. 27 dbConnection.Close(); 28 }
Primeiro, precisamos verificar qual modificador foi usado no último quadro (1). Dependendo disso, incrementamos a contagem de ocorrências correspondente e a atribuímos à variável local
hitCount
(2). Como antes, contamos qualquer outra chave além LeftShift
de e LeftControl
como unmodified
.Agora, tudo o que precisamos alterar na segunda parte desta função é o
id
que definimos estaticamente como 0
antes e, em vez disso, usar KeyCode
. A declaração SQL atualizada deve ter a seguinte aparência:1 "INSERT OR REPLACE INTO HitCountTableExtended (id, hits) VALUES (" + (int)modifier + ", " + hitCount + ")"
A tupla
VALUES
agora precisa definir (int)modifier
(observe que enum
precisa ser convertido em int
) e hitCount
como seus dois valores.Como antes, podemos iniciar o jogo e ver a parte de salvamento em ação primeiro. Clique algumas vezes até que o Inspector mostre alguns números para as três contagens de acertos:
Agora, vamos abrir o navegador de banco de dados novamente e, desta vez, escolher o
HitCountTableExtended
no menu suspenso:Como você pode ver, há três linhas, com o
value
sendo igual às contagens de ocorrências que você vê no Inspetor. Na coluna id
, vemos as três entradas para KeyCode.None
(0), KeyCode.LeftShift
(304), e KeyCode.LeftControl
(306).Finalmente, vamos ler esses valores do banco de dados ao reiniciar o jogo.
1 void Start() 2 { 3 // Read all values from the table. 4 IDbConnection dbConnection = CreateAndOpenDatabase(); // 1 5 IDbCommand dbCommandReadValues = dbConnection.CreateCommand(); // 2 6 dbCommandReadValues.CommandText = "SELECT * FROM HitCountTableExtended"; // 3 7 IDataReader dataReader = dbCommandReadValues.ExecuteReader(); // 4 8 9 while (dataReader.Read()) // 5 10 { 11 // The `id` has index 0, our `value` has the index 1. 12 var id = dataReader.GetInt32(0); // 6 13 var hits = dataReader.GetInt32(1); // 7 14 if (id == (int)KeyCode.LeftShift) // 8 15 { 16 hitCountShift = hits; // 9 17 } 18 else if (id == (int)KeyCode.LeftControl) // 8 19 { 20 hitCountControl = hits; // 9 21 } 22 else 23 { 24 hitCountUnmodified = hits; // 9 25 } 26 } 27 28 // Remember to always close the connection at the end. 29 dbConnection.Close(); 30 }
A primeira parte funciona essencialmente inalterada criando um
IDbConnection
(1) e um IDbCommand
(2) e então lendo todas as linhas novamente com SELECT *
(3) mas desta vez de HitCountTableExtended
, finalizado executando o comando com ExecuteReader()
(4).Para a próxima parte, agora precisamos ler cada linha (5) e verificar a qual
KeyCode
ela pertence. Pegamos o id
do índice 0
(6) e o hits
do índice 1
(7) como antes. Em seguida, verificamos o id
contra o KeyCode
(8) e o atribuímos ao hitCount
correspondente (9).Agora reinicie o jogo e experimente!
O SQLite é uma das opções quando se trata de persistência. Se você leu os tutoriais anteriores, percebeu que usá-lo pode, a princípio, parecer um pouco mais complicado do que o simples
PlayerPrefs
. Você precisa aprender uma "linguagem" adicional para poder se comunicar com seu banco de dados. E, como a natureza do SQL não é o formato mais fácil de ler, pode parecer um pouco intimidador no início. Mas o mundo dos bancos de dados oferece muito mais do que pode ser mostrado em um breve tutorial como este!Uma das desvantagens dos arquivos simples ou
PlayerPrefs
que vimos foi ter os dados de forma estruturada, especialmente quando eles se tornam mais complicados ou quando é necessário desenhar relacionamento entre os objeto. Analisamos o JSON como uma forma de melhorar essa situação, mas assim que precisamos alterar o formato e migrar nossa estrutura, isso se torna bastante complicado. A criptografia é outro tópico que pode ser importante para você -PlayerPrefs
e File
não são seguros e podem ser lidos facilmente. Essas são apenas algumas das áreas em que um banco de dados como o SQLite pode ajudá-lo a cumprir os requisitos de persistência dos dados.No próximo tutorial, examinaremos outro banco de dados, o Realm Unity SDK, que oferece vantagens semelhantes às do SQLite e, ao mesmo tempo, é muito fácil de usar.