Gerenciando várias referências da mesma entidade do jogo em locais diferentes usando IDs


8

Eu já vi ótimas perguntas sobre tópicos semelhantes, mas nenhuma que abordava esse método específico:

Como tenho várias coleções de entidades de jogo no meu jogo [XNA Game Studio], com muitas entidades pertencentes a várias listas, estou pensando em maneiras de acompanhar sempre que uma entidade é destruída e removê-la das listas às quais pertence. .

Muitos métodos em potencial parecem desleixados / complicados, mas me lembro de uma maneira que já vi antes, na qual, em vez de ter várias coleções de entidades de jogos, você tem coleções de IDs de entidades de jogos . Esses IDs são mapeados para entidades do jogo por meio de um "banco de dados" central (talvez apenas uma tabela de hash).

Portanto, sempre que qualquer parte do código quiser acessar os membros de uma entidade do jogo, ele primeiro verifica se ainda está no banco de dados. Caso contrário, pode reagir em conformidade.

Esta é uma abordagem sólida? Parece que isso eliminaria muitos dos riscos / aborrecimentos de armazenar várias listas, com a troca sendo o custo da pesquisa toda vez que você deseja acessar um objeto.

Respostas:


3

Resposta curta: sim.

Resposta longa: Na verdade, todos os jogos que eu vi funcionaram dessa maneira. A pesquisa da tabela de hash é rápida o suficiente; e se você não se importa com os valores reais de ID (ou seja, eles não são usados ​​em nenhum outro lugar), pode usar um vetor em vez de uma tabela de hash. O que é ainda mais rápido.

Como um bônus adicional, essa configuração possibilita a reutilização dos objetos do jogo, reduzindo a taxa de alocação de memória. Quando um objeto do jogo é destruído, em vez de "libertá-lo", você apenas limpa seus campos e coloca um "pool de objetos". Então, quando você precisar de um novo objeto, basta pegar um existente do pool. Se novos objetos aparecerem constantemente, isso pode melhorar notavelmente o desempenho.


Em geral, isso é preciso o suficiente, mas precisamos levar outras coisas em consideração primeiro. Se muitos objetos estão sendo criados e destruídos rapidamente, e nossa função de hash é boa, a hashtable pode ser mais adequada que um vetor. Se você estiver em dúvida, vá com o vetor.
precisa saber é o seguinte

Sim, eu tenho que concordar: o vetor não é absolutamente mais rápido que uma tabela de hash. Mas é mais rápido 99% do tempo (-8
Nevermind

Se você deseja reutilizar os slots (o que você definitivamente deseja), precisa garantir que os IDs inválidos sejam reconhecidos. ex. O objeto no ID 7 está nas listas A, B e C. O objeto é destruído e no mesmo slot outro objeto é criado. Esse objeto se registra nas listas A e C. A entrada do primeiro objeto na lista B agora "aponta" para o objeto recém-criado, o que está errado neste caso. Isso pode ser resolvido perfeitamente com o Handle-Manager de Scott Bilas scottbilas.com/publications/gem-resmgr . Eu já usei isso em jogos comerciais e funciona como um encanto.
DarthCoder

3

Esta é uma abordagem sólida? Parece que isso eliminaria muitos dos riscos / aborrecimentos de armazenar várias listas, com a troca sendo o custo da pesquisa toda vez que você deseja acessar um objeto.

Tudo o que você fez foi transferir o ônus de executar a verificação desde o momento da destruição até o momento do uso. Eu não diria que nunca fiz isso antes, mas não considero 'som'.

Sugiro usar uma das várias alternativas mais robustas. Se o número de listas for conhecido, você poderá simplesmente remover o objeto de todas as listas. Isso é inofensivo se o objeto não estiver em uma determinada lista e você garantiu que o removeu corretamente.

Se o número de listas for desconhecido ou impraticável, armazene-as de volta no objeto. A implementação típica disso é o padrão de observador , mas você provavelmente pode obter um efeito semelhante com os delegados, que retornam a chamada para remover o item de qualquer lista que contenha quando necessário.

Ou, às vezes, você realmente não precisa de várias listas. Geralmente, você pode manter um objeto em apenas uma lista e usar consultas dinâmicas para gerar outras coleções transitórias quando precisar delas. por exemplo. Em vez de ter uma lista separada para o inventário de um personagem, você pode simplesmente retirar todos os objetos onde "transportado" é igual ao personagem atual. Isso pode parecer bastante ineficiente, mas é bom o suficiente para bancos de dados relacionais e pode ser otimizado por uma indexação inteligente.


Sim, na minha implementação atual, toda lista ou objeto único que deseja uma referência a uma entidade do jogo precisa assinar o evento destruído e reagir de acordo. Em alguns aspectos, isso pode ser tão bom ou melhor quanto a pesquisa de ID.
vargonian

11
No meu livro, esse é exatamente o oposto de uma solução robusta. Comparado com a pesquisa por ID, você tem MAIS código, com MAIS lugares para erros, MAIS oportunidades para esquecer algo e uma possibilidade de destruir objetos SEM informar a todos para inicializar. Com a pesquisa por ID, você tem um pouco de código que quase nunca muda, um único ponto de destruição e uma garantia de 100% de que você nunca acessará um objeto destruído.
Nevermind

11
Com a pesquisa por ID, você tem código extra em todos os locais em que precisa usar o objeto. Com uma abordagem baseada no observador, você não tem código extra, exceto ao adicionar e remover itens de listas, o que provavelmente será um punhado de lugares em todo o programa. Se você referenciar itens mais do que criar, destruir ou movê-los entre listas, a abordagem do observador resultará em menos código.
Kylotan

Provavelmente existem mais locais que referenciam objetos do que locais que criam / destroem outros. No entanto, cada referência requer apenas um pouquinho de código, ou nenhum, se a pesquisa já estiver agrupada por propriedade (que deve ser na maioria das vezes). Além disso, você PODE esquecer de se inscrever para a destruição de objetos, mas NÃO PODE esquecer de procurar um objeto.
Nevermind

1

Essa parece ser apenas uma instância mais específica do problema "como remover objetos de uma lista, enquanto itera através dela", que é fácil de resolver (iterar na ordem inversa é provavelmente a maneira mais simples de uma lista no estilo de matriz como C # 's List)

Se uma entidade for referenciada de vários locais, ela deverá ser um tipo de referência de qualquer maneira. Portanto, você não precisa passar por um sistema de identificação complicado - uma classe em C # já fornece o indireto necessário.

E finalmente - por que se preocupar em "verificar o banco de dados"? Basta dar uma IsDeadbandeira a cada entidade e verificar isso.


Eu realmente não conheço C #, mas essa arquitetura é frequentemente usada em C e C ++, onde o tipo de referência normal da linguagem (ponteiros) não é seguro para uso se o objeto foi destruído. Existem várias maneiras de lidar com isso, mas todas envolvem uma camada de indireção, e essa é uma solução comum. (A lista de Backpointers como Kylotan sugere é outra - o que você escolhe depende se você quer gastar memória ou tempo.)

C # é lixo coletado, portanto, não importa se você abandonar instâncias. Para recursos não gerenciados (ou seja: pelo coletor de lixo), existe o IDisposablepadrão, que é muito semelhante à marcação de um objeto como IsDead. Obviamente, se você precisar agrupar instâncias, em vez de tê-las em GC, será necessária uma estratégia mais complicada.
Andrew Russell

A bandeira IsDead é algo que eu usei com sucesso no passado; Eu apenas gosto da idéia de o jogo travar se eu falhar em verificar um estado morto, em vez de continuar com alegria, com resultados potencialmente bizarros e difíceis de depurar. Usar uma pesquisa de banco de dados seria a primeira; IsDead sinaliza o último.
vargonian

3
@vargonian Com o padrão Disposable, o que geralmente acontece é que você marque IsDisposed na parte superior de cada função e propriedade de uma classe. Se a instância for descartada, você lança ObjectDisposedException(que geralmente "trava" com uma exceção não tratada). Movendo a verificação para o próprio objeto (em vez de verificar o objeto externamente), você permite que o objeto defina seu próprio comportamento "estou morto" e remova o ônus da verificação dos chamadores. Para seus propósitos, você pode implementar IDisposableou criar sua própria IsDeadbandeira com funcionalidade semelhante.
Andrew Russell

1

Costumo usar essa abordagem em meu próprio jogo C # /. NET. Além dos outros benefícios (e riscos!) Descritos aqui, também pode ajudar a evitar problemas de serialização.

Se você deseja aproveitar os recursos internos de serialização binária do .NET Framework, o uso de IDs de entidade pode ajudar a minimizar o tamanho do gráfico de objeto gravado. Por padrão, o formatador binário do .NET serializa um gráfico inteiro de objetos no nível do campo. Digamos que eu queira serializar uma Shipinstância. Se Shiphouver um _ownercampo referenciando Playerquem é o proprietário, essa Playerinstância também será serializada. Se Playercontiver um _shipscampo (de, digamos ICollection<Ship>), todos os navios do jogador também serão gravados, juntamente com outros objetos referenciados no nível do campo (recursivamente). É fácil serializar acidentalmente um grande gráfico de objetos quando você deseja serializar apenas uma pequena parte dele.

Se, em vez disso, tenho um _ownerIdcampo, posso usar esse valor para resolver a Playerreferência sob demanda. Minha API pública pode até permanecer inalterada, com a Ownerpropriedade simplesmente executando a pesquisa.

Embora as pesquisas baseadas em hash sejam geralmente muito rápidas, a sobrecarga adicional pode se tornar um problema para conjuntos de entidades muito grandes com pesquisas frequentes. Se isso se tornar um problema para você, você poderá armazenar em cache a referência usando um campo que não seja serializado. Por exemplo, você poderia fazer algo assim:

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

Claro, essa abordagem também pode fazer a serialização mais difícil quando você realmente quer para serializar um gráfico de objeto grande. Nesses casos, você precisaria criar algum tipo de 'contêiner' para garantir que todos os objetos necessários sejam incluídos.


0

Outra maneira de lidar com sua limpeza é inverter o controle e usar um objeto Descartável. Você provavelmente tem um fator que aloca suas entidades; portanto, passe para a fábrica todas as listas às quais deve ser adicionado. A entidade mantém uma referência a todas as listas das quais faz parte e implementa IDisposable. No método de descarte, ele se remove de todas as listas. Agora você só precisa se preocupar com o gerenciamento de listas em dois locais: fábrica (criação) e descarte. Excluir no objeto é tão simples quanto entity.Dispose ().

A questão de nomes versus referências em C # é realmente importante apenas para gerenciar componentes ou tipos de valor. Se você possui listas de componentes vinculados a uma entidade, pode ser útil usar um campo de ID como chave de hashmap. Se você apenas possui listas de entidades, use listas com uma referência. As referências de C # são seguras e simples de trabalhar.

Para remover ou adicionar itens da lista, você pode usar uma Fila ou qualquer número de outras soluções para lidar com a adição e exclusão de listas.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.