Como modelar vários "usos" (por exemplo, arma) para inventário / objeto / itens utilizáveis ​​(por exemplo, katana) em um banco de dados relacional

10

Então, estou trabalhando na expansão do uso de itens no site www.ninjawars.net e não sei exatamente como representá-los de maneira flexível no banco de dados relacional que usamos.

Eu posso estar latindo na árvore errada, então fique à vontade para fazer sugestões em outras direções, mas atualmente estou pensando que cada item deve ter "tags" relacionais.

Por exemplo, uma Katana atualmente é uma linha no banco de dados "itens". Para transformá-lo em uma arma, e uma coisa segura, eu estava pensando que teria um banco de dados de "características" e uma tabela item_traits que simplesmente se vinculava entre elas.

// Objects and their basic data

item_id | item
1 | Naginata


// Things that objects can do

trait_id | trait
1 | weapon
2 | holdable

// How those objects do those things, e.g. powerfully, weakly, while on fire

_item_id | _trait_id | item_trait_data
1 | 1 | damage: 5, damage_type: sharp, whatever, etc

Não tenho muita certeza de como modelar os dados extras resultantes (por exemplo, o dano que uma espada causará, o tipo de dano etc.).

Também não estou particularmente feliz que todo o item seja armazenado em mais de um local. Por exemplo, para criar uma cópia de um item com um nome diferente, como uma "espada curta", eu teria que copiar de várias tabelas para criar o item duplicado.

Existe uma maneira melhor de colocar isso em falta?

Edit: Devo apenas observar que já tenho um banco de dados postgresql em uso no site, e é por isso que quero usá-lo para meu armazenamento de dados.

Editar: eu adicionei uma resposta para a implementação que estou vendo atualmente.

Kzqai
fonte

Respostas:

10

Discordo levemente de todos e digo que a abordagem relacional é razoável aqui. O interessante aqui é que os itens podem ter várias funções. A questão principal será que o mapeamento entre esse layout relacional e um layout OO no código não parecerá "natural", mas acho que no lado do banco de dados várias funções podem ser expressas de maneira limpa (sem codificações ou redundância estranhas, apenas se juntam) .

A primeira coisa a decidir é quanto dos dados é específico do item e quanto é compartilhado por todos os itens de um determinado tipo.

Aqui está o que eu faria se todos os dados fossem específicos do item:

// ITEMS table: attributes common to all items
item_id | name        | owner         | location             | sprite_id | ...
1       | Light Saber | 14 (Tchalvek) | 381 (Tchalvek house) | 5663      | ...

// WEAPONS table: attributes for items that are weapons
item_id | damage | damage_type | durability | ...
1       | 5      | sharp       | 13         | ...

// LIGHTING table: attributes for items that serve as lights
item_id | radius   | brightness | duration | ...
1       | 3 meters | 50         | 8 hours  | ...

Nesse design, cada item está na tabela Itens, juntamente com os atributos que todos (ou a maioria) dos itens possuem. Cada função adicional que um item pode desempenhar é uma tabela separada.

Se você quiser usá-lo como arma, procure na tabela de Armas. Se estiver lá, é utilizável como arma. Se não estiver lá, não poderá ser usado como arma. A existência do registro informa se é uma arma. E se estiver lá, todos os seus atributos específicos de arma serão armazenados lá. Como esses atributos são armazenados diretamente em vez de em algum formato codificado, você poderá realizar consultas / filtros com eles. (Por exemplo, para a página de métricas do seu jogo, você pode agregar jogadores por tipo de dano da arma e poderá fazer isso com algumas junções e um tipo de dano por grupo.)

Um item pode ter várias funções e existir em mais de uma tabela específica de função (neste exemplo, arma e iluminação).

Se for apenas um valor booleano como "isso é suportável", eu o colocaria na tabela Items. Pode valer a pena armazenar em cache "isto é uma arma" etc. etc. para que você não precise fazer uma pesquisa nas armas e em outras tabelas de funções. No entanto, ele adiciona redundância, portanto, você deve ter cuidado para mantê-lo sincronizado.

A recomendação de Ari de ter uma tabela adicional por tipo também pode ser usada com essa abordagem se alguns dados não variarem por item. Por exemplo, se o dano da arma não variar por item, mas os papéis ainda variarem por item, você pode levar em consideração os atributos da arma compartilhada em uma tabela:

// WEAPONS table: attributes for items that are weapons
item_id | durability | weapon_type
1       | 13         | light_saber

// WEAPONTYPES table: attributes for classes of weapons
weapon_type_id | damage | damage_type
light_saber    | 5      | energy

Outra abordagem seria se os papéis desempenhados pelos itens não variarem por item, mas apenas por tipo de item. Nesse caso, você colocaria o item_type na tabela Items e poderá armazenar as propriedades como "é uma arma" e "é suportável" e "é uma luz" em uma tabela ItemTypes. Neste exemplo, também faço os nomes dos itens não variarem por item:

// ITEMS table: attributes per item
item_id | item_type    | owner         | location
1       | light_saber  | 14 (Tchalvek) | 381 (Tchalvek house)

// ITEMTYPES table: attributes shared by all items of a type
item_type   | name        | sprite_id | is_holdable | is_weapon | is_light
light_saber | Light Saber | 5663      | true        | true      | true

// WEAPONTYPES table: attributes for item types that are also weapons
item_type   | damage | damage_type
light_saber | 5      | energy

É provável que os tipos de item e tipo de arma não sejam alterados durante o jogo; portanto, você pode carregar essas tabelas na memória apenas uma vez e procurar esses atributos em uma tabela de hash em vez de em uma associação ao banco de dados.

amitp
fonte
+1. Esta é a única resposta que usa um banco de dados relacional como um banco de dados relacional. Qualquer coisa que não seja este ou o outro extremo da sugestão de blob do Nevermind e apenas usar o banco de dados como um armazenamento de dados, são planos horríveis.
+1 Mas essas abordagens parecem inflexíveis. Peço desculpas se parecer inconsistente, mas quero poder criar estruturas de banco de dados para itens essencialmente uma vez ... ... e depois codificar mais recursos para itens, inserir mais inserções no banco de dados para itens, mas não precisar alterar o banco de dados novamente no futuro (com exceções raras, é claro).
Kzqai
Se você deseja que o banco de dados relacional saiba sobre seus campos e otimize a representação deles, é necessário adicioná-los em algum lugar. É como tipos estáticos. Se você deseja que seu compilador conheça seus campos e otimize a representação deles, precisará declarar tipos. A alternativa é uma abordagem de tipo dinâmico, usada por alguns bancos de dados de documentos ou codificando seus dados no banco de dados relacional de alguma maneira. O banco de dados não saberá quais são os campos ou poderá otimizar seu armazenamento, mas você ganhará alguma flexibilidade.
Amitp
4

Infelizmente, situações como esta são onde os bancos de dados relacionais (como SQL) ficam aquém e os bancos de dados não relacionais (como o MongoDB ) se destacam. Dito isto, não é impossível modelar os dados em um banco de dados relacional e, como parece que sua base de código já é dependente do SQL, eis o modelo com o qual eu iria:

Em vez de criar um item e usar tabela, crie uma tabela e uma tabela de referência "[item] _type" para cada tipo de item.

Aqui estão alguns exemplos:

weapon_type_id | weapon_type
1 | sharp

weapon_id | weapon| weapon_type_id | damage
1 | Short Sword | 1 | 5

potion_type_id | potion_type
1 | HP
2 | Mana

potion_id | potion| potion_type_id | modifier
1 | Healing Elixer | 1| 5
2 | Mana Drainer | 2| -5

Essa solução oferece muita flexibilidade e escalabilidade a longo prazo, reduz (se não elimina) o espaço desperdiçado e é auto-documentada (ao contrário de "item_use_data"). Isso envolve um pouco mais de configuração no final do desenvolvedor, mas, na minha opinião, é a solução mais elegante disponível para situações em que você precisa usar um banco de dados relacional para armazenar seus dados. De um modo geral, os bancos de dados não relacionais são muito melhores para o desenvolvimento de jogos, pois são mais flexíveis na maneira como modelam dados, além de terem melhor desempenho do que os bancos de dados baseados em SQL - tornando-os uma opção "ganha-ganha".

Edição 1: corrigido um erro com o campo potion_type_id

Edit 2: Adicionado mais detalhes sobre bancos de dados não relacionais x relacionais para fornecer uma perspectiva adicional

Ari Patrick
fonte
Realmente não vejo muita flexibilidade saindo desse sistema. Por exemplo, se eu quisesse um frasco de vidro, capaz de ser usado como arma e de ser "afiado" ... ... ficaria sem sorte. Além disso, para cada novo tipo de item, eu precisaria criar uma estrutura de banco de dados para ele. Mesmo se houvesse apenas um ou dois do tipo.
Kzqai #
Os pontos apresentados são muito válidos e destacam exemplos adicionais de por que os bancos de dados relacionais não são adequados para essa situação específica. O modelo que apresentei mostra apenas a melhor maneira de organizar os dados de maneira que a memória seja conservada e a funcionalidade SQL seja mantida. Para armazenar itens de vários tipos, a melhor maneira de representar esses dados nesse modelo seria criar uma tabela potion_weapon com as propriedades apropriadas de poção e arma. Novamente, essa solução NÃO é ideal, mas é a solução mais elegante ao usar um banco de dados relacional.
Ari Patrick
@Ari: Sem desrespeito, mas seu problema é precisamente o motivo pelo qual a abordagem relacional é mais válida, não menos. É sabido que os bancos de dados NoSQL (ou NoRel) são ótimos para leituras de alto volume, dados distribuídos / altamente disponíveis ou esquemas mal definidos. Este caso é simplesmente uma associação clássica de muitos para muitos, e dbs como o Mongo não fazem isso tão bem quanto um RDBMS. Veja stackoverflow.com/questions/3332093/…
alphadogg
@alphadogg Como é esse relacionamento muitos-para-muitos? No banco de dados, as armas conhecem os tipos de armas, mas não precisam saber sobre as armas às quais estão associadas. Se, por algum motivo, você quiser conhecer todas as armas de um tipo específico de arma, basta escrever uma consulta que itere sobre a coleção de documentos da arma.
Ari Patrick
@Ari: Na pergunta do OP, uma arma pode ter muitos tipos e um tipo pode estar vinculado a muitas armas. Por exemplo, uma poção e uma espada são seguráveis. Uma espada é ao mesmo tempo segura e arma.
alphadogg
3

Primeiro, despeje a abordagem de herança orientada a objetos e use um sistema baseado em componentes .

Depois de fazer isso, o layout do SQL fica muito mais fácil. Você tem uma tabela para cada tipo de componente com um número de identificação compartilhado. Se você quiser o item 17, procure "item 17" em todas as tabelas. Qualquer tabela que tenha uma chave recebe seu componente adicionado.

Sua tabela de itens contém todos os dados necessários para itens em geral (nome, preço de venda, peso, tamanho, qualquer outra coisa que seja compartilhada entre todos os itens.) Sua tabela de armas contém todos os dados apropriados para armas, sua tabela de poções contém todos os dados apropriados para poções, sua tabela de armadura contém todos os dados apropriados para a armadura. Quer um peitoral, é uma entrada no Item e uma entrada na Armadura. Quer um espada que você possa beber, basta colocar uma entrada em cada mesa, e bam, está feito.

Lembre-se de que esse mesmo padrão não é particularmente específico de um item - você pode usá-lo para criaturas, zonas ou qualquer outra coisa que desejar. É surpreendentemente versátil.

ZorbaTHut
fonte
2

Usar o SQL foi seu grande erro. Não é absolutamente adequado para armazenar dados estáticos de design de jogos.

Se você não pode se afastar do SQL, eu consideraria armazenar itens em um serializado. Ou seja,

item_id (int) | item (BLOB)
1             | <binary data>

Claro, isso é feio e lança todas as "sutilezas" do SQL pela janela, mas você realmente precisa delas? Provavelmente, você lê todos os dados de seus itens no início do jogo de qualquer maneira, e nunca SELECTpor nada além de item_id.

deixa pra lá
fonte
3
Como (pelo que entendi) o jogo é multiplayer e suporta PvP, é provável que a maioria das informações de jogo não seja armazenada no cliente. Se isso for preciso, toda vez que os dados forem lidos da tabela, eles precisarão ser desserializados antes de serem remotamente úteis. Como resultado, esse formato torna a consulta de itens por suas propriedades MUITO cara; por exemplo: se você quiser recuperar todos os itens do tipo "arma", precisará recuperar todos os itens, desserializar cada um e filtrar manualmente o tipo de "arma", em vez de executar uma consulta simples .
Ari Patrick
1
De acordo com minha experiência, todos os itens são lidos no cache da memória no início do jogo. Portanto, você os desserializa apenas uma vez na inicialização e não usa o banco de dados depois. Se não for o caso, concordo que armazenar objetos serializados é uma má idéia.
Nevermind
Hmmm, a única coisa é que isso me deixa com uma interface necessária para editar os dados, o que seria inconveniente.
Kzqai #
2

Dependendo de quantas características você provavelmente precisará, você pode usar uma máscara de bits simples para objetos com os diferentes bits correspondentes a diferentes características:

000001 = holdable
000010 = weapon
000100 = breakable
001000 = throwable
010000 = solids container
100000 = liquids container

Em seguida, você pode fazer testes de bits simples para verificar se um objeto pode ser usado para uma determinada função.

Portanto, "garrafa de vidro" pode ter um valor de 101111, o que significa que pode ser segurado, pode ser usado como uma arma, ele se quebra facilmente, você pode jogá-lo e conter líquidos.

Qualquer editor que você criar para itens poderá ter um conjunto simples de caixas de seleção para ativar / desativar traços em um objeto.

Muttley
fonte
1
Gosto da ideia, porque ela permite que um item mantenha suas próprias características, e não sei se vou precisar pesquisar as características no banco de dados, mas não tenho muita certeza de como fazer isso funcionar. em um contexto relacional. Eu acho que cada bit iria mapear para os ids incrementais dos traços no banco de dados ...
Kzqai
Eu acho que bitmasking é mais útil para um número fixo de características, no entanto?
Kzqai
Muito mesmo. O que acontece se você tiver usado o seu último "bit"? Quão fácil é aumentar o tipo de dados para ganhar bits e percolá-lo através da camada do aplicativo ou adicionar uma coluna e fazer o mesmo? Além disso, seu mecanismo de banco de dados específico está bem ajustado para indexar operações de bits? É melhor em um banco de dados relacional permanecer atômico com as colunas de dados. Não agrupe vários domínios em uma coluna. Você não pode aproveitar os benefícios da plataforma.
alphadogg
1
Se você quisesse uma tabela de pesquisa no banco de dados para os traços (provavelmente uma boa idéia, pois você poderá adicioná-los dinamicamente a qualquer editor), então teria algo parecido com isto: id, bitmask, description. 1,1, "Holdable"; 2,2, "Arma"; 3,4, "Quebrável", etc. Com relação ao número fixo, sim, isso é verdade. Mas se você usa um int de 64 bits, deve ter muito escopo.
Muttley
1

Em nosso projeto, temos item_attributes para os diferentes "dados extras" que um item pode ter. É apresentado algo como isto:

item_id | attribute_id    | attribute_value | order 
1        25 (attackspeed)       50             3    

Em seguida, temos uma tabela de atributos com a seguinte aparência:

id |   name       | description
1    attackspeed    AttackSpeed:

Ari Patrick está certo, no entanto, os bancos de dados relacionais não são feitos para isso. A desvantagem é que temos alguns procedimentos bastante complexos para gerar novos itens (feitos por meio de uma ferramenta externa - que eu recomendo, não tente adicioná-los manualmente, você só se confundirá)

A outra opção que você tem é o uso de uma linguagem de script para criar os modelos de itens. Em seguida, você pode facilmente analisá-los e usá-los para criar novos itens. É claro que você ainda precisa salvar os dados do item no banco de dados, mas nesse momento não precisa se preocupar com detalhes da criação de novos itens, basta copiar e copiar o arquivo de script antigo, alterar algumas informações e é bom ir.

Honestamente, se refizéssemos como criamos novos itens estáticos, provavelmente adotaríamos uma abordagem muito mais simples usando modelos de itens de script.

Kyle C
fonte
1

Esse é seu relacionamento típico de muitos para muitos, nada esotérico demais para qualquer banco de dados relacional capaz. Você tem muitas características para qualquer objeto, e qualquer característica pode ser usada por um ou mais tipos diferentes de objetos. Modele três relações (tabelas), sendo uma delas uma relação de associação, e pronto. A indexação adequada garantirá leituras rápidas de dados.

Coloque em camadas um ORM no seu código e você deverá ter muito poucos problemas entre o DB e o middleware. Muitos ORMs também são capazes de gerar automaticamente as próprias classes, tornando-se ainda mais "invisíveis".

Quanto aos bancos de dados NoSQL, não há razão para que você não possa fazê-lo. Atualmente, está na moda torcer por essa tecnologia em trapos e blogs comerciais, mas eles vêm com uma série de problemas próprios: plataformas relativamente imaturas, ótimas para leituras simples e com pouca frequência alteradas (como um twits um-para-muitos em um perfil), mas ruim para leituras ou atualizações dinâmicas complexas, cadeia de ferramentas de suporte ruim, dados redundantes e os problemas de integridade que o acompanham, etc. No entanto, eles oferecem o apelo a uma melhor escalabilidade porque evitam recursos transacionais e sua sobrecarga de graus variados e modelos mais fáceis de distribuição / replicação .

O hobgoblin usual de bancos de dados relacionais versus bancos de dados NoSQL é o desempenho. RDMBSes diferentes têm diferentes graus de sobrecarga envolvidos, o que os torna menos preferidos para escalar para os níveis do Facebook ou Twitter. No entanto, é muito improvável que você enfrente esses problemas. Mesmo assim, sistemas simples de servidores baseados em SSD podem tornar o debate sobre desempenho inútil.

Sejamos claros: a maioria dos bancos de dados NoSQL são tabelas de hash fundamentalmente distribuídas e limitarão você da mesma forma que uma tabela de hash no seu código, ie. nem todos os dados se encaixam bem nesse modelo. O Modelo Relacional é muito mais poderoso para modelar relações entre dados. (O fator de confusão é que a maioria dos RDBMSs são sistemas legados que estão mal ajustados para as demandas dos populares 0,0001% da web, como Facebook et al.)

alphadogg
fonte
Se você vai consultá-los coletivamente, nem se preocupe em falar sobre bancos de dados. O NoSQL não é uma entidade que possa ser discutida de maneira significativa, é uma referência em massa inútil que os fãs do SQL usam para aplicar a lógica falha que, se um banco de dados desse grupo definido não possui um recurso, todos eles não possuem esse recurso.
Aaaaaaaaaaaa
NoSQL é o termo que os defensores do NoSQL escolheram adaptar. Não sei por que, não é muito descritivo. Mas não é algum tipo de pejorativo. Tendo trabalhado com vários, acho que a descrição deles pelo alphadogg é justa.
Talvez meu comentário tenha sido um pouco duro, ainda assim, eu odeio esse nome e agrupamento, é impossível ter um debate qualificado sobre os detalhes mais refinados de diferentes sistemas de banco de dados quando as pessoas mantêm essa visão de mundo simplificada.
Aaaaaaaaaaaa
@eBusiness: Acho seu comentário divertido, já que é o campo anti-RDBMS, se houver um, que auto-ungiu seu grupo de tecnologias como "NoSQL". É certo que muitos deles gostariam de ver esse nome obsoleto. Especialmente porque muitos deles estão finalmente estratificando o SQL sobre suas implementações físicas. Quanto a falar sobre eles, não achei seu comentário tão duro, ou tenho pele grossa, sua escolha! :) Quero dizer, pode-se falar sobre o Tea Party, por que não o grupo de bancos de dados NoSQL?
alphadogg
Isso é justo, eu reconheço que o relacionamento é totalmente de muitos para muitos. O que está me impedindo de criá-lo em um estilo verdadeiramente normalizado é o pensamento de que as "coisas" que formariam um item serão espalhadas por pelo menos duas tabelas. Não gosto da ideia de não conseguir criar inserções atômicas, acho.
Kzqai
0

É aqui que acho que os mapeadores OR mais modernos são realmente úteis, como o Entity Framework 4 e seu recurso de primeiro código (CTP) . Você acabou de escrever as classes e seus relacionamentos em código regular e, mesmo sem precisar decorá-las ou executar manualmente quaisquer ferramentas, a EF gerará o backing store no formato SQL para você, com tabelas de links necessárias e tudo ... isso realmente desencadeia a criatividade. ^^

Oskar Duveborn
fonte
Bem, vou criar o código e as classes e tudo isso no código apenas primeiro ... ... só tenho que traduzir isso para a estrutura de banco de dados existente depois.
Kzqai
0

Aqui está o que eu estou considerando agora:

Como toda "característica" requer essencialmente alterações no código, decidi manter as características (e quaisquer dados padrão que elas requeiram) no próprio código (pelo menos por enquanto).

Por exemplo $traits = array('holdable'=>1, 'weapon'=>1, 'sword'=>array('min_dam'=>1, 'max_dam'=>500));

Os itens obtêm um campo "trait_data" no banco de dados que usará a json_encode()função para ser armazenado no formato JSON.

item_id | item_name | item_identity | traits
1 | Katana | katana | "{"holdable":1,"weapon":1,"sword":{"min_dam":1,"max_dam":1234,"0":{"damage_type":"fire","min_dam":5,"max_dam":50,"type":"thrown","bob":{},"ids":[1,2,3,4,5,6,7]}}}"

O plano é que os itens herdem todas as estatísticas padrão de seus traços pai e tenham apenas traços de substituição que especifiquem diferenças do que os traços definem como padrão.

A desvantagem do campo traits é que, embora eu possa editar a parte do json manualmente ... ... não será realmente fácil ou seguro fazê-lo com os dados que estão no banco de dados.

Kzqai
fonte
1
O que acontece quando você introduz uma nova característica? Quantas vezes isso pode acontecer? Quais seriam os efeitos posteriores na carga de trabalho de manutenção de código? Compare com armazená-lo como muitos para muitos, em que suas instâncias de objetos são construídas dinamicamente (por meio da sua escolha do ORM) para armazenar esses dados.
alphadogg
Obrigado, esse é um bom ponto, adicionar novas características não será trivial. Você adicionou à minha paralisia de análise. : p
Kzqai 22/11/2010
Desculpe, mas isso é bastante crítico. Você só precisa pensar sobre isso. Modele o exercício em sua mente do que você faria quando algumas armas precisassem ser atualizadas para adicionar uma nova característica, para que o aprimoramento do jogo funcione.
alphadogg
-1

Isso é um pouco hacky e não garanto que seja um bom design de banco de dados; Minhas aulas de DB ocorreram há um tempo atrás e elas não vêm à mente rapidamente. Mas se for garantido que os itens tenham, digamos, menos de 10 itens, você pode atribuir a cada item um campo attribute1, e attribute2, e assim por diante, até attribute10. Isso eliminaria sua necessidade da tabela de atributos de itens muitos para muitos, com o custo de limitar o número de atributos.

Olhando para trás, tenho certeza de que é um design terrível, mas significa que você pode usar um banco de dados relacional confortável e não precisar entrar em território desconhecido. Cabe a você decidir se a troca vale a pena.

Gregory Avery-Weir
fonte
Este é um design terrível, nunca faça isso. Você ficaria melhor se não usasse um banco de dados.
ZorbaTHut
-1

Nevermind deu uma boa resposta, mas eu me pergunto, você precisa de um banco de dados para isso? Armazenar todos os dados em um arquivo simples e carregá-los no programa no lançamento parece ser a maneira mais inteligente de fazer isso.

Edit:
Certo, em um ambiente controlado por solicitação sem estado, é melhor manter os dados em um banco de dados. Escreva seus dados em um arquivo e escreva um pedaço de código para transformá-lo em um banco de dados do tipo sugerido por Nevermind.

Como alternativa, se o número de objetos não for muito grande, declarar literalmente o lote em um arquivo de código pode ser o método mais rápido.

aaaaaaaaaaaa
fonte
Err, eu já tenho um banco de dados em uso e integrado ao resto do site.
Kzqai