Mapa com 20 milhões de peças faz com que o jogo fique sem memória, como evito isso?

11

Ao carregar mapas enormes adicionais, o método de carregamento lança uma exceção de falta de memória onde uma nova instância do bloco de mapas é criada. Eu gostaria de ter o mapa inteiro processado pelo menos no aplicativo do servidor (e no cliente, se possível). Como devo resolver este problema?

UPD: a questão aqui é como fazer com que o jogo pare de funcionar quando ainda houver memória livre para uso. Quanto à divisão do mapa em partes, é uma boa abordagem, mas não o que eu quero no meu caso.

UPD2: No começo, designei uma textura para cada nova instância da classe de blocos e foi isso que consumiu muita memória (também carregando tempo). Agora, leva cerca de quatro vezes menos espaço. Obrigado a todos, agora eu posso rodar mapas enormes sem pensar em dividi-los em pedaços ainda.

UPD3: Depois de reescrever o código para ver se as matrizes de propriedades do bloco funcionam mais rapidamente ou consomem menos memória (do que as referidas propriedades em seus respectivos blocos como propriedades do objeto), descobri que não apenas levei muito tempo para tentar isso, não trouxe nenhuma melhoria de desempenho e tornou o jogo muito difícil de depurar.

user1306322
fonte
8
Carregue apenas as áreas ao redor dos jogadores.
MichaelHouse
4
20 milhões de blocos com 4 bytes por bloco são apenas cerca de 80 MB - portanto, parece que seus blocos não são tão pequenos. Como não podemos magicamente fornecer mais memória, precisamos descobrir como diminuir seus dados. Então, mostre-nos o que há nessas peças.
Kylotan
O que o método de carregamento faz? Código postal, você usa o pipeline de conteúdo? Carrega texturas na memória ou apenas metadados? Quais metadados? Os tipos de conteúdo XNA geralmente fazem referência a itens do DirectX não gerenciado, de modo que os objetos gerenciados são muito pequenos, mas no lado não gerenciado, pode haver um monte de itens que você não vê, a menos que esteja executando um criador de perfil do DirectX. stackoverflow.com/questions/3050188/…
Oskar Duveborn
2
Se o sistema lançar uma exceção de falta de memória, não haverá memória livre suficiente para usar, apesar do que você possa imaginar. Não haverá uma linha secreta de código que possamos fornecer para permitir memória extra. O conteúdo de cada bloco e a maneira como você os está alocando é importante.
Kylotan 17/07
3
O que faz você pensar que tem memória livre? Você está executando um processo de 32 bits dentro de um sistema operacional de 64 bits?
Dalin Seivewright 17/07/12

Respostas:

58

o aplicativo falha quando atinge 1,5 GB.

Isso sugere fortemente que você não está representando seus blocos corretamente, pois isso significa que cada bloco tem aproximadamente 80 bytes de tamanho.

O que você precisa entender é que é preciso haver uma separação entre o conceito de jogo de um bloco e o bloco visual que o usuário vê. Esses dois conceitos não são a mesma coisa .

Veja Terraria, por exemplo. O menor mundo de Terraria ocupa 4200x1200 peças, ou seja, 5 milhões de peças. Agora, quanta memória é necessária para representar esse mundo?

Bem, cada ladrilho tem uma camada de primeiro plano, uma camada de fundo (as paredes do fundo), uma "camada de arame" para onde os fios vão e uma "camada de móveis" para onde os itens de móveis vão. Quanta memória cada bloco ocupa? Mais uma vez, estamos apenas falando conceitualmente, não visualmente.

Um bloco de primeiro plano pode ser facilmente armazenado em um curto não assinado. Não existem mais de 65536 tipos de blocos em primeiro plano, portanto, não faz sentido usar mais memória que isso. Os blocos de plano de fundo podem facilmente estar em um byte não assinado, pois existem menos de 256 tipos diferentes de blocos de plano de fundo. A camada de arame é puramente binária: ou um ladrilho tem um arame ou não. Então isso é um bit por bloco. E a camada de móveis poderia novamente ser um byte não assinado, dependendo de quantas peças de móveis diferentes fossem possíveis.

Tamanho total da memória por bloco: 2 bytes + 1 byte + 1 bit + 1 byte: 4 bytes + 1 bit. Portanto, o tamanho total de um pequeno mapa de Terraria é 20790000 bytes, ou ~ 20 MB. (nota: esses cálculos são baseados no Terraria 1.1. O jogo se expandiu muito desde então, mas mesmo o Terraria moderno pode caber dentro de 8 bytes por local de bloco, ou ~ 40MB. Ainda é bastante tolerável).

Você nunca deve ter essa representação armazenada como matrizes de classes C #. Eles devem ser matrizes de números inteiros ou algo semelhante. A estrutura do AC # também funcionaria.

Agora, quando chega a hora de desenhar parte de um mapa (observe a ênfase), o Terraria precisa converter esses blocos conceituais em blocos reais . Cada ladrilho precisa realmente escolher uma imagem de primeiro plano, imagem de fundo, uma imagem de mobília opcional e ter uma imagem de arame. É aqui que o XNA entra com suas várias folhas de sprite e tal.

O que você precisa fazer é converter a parte visível do seu mapa conceitual em blocos de folhas de sprite XNA reais. Você não deve tentar converter a coisa toda de uma só vez . Cada bloco que você armazena deve ser apenas um índice dizendo "Eu sou o tipo X do bloco", em que X é um número inteiro. Você usa esse índice inteiro para buscar qual sprite você usa para exibi-lo. E você usa as folhas de sprite do XNA para tornar isso mais rápido do que apenas desenhar quads individuais.

Agora, a região visível dos ladrilhos precisa ser dividida em vários pedaços, para que você não construa constantemente folhas de sprite sempre que a câmera se mover. Portanto, você pode ter pedaços de 64x64 do mundo como folhas de sprite. Quaisquer pedaços de 64x64 do mundo que são visíveis na posição atual da câmera do jogador são aqueles que você desenha. Quaisquer outros pedaços nem sequer têm folhas de sprite; se um pedaço cair da tela, você joga fora essa folha (nota: você realmente não o exclui; você o mantém por perto e o resecifica para um novo pedaço que pode se tornar visível mais tarde).

Eu gostaria de ter o mapa inteiro processado pelo menos no aplicativo do servidor (e no cliente, se possível).

Seu servidor não precisa conhecer ou se preocupar com a representação visual de blocos. Tudo o que precisa é se preocupar com a representação conceitual. O usuário adiciona um bloco aqui, por isso altera esse índice.

Nicol Bolas
fonte
4
+1 Excelente resposta. Precisa ser votado em mais do que o meu.
MichaelHouse
22

Divida o terreno em regiões ou pedaços. Depois, carregue apenas os pedaços visíveis para os jogadores e descarregue os que não são. Você pode pensar nisso como uma correia transportadora, onde você está carregando pedaços em uma extremidade e descarregando-os na outra enquanto o jogador se move. Sempre ficando à frente do jogador.

Você também pode usar truques como instanciamento. Onde se todos os blocos de um tipo têm apenas uma instância na memória e são desenhados várias vezes em todos os locais onde são necessários.

Você pode encontrar mais idéias como esta aqui:

Como eles fizeram isso: milhões de peças em Terraria

'Zoneamento' de áreas em um grande mapa de peças, bem como masmorras

MichaelHouse
fonte
O problema é que essa abordagem é boa para o aplicativo cliente, mas não tão boa para o servidor. Quando algumas peças do mundo disparam e uma reação em cadeia começa (e isso acontece muito), todo o mapa deve estar na memória para isso, e é um problema quando a memória fica inoperante.
user1306322
6
O que cada bloco contém? Quanta memória é usada por bloco? Mesmo com 10 bytes cada, você tem apenas 190 megabytes. O servidor não deve carregar a textura ou qualquer outra informação desnecessária. Se você precisar de uma simulação para 20 milhões de blocos, precisará separar o máximo possível desses blocos para formar um conjunto de simulação que tenha apenas as informações necessárias para suas reações em cadeia.
MichaelHouse
5

A resposta do Byte56 é boa, e comecei a escrever isso como um comentário, mas demorou muito e contém algumas dicas que podem ser mais úteis em uma resposta.

A abordagem é absolutamente tão boa para o servidor quanto para um cliente. Na verdade, é provavelmente mais apropriado, já que o servidor será solicitado a se preocupar com muito mais áreas do que o cliente. O servidor tem uma idéia diferente sobre o que precisa ser carregado do que um cliente (se preocupa com todas as regiões com as quais todos os clientes conectados se importam), mas é igualmente capaz de gerenciar um conjunto de regiões de trabalho.

Mesmo que você possa colocar um mapa tão gigantesco (e provavelmente não pode) na memória, não quer . Não há absolutamente nenhum ponto em ter dados carregados que você não precisa usar imediatamente, é ineficiente e lento. Mesmo se você pudesse ajustá-lo na memória, provavelmente não seria capaz de processar tudo isso em qualquer período de tempo sensato.

Suspeito que sua objeção a descarregar determinadas regiões seja porque sua atualização mundial está iterando todos os blocos e processando-os? Certamente isso é válido, mas não impede que apenas certas peças sejam carregadas. Em vez disso, quando uma região for carregada, aplique as atualizações perdidas, para atualizá-la. Isso também é muito mais eficiente em termos de processamento (concentrando um grande esforço em uma pequena área de memória). Se houver algum efeito de processamento que possa cruzar os limites da região (por exemplo, um fluxo de lava ou um fluxo de água que transitará para outra região), vincule essas duas regiões para que, quando uma for carregada, a outra também. Idealmente, essas situações seriam minimizadas; caso contrário, você voltará rapidamente ao caso 'todas as regiões devem ser carregadas o tempo todo'.

MrCranky
fonte
3

Uma maneira eficiente que usei em um jogo XNA foi:

  • use planilhas, não uma imagem para cada bloco / objeto, e apenas carregue o que você precisará nesse mapa;
  • divida o mapa em partes menores, como mapas de blocos de 500 x 200, se o seu mapa tiver 4 vezes esse tamanho ou algo que você possa manipular;
  • carregue esse pedaço na memória e pinte apenas as partes visíveis do mapa (digamos, os blocos visíveis mais um bloco para cada direção que o jogador estiver se movendo) em um buffer fora da tela;
  • limpe a janela de visualização e copie os pixels do buffer (isso é chamado de buffer duplo e suaviza o movimento);
  • quando você move, rastreie os bouds do mapa e carregue o próximo pedaço quando ele chegar perto dele e acrescente ao atual;
  • Quando o jogador estiver totalmente nesse novo mapa, você poderá descarregar o último.

Você também pode usar um único mapa grande dessa maneira, se não for tão grande e usar a lógica apresentada.

É claro que o tamanho das suas texturas deve ser equilibrado e a resolução paga um ótimo preço nisso. Tente trabalhar em uma resolução mais baixa e, se necessário, faça um pacote de alta resolução para carregar se uma configuração estiver definida como.

Você terá que repetir apenas as partes relevantes deste mapa a cada vez e renderizar apenas o necessário. Dessa forma, você melhorará muito o desempenho.

Um servidor pode processar um mapa enorme mais rapidamente porque não precisa renderizar (uma das operações mais lentas) o jogo; portanto, a lógica para isso pode ser o uso de pedaços maiores ou até mesmo o mapa inteiro, mas processa apenas a lógica em torno de jogadores, como em uma área de exibição de área 2 vezes maior que o jogador.

Um conselho aqui é que eu não sou especialista em desenvolvimento de jogos e meu jogo foi feito para o projeto final da minha graduação (que eu tive a melhor pontuação), então talvez eu não esteja tão certo em todos os pontos, mas eu tenho pesquisei na web e sites como http://www.gamasutra.com e o site de criadores de XNA creators.xna.com (no momento http://create.msdn.com ) para reunir conhecimentos e habilidades e funcionou bem para mim .

Ricardo Souza
fonte
2

Ou

  • compre mais memória
  • representar suas estruturas de dados de forma mais compacta
  • não mantenha todos os dados na memória de uma só vez.
Thomas
fonte
Eu tenho 8 Gb de ram no win7 64bit e o aplicativo trava quando chega a 1,5 Gb. Portanto, não há realmente sentido em comprar mais memória RAM, a menos que eu esteja perdendo alguma coisa.
user1306322
1
@ user1306322: Se você possui 20 milhões de blocos e ocupa cerca de 1,5 GB de memória, isso significa que seus blocos são aproximadamente 80 bytes por bloco . O que exatamente você está armazenando nessas coisas?
Nicol Bolas
@ NicolBolas como eu disse, algumas polegadas, bytes e um texture2d. Além disso, nem todos eles levam 1,5 GB, o aplicativo trava quando atinge esse custo de memória.
User1306322
2
@ user1306322: Ainda é muito mais do que precisa ser. Quão grande é um texture2d? Cada bloco possui um único ou eles compartilham texturas? Além disso, onde você disse isso? Você certamente não colocou na sua pergunta, que é onde as informações devem estar. Explique melhor suas circunstâncias específicas, por favor.
Nicol Bolas
@ user1306322: Francamente, não entendo por que minha resposta foi reduzida. Eu enumerei três remédios para situações de falta de memória - é claro, isso não significa que todos eles necessariamente se apliquem. Mas ainda acho que eles estão corretos. Eu provavelmente deveria ter escrito "estenda sua memória" em vez de "comprar" para incluir o caso de máquinas virtuais. Estou com voto negativo porque minha resposta foi concisa em vez de detalhada? Eu acho que eles são diretos o suficiente para serem entendidos sem maiores explicações ?!
Thomas
1

a questão aqui é como fazer com que o jogo pare de travar quando ainda houver memória livre para uso. Quanto à divisão do mapa em partes, é uma boa abordagem, mas não o que eu quero no meu caso.

Em resposta à sua atualização, você não pode alocar matrizes Int32.MaxValueem tamanho maior. Você tem que dividi-lo em pedaços. Você pode até encapsulá-lo em uma classe de invólucro que expõe uma fachada semelhante a uma matriz:

Jimmy
fonte