🤔 Para Refletir :
"Mudar não é fraqueza, pelo contrário, é um sinônimo de superação"
- Ricky O Bardo

Manejo e persistência de dados. Qual solução utilizar em um projeto de longa duração e com entregas contínuas?

TerereZero

Plebeu
Membro
Membro
Tereré é vida 🧉 🎮
Juntou-se
08 de Julho de 2020
Postagens
41
Bravecoins
174
Boa Madrugada habitantes deste condado.

Seguinte, estou dando continuidade ao projeto que fiz na BraveJam usando a Unity, o Dungeon Controller. E expandindo para ser algo de longa duração, com entregas de conteúdo novo a cada 30 ou 45 dias.

Será um jogo mobile, com várias masmorras temáticas (gelo, lava, ruínas etc.), cada masmorra tendo umas 10 fases. A cada fase concluída você libera acesso a fase seguinte, e conseguindo uma pontuação X você libera a próxima masmorra.

Para controlar esses dados, e talvez um possível mercado in-game eu estou pensando em utilizar um banco de dados com um ORM, já que playerprefs não são seguros e arquivos de texto podem ser custosos de gerenciar. Fora o risco de resetar os dados do usuário a cada atualização, o que ia queimar meu filme com o público.

Algum de vocês tem experiência com projetos desse tipo, ou experiência em manejo e persistência dos dados do jogo usando banco de dados?
Agradeço pela atenção.
 
Se a ideia é um banco de dados local, a @Mayleone fez um tutorial bem maneiro no Unity usando SQLite pra gerenciar cartas num sistema de yu-gi-oh, apesar da aplicação ser bem diferente é uma referência legal pro que você quer fazer.

Não sei se eu iria com um banco de dados na nuvem pra guardar dados de cada jogador tho, se o problema é perder os dados com as atualizações, parece uma solução que não lida com o problema em si, só contorna; deve dar pra garantir isso de algum jeito mais simples. Apesar disso, confesso que não manjo de como funcionam as atualizações no ambiente mobile, então não vou saber direcionar pra uma solução melhor.

Outra coisa que vale considerar é que o banco de dados não é um "arquivo texto só que mais confiável", tem um monte de coisa a mais nele que podem tanto ser super úteis como um desperdício de recurso dependendo do que você quer; acho que guardar dados simples tipo "masmorras desbloqueadas" e "pontuação" cairia no segundo caso (pra um único jogador, se for pra todos os jogadores aí a história muda, apesar que ainda parece forçado usar um banco relacional). Aliás, se você pensou em um banco de dados local, na prática isso é só um arquivo binário especial, a única coisa que você ganha é não ter que lidar com a serialização e ter as operações do banco disponíveis em cima.

Pessoalmente, eu tentaria seguir na linha de guardar as coisas no armazenamento persistente do dispositivo mesmo, salvando objetos enxutos (structs puras mesmo) com alguma serialização simples (o Unity deve ter alguns codecs embutidos, quiçá o próprio C# tenha também, e JSON sempre é uma opção). Deve ter algum jeito de configurar as atualizações ou o armazenamento pra não perder esses dados, seria muito estranho se não tivesse; em último caso sempre dá pra jogar o arquivo pra nuvem também.
 
Eu nunca usei, mas por completude, a Unity tem o assetbundles e o sistema de addressables para carregar conteúdo de fora em realtime, como DLCs.

Agora, se você quiser uma resposta local para o banco de dados, acho que nem precisa complicar tanto, os scriptable objects ou até o atributo serializador de classes padrão da Unity funciona bem ([system.serializable]), ainda mais para um jogo relativamente pequeno e que não vá ter alterações de fora, caso for ter, ai é arquivo externo.
 
Vou dar uma pesquisada no assetbundles e nos addressables mais tarde, em um projeto futuro eles serão interessantes. Obrigado @rafaelrocha00.

Andei dando uma pesquisada, e realmente, usar um Banco de dados seria um exagero. Entretanto, arquivos locais não iriam satisfazer 100% a necessidade de atualizações do jogo, e no futuro a dificuldade de manutenção do projeto se tornaria um problema.

Baseado no post indicado pelo @Brandt, eu me recordei de um esquema utilizado em um projeto Android que trabalhei uns anos atrás chamado "Migrações". Ele utiliza uma tabela de controle no banco de dados e scripts para fazer as alterações nas tabelas, tanto estruturalmente quanto nos dados.

Desde que seja mantidos na ordem certa, os scripts são executados e os dados do usuário são mantidos. Isso pode ser interessante até mesmo para adição de eventos sazonais nos jogos usando triggers baseado em datas específicas, o que economiza trabalho em subir uma nova build a cada evento caso o jogo esteja estável.

Infelizmente, ainda não encontrei nada parecido para a Unity, então terei que programar manualmente usando Sqlite que é uma solução que não pesa tanto. Caso fique um sistema maneiro, vou criar um repositório no Github e disponibilizar para a comunidade. :computador:
 
@TerereZero Migrations são a solução mais comum quando você quer ter um esquema mutável no banco mesmo, afaik. Pra plataforma .NET existe o Entity Framework da microsoft, que tem suporte embutido para migrações. Acredito que deve funcionar bem junto do Unity.

Entretanto, sobre isso:

Andei dando uma pesquisada, e realmente, usar um Banco de dados seria um exagero. Entretanto, arquivos locais não iriam satisfazer 100% a necessidade de atualizações do jogo, e no futuro a dificuldade de manutenção do projeto se tornaria um problema.

Acho que você pegou errado a ideia das migrações, e do porque elas existem para bancos de dados.

Primeiro, o banco não facilita a manutenção do modelo de dados, pelo contrário: ele estabelece várias restrições em cima do modelo, de forma que é sempre sofrível ter que mudar o esquema. Aí entram as migrações, elas são um jeito "fácil" de manter um registro das transformações feitas no modelo do banco de forma reprodutível e que garantem que o banco não explode entre uma versão do modelo e a seguinte.

Isso não quer dizer que migrations vão tornar seu trabalho mais fácil se comparado à solução simples usando serialização simples de structs, e sim que se você precisar de um banco, e precisar mudar o modelo dele, então migrations são a solução. Se você eliminar qualquer uma das condições (i.e., não usar um banco, ou não mudar o modelo), então você não ganha nada de fato usando elas.

TL;DR: migrações de banco só existem porque é difícil mudar o esquema de um banco relacional, se você eliminar o banco não existe necessidade das migrações, porque o problema que elas resolvem deixa de existir, basicamente.

Serializando estruturas simples, você ainda consegue fazer todas as transformações que uma migration te daria. Por exemplo, supondo que começamos com um modelo assim:

C #:
[Serializable]
public class SaveData {
    public uint LastUnlockedStage;
    public ulong Score; 
}

Seguem algumas operações que poderíamos fazer com migrations, e como faríamos usando a struct:

Adicionar um campo:

Por exemplo, vamos supor que agora existem dois conjuntos de fases, que podem ser desbloqueadas independentemente. Basta definir um campo novo com um valor default na struct:

C #:
[Serializable]
public class SaveData {
    public uint LastUnlockedStage;
    public ulong Score;

    // Novo campo
    public uint LastUnlockedSpecialStage = 0;
}

Nota: adicionar campos em tabelas existentes em migrations depende sempre de definir um valor default, caso contrário você invalida os dados da tabela.


Adicionar uma "tabela":

Com classes não temos tabelas, mas o equivalente seria uma classe nova no modelo. Por exemplo:

C #:
[Serializable]
public class SaveData {
    public uint LastUnlockedStage;
    public ulong Score;

    // Novo campo
    public uint LastUnlockedSpecialStage = 0;

    // Nova classe, em um novo campo:
    public InventorySaveData Inventory = default(InventorySaveData);
}

[Serializable]
public class ItemSaveData {
  public uint Id;
  public uint Amount;
}

[Serializable]
public class InventorySaveData {
  public List<ItemSaveData> Items;
}


Remover um campo

Se por qualquer motivo você de repente quiser tirar um campo do modelo, pode simplesmente apagar ele que tudo continua funcionando:

C #:
[Serializable]
public class SaveData {
    public uint LastUnlockedStage;
    public ulong Score;

    // Acabou o evento das fases especiais, então tiramos o campo de fase especial

    public InventorySaveData Inventory = default(InventorySaveData);
}

[Serializable]
public class ItemSaveData {
  public uint Id;
  public uint Amount;
}

[Serializable]
public class InventorySaveData {
  public List<ItemSaveData> Items;
}




Isso são alguns exemplos de transformações que você faria numa migration e que são praticamente triviais com serialização simples.

Montei um Repl.It implementando os exemplos usando o XmlSerializer do C# (o código tá uma bagunça, é só uma prova de conceito mesmo haha). Usei XML ao invés de JSON porque o serializador JSON que vem embutido no .NET não funciona tão bem só com o [Serializable], o XML serializa melhor as coisas automaticamente. Qualquer serialização serve, desde que se comporte bem com esse esquema.
 
Voltar
Topo Inferior