Como criar um AssetManager?

26

Qual é a melhor abordagem para projetar um AssestManager que contém referências a gráficos, sons etc. de um jogo?

Esses ativos devem ser armazenados em um par de mapa de chave / valor? Ou seja, peço o recurso "background" e o mapa retorna o bitmap associado? Existe uma maneira ainda melhor?

Especificamente, estou escrevendo um jogo para Android / Java, mas as respostas podem ser genéricas.

Bryan Denny
fonte

Respostas:

16

Depende do escopo do seu jogo. Um gerente de ativos é absolutamente essencial para títulos maiores, menos para jogos menores.

Para títulos maiores, você precisa gerenciar problemas como os seguintes:

  • Recursos compartilhados - essa textura de tijolos está sendo usada por vários modelos?
  • Duração do ativo - esse ativo que você carregou há 15 minutos não é mais necessário? Referência contando seus ativos para garantir que você saiba quando algo termina, etc.
  • No DirectX 9, se certos tipos de ativos são carregados e seu dispositivo gráfico é "perdido" (isso acontece se você pressionar Ctrl + Alt + Del, entre outras coisas) - seu jogo precisará recriá-los
  • Carregando ativos antes de precisar deles - você não poderia criar grandes jogos de mundo aberto sem isso
  • Ativos de carregamento em massa - geralmente empacotamos muitos ativos em um único arquivo para melhorar o tempo de carregamento - procurar em torno do disco consome muito tempo

Para títulos menores, essas coisas são menos problemáticas, estruturas como o XNA possuem gerenciadores de ativos - há muito pouco sentido em reinventá-lo.

Se você precisar de um gerente de ativos, não há realmente uma solução única, mas descobri que um mapa de hash com a chave como hash * do nome do arquivo (abaixados e os separadores todos 'fixos') funciona bem para os projetos em que trabalhei.

Geralmente, não é aconselhável codificar nomes de arquivos em seu aplicativo, geralmente é melhor ter outro formato de dados (como xml) que represente nomes de arquivos como 'IDs'.

  • Como uma observação lateral divertida, você normalmente recebe uma colisão de hash por projeto.
icStatic
fonte
Só porque você precisa gerenciar ativos, não é necessário o AssetManagers, um substantivo importante em maiúscula que provavelmente possui muitos métodos, baixo desempenho e semântica de memória turva. Para uma comparação, pense no que acontece se você tiver muito gerenciamento de projetos (geralmente bom) e, em seguida, quando tiver muitos gerentes de projetos (geralmente ruins).
2
@ Joe Wreschnig - como você abordaria os cinco requisitos mencionados pelo icStatic sem usar um gerente de ativos?
Antinome
8

(Tentando evitar a discussão "não use um gerente de ativos" aqui, já que considero offtopic.)

Um mapa de chave / valor é uma abordagem muito útil.

Temos uma implementação do ResourceManager em que Fábricas para diferentes tipos de Recursos podem se registrar.

O método "getResource" usa modelos para encontrar o Factory correto para o tipo de recurso desejado e retorna um ResourceHandle específico (novamente usando o modelo para retornar um SpecificResourceHandle).

Os recursos são recontados pelo ResourceManager (dentro do ResourceHandle) e liberados quando não são mais necessários.

O primeiro complemento que escrevemos foi o método "reload (XYZ)", que permite alterar recursos de fora do mecanismo em execução sem alterar nenhum código ou recarregar o jogo. (Isso é essencial quando os artistas trabalham em consoles;))

Na maioria das vezes, temos apenas uma instância do ResourceManager, mas às vezes criamos uma nova instância apenas para um nível ou um mapa. Dessa forma, podemos simplesmente chamar "shutdown" no levelResourceManager e garantir que nada esteja vazando.

exemplo (breve)

// very abbreviated!
// this code would never survive our coding guidelines ;)

ResourceManager* pRm = new ResourceManager;
pRm->initialize( );
pRm->registerFactory( new TextureFactory );
// [...]
TextureHandle tex = pRm->getResource<Texture>( "test.otx" ); // in real code we use some macro magic here to use CRCs for filenames
tex->storeToHardware( 0 ); // channel 0

pRm->releaseResource( pRm );

// [...]
pRm->shutdown(); // will log any leaked resource
Andreas
fonte
6

As classes de gerente dedicado quase nunca são a ferramenta de engenharia certa. Se você precisar do ativo apenas uma vez (como um plano de fundo ou mapa), solicite-o apenas uma vez e deixe que ele morra normalmente quando terminar. Se você precisar armazenar em cache um tipo específico de objeto, use uma fábrica que primeiro verifique um cache e carregue algo, coloque-o no cache e o retorne - e essa fábrica pode ser apenas uma função estática acessando uma variável estática , não um tipo próprio.

Steve Yegge (entre muitos, muitos outros) escreveu uma boa história sobre como as classes inúteis de gerente, por meio do padrão singleton, acabam sendo. http://sites.google.com/site/steveyegge2/singleton-considered-stupid


fonte
2
OK, claro. Mas em casos como o Android (ou outros jogos), você precisa carregar muitos gráficos / sons na memória antes de iniciar o jogo, não durante. Como posso usar o que você está dizendo (fábricas) para fazer isso durante uma tela de carregamento? Basta acertar todos os objetos da fábrica na tela de carregamento para que eles sejam armazenados em cache?
Bryan Denny
Não estou familiarizado com os detalhes do Android, mas não tenho idéia do que você quer dizer com "antes de iniciar o jogo". É realmente impossível carregar um recurso quando você precisar (ou quando precisar 'em breve'), e não quando iniciar o programa? Acho isso extremamente improvável, caso contrário, por exemplo, você nunca poderia ter mais texturas do que caber na RAM insuficiente do Android.
@Joe, dê uma olhada na minha outra pergunta sobre "carregamento de telas": gamedev.stackexchange.com/questions/1171/… Atingir um cache vazio significa muito tempo para ir para o disco e pode resultar em alguns resultados de desempenho do FPS nessas primeiras chamadas . Se você já sabe o que vai acertar com antecedência, é melhor acertá-lo durante o carregamento para pré-armazenar em cache, certo?
Bryan Denny
Novamente, eu não posso falar com o Android, mas geralmente ir para o disco é exatamente o que você pode fazer sem receber hits do FPS, porque o thread que entra no disco não usa nenhuma CPU. Você só precisa fazer um orçamento com antecedência suficiente para não receber pop-in. Se você vai pré-armazenar em cache tudo, porque sabe antecipadamente o que precisa, realmente não precisa de um AssetManager, porque não precisa gerenciar nenhum ativo - eles já estão disponíveis.
11
@ Joe, uma fábrica também não é um "Gerente Dedicado"?
MSN
2

Sempre achei que um bom gerente de ativos deveria ter vários modos de operação. Esses modos provavelmente seriam módulos de origem separados, aderindo a uma interface comum. Os dois modos básicos de operação seriam:

  • Modo de produção - todos os ativos são locais e sem todos os metadados
  • Modo de Desenvolvimento - os testes são armazenados em um banco de dados (por exemplo, MySQL, etc) com metadados adicionais. O banco de dados seria um sistema de duas camadas com um banco de dados local armazenando em cache um banco de dados compartilhado. Os criadores de conteúdo poderão editar e atualizar o banco de dados compartilhado e as atualizações automaticamente propagadas para os sistemas de desenvolvedor / controle de qualidade. Também deve ser possível criar conteúdo de espaço reservado. Como tudo está em um banco de dados, é possível fazer consultas no banco de dados e gerar relatórios para analisar o estado da produção.

Você precisaria de uma ferramenta que possa obter todos os testes do banco de dados compartilhado e criar o conjunto de dados de produção.

Nos meus anos como desenvolvedor, nunca vi nada assim, embora tenha trabalhado apenas para um punhado de empresas, portanto minha visão não é realmente representativa.

Atualizar

OK, alguns votos negativos. Vou expandir esse design.

Primeiro, você realmente não precisa de aulas de fábrica, porque se você tem:

TextureHandle tex = pRm->getResource<Texture>( "test.otx" );

você conhece o tipo, então faça:

TextureHandle tex = new TextureHandle ("test.otx");

mas o que eu estava tentando dizer acima é que você não usaria nomes de arquivos explícitos, a textura a ser carregada seria especificada pelo modelo em que a textura é usada, para que você não precise realmente de um nome legível por humanos, poderia ser um valor inteiro de 32 bits, que é muito mais fácil para a CPU manipular. Portanto, no construtor para TextureHandle, você teria:

if (texture already loaded)
  update texture reference count
else
  asset_stream = new AssetStream (resource_id)
  asset_stream->ReadBytes
  create texture
  set texture ref count to 1

O AssetStream usa o parâmetro resource_id para encontrar a localização dos dados. A maneira como isso foi feito depende do ambiente em que você está executando:

Em desenvolvimento: o fluxo consulta o ID em um banco de dados (usando SQL, por exemplo) para obter um nome de arquivo e, em seguida, abre o arquivo, o arquivo pode ser armazenado em cache localmente ou extraído de um servidor se o arquivo local não existir ou for desatualizado.

Na liberação: o fluxo consulta o ID em uma tabela de chave / valor para obter um deslocamento / tamanho em um arquivo grande e compactado (como o arquivo WAD do Doom).

Skizz
fonte
Eu votei contra você, porque você sugeriu colocar tudo em uma tabela SQL com chaves primárias, em vez de usar um VCS real. Também considero usar IDs opacos em vez de otimização prematura de nomes de string. Usei cadeias de caracteres em dois grandes projetos para todos os ativos, exceto chaves de conversão, das quais tínhamos centenas de milhares de chaves de cadeia muito longas (e depois apenas para portar para consoles). Eles eram normalmente normalizados, para que pudéssemos usar comparações de ponteiros em vez de comparações de strings, mas as comparações de strings são geralmente dominadas pelo custo da busca de memória e, de qualquer maneira, não pela comparação real.
@ Joe: Eu só dei SQL como exemplo e somente em um ambiente de desenvolvimento, você também pode usar um VCS. Sugeri apenas o banco de dados SQL, pois você pode adicionar informações extras aos objetos armazenados e usar as funções SQL para consultar informações do banco de dados (mais um ganho de gerenciamento do que qualquer outra coisa). Quanto aos IDs opacos como otimização prematura - alguns podem vê-lo dessa maneira, eu acho, mas acho que seria mais fácil começar com isso do que exibi-lo em um momento posterior do desenvolvimento. Eu não acho que isso afetaria muito o desenvolvimento se você usasse ID ou strings.
Skizz 29/07
2

O que eu gosto de fazer por ativos é configurar um gerenciador fixo . Inspirados no mecanismo Doom, os grumos são pedaços de dados que contêm ativos, armazenados em um arquivo fixo que declara nomes, comprimentos, tipo (tamanho de bitmap, som, sombreador etc.) e tipo de conteúdo (arquivo, outro tamanho fixo, interno) o próprio arquivo fixo). Na inicialização, esses agrupamentos são inseridos em uma árvore binária, mas ainda não foram carregados. Cada mapa (que também é um agrupamento) possui uma lista de dependências, que são simplesmente os nomes dos agrupamentos que o mapa precisa para funcionar. Esses pedaços, a menos que já tenham sido carregados, são carregados no momento em que o mapa é carregado. Além disso, os blocos dos mapas adjacentes do mapa são carregados, não apenas ao mesmo tempo, mas quando o mecanismo está ocioso por algum motivo. Isso pode tornar os mapas perfeitos e não há tela de carregamento.

Meu método é perfeito para mapas de mundo aberto, mas um jogo baseado em níveis não se beneficiará da uniformidade que esse método proporciona. Espero que isto ajude!

Marcus Cramer
fonte