Como lidar com imagens arbitrariamente grandes em jogos 2D?

13

Questão

Quando você tem um jogo 2D que usa imagens muito grandes (maiores do que o seu hardware pode suportar), o que você faz? Talvez haja alguma solução interessante para esse problema que nunca me ocorreu antes.

Estou basicamente trabalhando em uma espécie de criador de jogos de aventura gráfica. Meu público-alvo são pessoas com pouca ou nenhuma experiência em desenvolvimento (e programação) de jogos. Se você estivesse trabalhando com esse aplicativo, não preferiria que ele resolvesse o problema internamente, em vez de pedir para "dividir suas imagens"?

Contexto

É sabido que as placas gráficas impõem um conjunto de restrições ao tamanho das texturas que podem ser usadas. Por exemplo, ao trabalhar com o XNA, você fica restrito a um tamanho máximo de textura de 2048 ou 4096 pixels, dependendo do perfil escolhido.

Normalmente, isso não representa um problema nos jogos 2D, porque a maioria deles possui níveis que podem ser construídos a partir de peças individuais menores (por exemplo, peças ou sprites transformados).

Mas pense em alguns jogos clássicos de aventura gráfica point'n'click (por exemplo, Monkey Island). As salas de jogos gráficos de aventura geralmente são projetadas como um todo, com poucas ou nenhuma seção repetível, como um pintor trabalhando na paisagem. Ou talvez um jogo como o Final Fantasy 7, que usa grandes fundos pré-renderizados.

Algumas dessas salas podem ficar bem grandes, abrangendo frequentemente três ou mais telas de largura. Agora, se considerarmos esses planos de fundo no contexto de um jogo moderno de alta definição, eles facilmente excederão o tamanho máximo de textura suportado.

Agora, a solução óbvia para isso é dividir o plano de fundo em seções menores que se encaixam nos requisitos e desenhá-las lado a lado. Mas você (ou seu artista) deve ser o único a dividir todas as suas texturas?

Se você estiver trabalhando com uma API de alto nível, talvez não deva estar preparado para lidar com essa situação e fazer a divisão e a montagem das texturas internamente (como um pré-processo, como o Content Pipeline do XNA ou em tempo de execução)?

Editar

Aqui está o que eu estava pensando, do ponto de vista da implementação do XNA.

Meu mecanismo 2D requer apenas um subconjunto muito pequeno da funcionalidade do Texture2D. Na verdade, posso reduzi-lo a essa interface:

public interface ITexture
{
    int Height { get; }
    int Width { get; }
    void Draw(SpriteBatch spriteBatch, Vector2 position, Rectangle? source, Color  color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
}

O único método externo que eu uso que depende de Texture2D é SpriteBatch.Draw (), para que eu pudesse criar uma ponte entre eles adicionando um método de extensão:

    public static void Draw(this SpriteBatch spriteBatch, ITexture texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
    {
        texture.Draw(spriteBatch, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
    }

Isso me permitiria usar uma ITexture de forma intercambiável sempre que eu usasse um Texture2D antes.

Então, eu poderia criar duas implementações diferentes do ITexture, por exemplo, RegularTexture e LargeTexture, em que RegularTexture simplesmente envolvia um Texture2D comum.

Por outro lado, LargeTexture manteria uma lista de Texture2D correspondendo a cada uma das partes divididas da imagem completa. A parte mais importante é que chamar Draw () na LargeTexture deve poder aplicar todas as transformações e desenhar todas as peças como se fossem apenas uma imagem contígua.

Por fim, para tornar todo o processo transparente, eu criaria uma classe unificada de carregador de textura que atuaria como um Método de Fábrica. Ele pegaria o caminho da textura como parâmetro e retornaria uma ITexture que seria uma RegularTexture ou uma LargeTexture, dependendo do tamanho da imagem que excede ou não o limite. Mas o usuário não precisava saber qual era.

Um pouco exagerado ou soa bem?

David Gouveia
fonte
1
Bem, se você quiser dividir a imagem, estenda o Pipeline de conteúdo para fazer isso programaticamente. Nem todas as funcionalidades existem para cobrir todos os cenários e, em algum momento, você precisará estendê-las. A divisão pessoal parece a solução mais simples, pois você pode manipular imagens como mapas de blocos, nos quais existem vários recursos.
dman
Fazer isso no pipeline de conteúdo é a abordagem mais sensata. Mas, no meu caso, eu precisava carregar minhas imagens em tempo de execução e já consegui encontrar uma solução em tempo de execução (e foi por isso que fiz essa pergunta ontem). Mas a verdadeira beleza vem de pegar esse "mapa de blocos" e fazê-lo se comportar como uma textura normal. Ou seja, você ainda pode passá-lo para SpriteBatch para desenhar, e você ainda pode especificar posição, rotação, escala, src / dest retângulos, etc. Estou na metade do caminho até agora :)
David Gouveia
Mas, verdade seja dita, já estou em um ponto em que me pergunto se tudo isso realmente vale a pena ou se devo simplesmente mostrar um aviso ao usuário dizendo que o tamanho máximo da imagem suportada é 2048x2048 e pronto.
David Gouveia
É realmente você quem decide sobre o design. Se você terminar o projeto e não puder rolar entre as salas, será melhor o editor não finalizado, que permite rolar grandes texturas: D
Aleks
Escondê-lo na API parece uma boa maneira de facilitar a vida dos autores de jogos. Provavelmente eu o implementaria nas classes Polygon (sprite) e Texture, e não em casos especiais nas imagens menores, mas apenas faria um mosaico de 1x1. Portanto, a classe de textura é um grupo de texturas que descreve todos os blocos e quantas linhas / colunas. A classe Polygon examina essas informações para se dividir e desenhar cada lado a lado com a textura apropriada. Dessa forma, é da mesma classe. Pontos de bônus se sua classe de sprite desenhar apenas blocos visíveis e grupos de textura não carregarão texturas até que seja necessário.
uliwitness

Respostas:

5

Eu acho que você está no caminho certo. Você precisa dividir as imagens em pedaços menores. Desde que você criou um game maker, não pode usar o pipeline de conteúdo do XNA. A menos que seu criador seja mais como um mecanismo que ainda exigirá que os desenvolvedores usem o Visual Studio. Então você precisa criar suas próprias rotinas que farão a divisão. Você deve até considerar transmitir as peças um pouco antes que se tornem visíveis, para que você ainda não limite a quantidade de memória de vídeo que os players devem ter.

Existem alguns truques que você pode fazer especificamente para jogos de aventura. Eu imagino que, como todos os jogos clássicos de aventura, você terá poucas camadas dessas texturas para que ... seu personagem possa andar atrás da árvore ou do prédio ...

Se você deseja que essas camadas tenham um efeito paralaxante, ao dividir suas peças, pule toda a translúcida uma vez. Aqui você precisa pensar no tamanho dos ladrilhos. Os blocos menores adicionarão o gerenciamento de despesas gerais e as chamadas de chamadas, mas haverá menos dados, onde os blocos maiores serão mais amigáveis ​​à GPU, mas terão muita translucidez.

Se você não tiver um efeito paralaxante, em vez de cobrir a cena inteira com 2 a 4 imagens gigantes com profundidade de 32 bits, você poderá criar apenas uma profundidade de 32 bits. E você pode ter uma camada de stencil ou profundidade que será de apenas 8 bits por pixel.

Eu sei que isso parece difícil de gerenciar ativos por não desenvolvedores de jogos, mas na verdade não precisa ser. Por exemplo, se você tem uma rua com 3 árvores e o jogador fica atrás de 2 delas, na frente da terceira árvore e no restante do plano de fundo ... Faça o layout da cena da maneira que desejar no seu editor e depois em um Para criar uma etapa do seu criador de jogos, você pode assar essas imagens gigantes (bem pequenas peças de uma imagem grande).

Sobre como os jogos clássicos estavam fazendo isso. Não sei se eles provavelmente fizeram truques semelhantes, mas você deve se lembrar de que na época a tela tinha 320x240, os jogos tinham 16 cores que, ao usar texturas palatizadas, permitem codificar 2 pixels em um byte. Mas não é assim que as arquiteturas atuais funcionam: D

Boa sorte

Aleks
fonte
Obrigado, muitas idéias muito interessantes aqui! Na verdade, eu já vi a técnica de camada de estêncil usada antes no Adventure Game Studio. Quanto à minha inscrição, estou fazendo as coisas de maneira um pouco diferente. Os quartos não têm necessariamente uma imagem de fundo. Em vez disso, há uma paleta de imagens que o usuário importou e uma camada de segundo plano / primeiro plano. O usuário é livre para arrastar e soltar peças na sala para criá-lo. Portanto, se ele quisesse, também poderia compor peças menores, sem precisar criar objetos de jogo não interativos. Não existe paralaxe, pois existem apenas essas duas camadas.
David Gouveia
E eu olhei os arquivos de dados para Monkey Island Special Edition (o remake de HQ). Todo plano de fundo é dividido em exatamente 1024x1024 partes (mesmo que uma grande parte da imagem seja deixada sem uso). De qualquer forma, verifique minha edição para saber o que está em mente, a fim de tornar todo o processo transparente.
David Gouveia
Eu provavelmente escolheria algo mais razoável como 256x256. Dessa forma, não desperdiçarei recursos e, se mais tarde decidir implementar o streaming, estará carregando pedaços mais razoáveis. Talvez eu não tenha explicado bem o suficiente, mas sugeri que você pudesse transformar todos esses pequenos objetos estáticos em uma textura grande. O que você salvará é, caso tenha uma grande textura de fundo, todos os pixels cobertos pelos objetos estáticos na parte superior.
Aleks
Entendo, então mescle todos os objetos estáticos em segundo plano, depois divida e crie um atlas de textura. Eu também poderia transformar tudo o mais em um atlas de textura seguindo essa linha de pensamento. Essa é uma boa ideia também. Mas deixarei para depois, porque agora não tenho nenhuma etapa de "construção", ou seja, o editor e o cliente do jogo rodam no mesmo mecanismo e no mesmo formato de dados.
David Gouveia
Eu gosto da abordagem de estêncil, mas se eu estiver criando editor de jogos de aventura, provavelmente não a usaria ... Talvez porque se eu estiver criando essa ferramenta, pretenderei para desenvolvedores de jogos. Bem, tem seus prós e contras. Por exemplo, você pode usar outra imagem na qual os bits podem definir gatilhos por pixel ou, se você tiver um pouco de água animada, poderá estêncil facilmente. Nota: Uma sugestão não relacionada estranha analisa os diagramas de Estado / Atividade da UML para o seu script.
Aleks
2

Corte-os para que eles não sejam mais arbitrariamente grandes, como você diz. Não há mágica. Esses jogos 2D antigos fizeram isso ou foram renderizados em software; nesse caso, não havia restrições de hardware além do tamanho da RAM. É digno de nota que muitos desses jogos 2D realmente não tinham grandes origens, apenas rolavam lentamente para se sentirem enormes.

O que você está procurando fazer em seu criador de jogos de aventura gráfica é pré-processar essas imagens grandes durante algum tipo de etapa de "compilação" ou "integração" antes que o jogo seja empacotado para execução.

Se você quiser usar uma API e uma biblioteca existentes, em vez de escrever a sua própria, sugiro que você converta essas imagens grandes em blocos durante essa "construção" e use um mecanismo de bloco 2D, dos quais existem muitos.

Se você quiser usar suas próprias técnicas em 3D, poderá dividir-se em blocos e usar uma API 2D bem conhecida e bem projetada para escrever sua própria biblioteca em 3D, dessa forma, é garantido que você estará começando pelo menos com um bom design de API e menos design necessário de sua parte.

Patrick Hughes
fonte
Fazer isso no tempo de compilação seria o ideal ... Mas meu editor também usa o XNA para renderização, não é apenas o mecanismo. Isso significa que, uma vez que o usuário importe a imagem e a arraste para uma sala, o XNA já poderá processá-la. Portanto, meu grande dillema é fazer a divisão na memória ao carregar o conteúdo ou no disco ao importar o conteúdo. Além disso, dividir fisicamente a imagem em vários arquivos significaria que eu precisaria de uma maneira de armazená-los de maneira diferente, enquanto fazê-lo na memória em tempo de execução não exigiria nenhuma alteração nos arquivos de dados do jogo.
David Gouveia
1

Se você estiver familiarizado com a aparência de um quadtree na prática visual, usei um método que corta imagens grandes e detalhadas em imagens menores, relativamente semelhantes à aparência de uma prova / visualização de um quadtree. No entanto, as minhas foram feitas dessa maneira para cortar áreas especificamente que poderiam ser codificadas ou compactadas de maneira diferente com base na cor. Você pode usar esse método e fazer o que descrevi, pois também é útil.

Em tempo de execução, cortá-los exigiria mais memória temporariamente e reduziria o tempo de carregamento. Eu diria que isso depende se você se preocupa mais com o armazenamento físico ou o tempo de carregamento de um jogo de usuários feito com seu editor.

Carga / tempo de execução: A maneira que eu usaria o carregamento é apenas cortá-lo em pedaços de tamanho proporcional. No entanto, considere um pixel na parte mais à direita, se for um número ímpar de pixels, ninguém gosta que suas imagens sejam cortadas, especialmente usuários inexperientes que talvez não saibam que isso não depende inteiramente deles. Torne-os com metade da largura OU metade da altura (ou 3º, 4º, o que for), o motivo não para quadrados ou 4 seções (2 linhas e 2 colunas) é porque isso tornaria a memória lado a lado menor, pois você não se preocuparia com xey ao mesmo tempo (e dependendo de quantas imagens são grandes, pode ser muito importante)

Antes da Mão / Automaticamente: Nesse caso, gosto da idéia de oferecer ao usuário a opção de armazená-lo em uma imagem ou em partes separadas (uma imagem deve ser o padrão). Armazenar a imagem como um gif funcionaria muito bem. A imagem estaria em um arquivo e, em vez de cada quadro ser uma animação, seria mais como um arquivo compactado da mesma imagem (que é essencialmente o que é um .gif). Isso permitiria facilidade de transporte físico de arquivos, facilidade de carregamento:

  1. É um único arquivo
  2. Todos os arquivos teriam formatação quase idêntica
  3. Você pode escolher facilmente quando E se deseja dividi-lo em texturas individuais
  4. Pode ser mais fácil acessar as imagens individuais se elas já estiverem em um único .gif carregado, uma vez que não exigiria tanta confusão de arquivos de imagem na memória

Ah, e se você usar o método .gif, altere a extensão do arquivo para outra coisa (.gif -> .kittens) para evitar menos confusão quando o seu .gif for animado como se fosse um arquivo quebrado. (não se preocupe com a legalidade disso, .gif é um formato aberto)

Joshua Hedges
fonte
Olá e obrigado pelo seu comentário! Desculpe se eu entendi errado, mas o que você escreveu não se preocupa principalmente com compactação e armazenamento? Nesse caso, estou realmente preocupado com o fato de o usuário do meu aplicativo poder importar qualquer imagem e o mecanismo (no XNA) ser capaz de carregar e exibir qualquer imagem em tempo de execução . Consegui resolvê-lo simplesmente usando o GDI para ler seções diferentes da imagem em XNA Textures separadas e agrupando-as em uma classe que se comporta exatamente como uma textura regular (ou seja, suporta desenho e transformações como um todo).
David Gouveia
Mas, por curiosidade, você poderia elaborar um pouco mais sobre quais critérios usou para selecionar qual área da imagem foi inserida em cada seção?
David Gouveia
Algumas dessas informações podem ficar exageradas se você tiver menos experiência com o manuseio de imagens. Mas, essencialmente, eu estava descrevendo o corte da imagem em segmentos. E sim, o ponto .gif estava descrevendo o armazenamento e o carregamento, mas serve como um método para você cortar a imagem e armazená-la no computador como está. Se você salvá-lo como um gif, eu estava dizendo que você pode recortá-lo nas imagens individuais e salvá-lo como um gif animado, com cada corte da imagem sendo salvo como quadros de animação separados. Funciona bem e eu até adicionei outros recursos que podem ajudar.
Joshua Hedges
O sistema quadtree, como descrevi, é inteiramente declarado como referência, pois é daí que tenho uma ideia. Embora, em vez de agrupar uma pergunta possivelmente útil para leitores que possam ter problemas semelhantes, devemos usar mensagens privadas se você quiser saber mais.
Joshua Hedges
Roger, recebi o papel de usar quadros GIF como um tipo de arquivo. Mas também estou pensando: o GIF não é um formato indexado com paleta de cores limitada? Posso apenas armazenar imagens RGBA de 32bpp inteiras em cada quadro? E imagino que o processo de carregá-lo seja ainda mais complicado, pois o XNA não suporta nada disso.
David Gouveia
0

Isso vai parecer óbvio, mas, por que não dividir seu quarto em pedaços menores? Escolha um tamanho arbitrário que não esbarrar nas placas gráficas (como 1024x1024, ou talvez maior) e apenas dividir suas salas em várias partes.

Sei que isso evita o problema e, honestamente, esse é um problema muito interessante de resolver. De qualquer forma, essa é uma solução trivial, que pode funcionar algumas vezes para você.

ashes999
fonte
Eu acho que posso ter mencionado o motivo em algum lugar deste tópico, mas como não tenho certeza, vou repetir. Basicamente, porque não é um jogo, mas um criador de jogos para o público em geral (pense em algo no mesmo escopo que o criador de RPG), então, em vez de forçar a limitação do tamanho da textura quando os usuários estão importando seus próprios ativos, pode ser bom lidar com isso. a divisão e a costura automaticamente nos bastidores, sem nunca incomodá-los com os detalhes técnicos das limitações de tamanho de textura.
David Gouveia
Ok, isso faz sentido. Desculpe, acho que perdi isso.
precisa