Padrões de procedimentos que podem ser usados ​​com cartões lado a lado

A geração de procedimentos é usada para aumentar a variabilidade dos jogos. Projetos conhecidos incluem Minecraft , Enter the Gungeon e Descenders . Neste post, explicarei alguns dos algoritmos que podem ser usados ​​ao trabalhar com o sistema Tilemap , que apareceu como uma função 2D no Unity 2017.2 e com o RuleTile .

Com a criação processual de mapas, cada jogo que passa será único. Você pode usar vários dados de entrada, como hora ou nível atual do jogador, para alterar dinamicamente o conteúdo, mesmo após a montagem do jogo.

Sobre o que é este post?


Veremos algumas das maneiras mais comuns de criar mundos procedimentais, bem como algumas variações que eu criei. Aqui está um exemplo do que você pode criar depois de ler o artigo. Três algoritmos trabalham juntos para criar um mapa usando o Tilemap e o RuleTile :


No processo de gerar um mapa usando qualquer algoritmo, obtemos uma matriz int contendo todos os novos dados. Você pode continuar a modificar esses dados ou renderizá-los em um mapa de blocos.

Antes de ler mais, seria bom saber o seguinte:

  1. Distinguimos o que é um bloco e o que não está usando valores binários. 1 é uma peça, 0 é a sua ausência.
  2. Armazenaremos todos os cartões em um array inteiro bidimensional retornado ao usuário no final de cada função (exceto aquele em que a renderização é realizada).
  3. Usarei a função de matriz GetUpperBound () para obter a altura e a largura de cada mapa, para que a função receba menos variáveis ​​e o código seja mais limpo.
  4. Costumo usar Mathf.FloorToInt () , porque o sistema de coordenadas Tilemap é iniciado no canto inferior esquerdo, e Mathf.FloorToInt () permite arredondar números para um número inteiro.
  5. Todo o código nesta postagem está escrito em C #.

Geração de matriz


GenerateArray cria uma nova matriz int do tamanho especificado. Também podemos indicar se a matriz deve ser preenchida ou vazia (1 ou 0). Aqui está o código:

 public static int[,] GenerateArray(int width, int height, bool empty) { int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (empty) { map[x, y] = 0; } else { map[x, y] = 1; } } } return map; } 

Renderização de mapa


Esta função é usada para renderizar um mapa em um mapa de blocos. Fazemos um loop pela largura e altura do mapa, colocando blocos somente quando a matriz no ponto testado tiver o valor 1.

 public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile) { //Clear the map (ensures we dont overlap) tilemap.ClearAllTiles(); //Loop through the width of the map for (int x = 0; x < map.GetUpperBound(0) ; x++) { //Loop through the height of the map for (int y = 0; y < map.GetUpperBound(1); y++) { // 1 = tile, 0 = no tile if (map[x, y] == 1) { tilemap.SetTile(new Vector3Int(x, y, 0), tile); } } } } 

Atualização do mapa


Esta função é usada apenas para atualizar o mapa e não para renderizar novamente. Graças a isso, podemos usar menos recursos sem redesenhar cada bloco e seus dados.

 public static void UpdateMap(int[,] map, Tilemap tilemap) //Takes in our map and tilemap, setting null tiles where needed { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //We are only going to update the map, rather than rendering again //This is because it uses less resources to update tiles to null //As opposed to re-drawing every single tile (and collision data) if (map[x, y] == 0) { tilemap.SetTile(new Vector3Int(x, y, 0), null); } } } } 

Perlin de ruído


O ruído Perlin pode ser usado para vários propósitos. Primeiro, podemos usá-lo para criar a camada superior do nosso mapa. Para fazer isso, basta obter um novo ponto usando a posição atual xe seed.

Solução simples


Este método de geração usa a forma mais simples de realização do ruído Perlin na geração de níveis. Podemos usar a função Unity para ruído Perlin, para não escrevermos o código. Também usaremos apenas números inteiros para o mapa de blocos , usando a função Mathf.FloorToInt () .

 public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Create the Perlin for (int x = 0; x < map.GetUpperBound(0); x++) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1)); //Make sure the noise starts near the halfway point of the height newPoint += (map.GetUpperBound(1) / 2); for (int y = newPoint; y >= 0; y--) { map[x, y] = 1; } } return map; } 

Aqui está o que parece depois da renderização em um mapa de blocos:


Suavização


Você também pode pegar esta função e suavizá-la. Defina intervalos para fixar as alturas de Perlin e execute a suavização entre esses pontos. Essa função será um pouco mais complicada, pois para intervalos você precisa considerar listas de valores inteiros.

 public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) { //Smooth the noise and store it in the int array if (interval > 1) { int newPoint, points; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Used in the smoothing process Vector2Int currentPos, lastPos; //The corresponding points of the smoothing. One list for x and one for y List<int> noiseX = new List<int>(); List<int> noiseY = new List<int>(); //Generate the noise for (int x = 0; x < map.GetUpperBound(0); x += interval) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1)); noiseY.Add(newPoint); noiseX.Add(x); } points = noiseY.Count; 

Na primeira parte desta função, primeiro verificamos se o intervalo é maior que um. Nesse caso, gere ruído. A geração é realizada em intervalos para que a suavização possa ser aplicada. A próxima parte da função é suavizar os pontos.

 //Start at 1 so we have a previous position already for (int i = 1; i < points; i++) { //Get the current position currentPos = new Vector2Int(noiseX[i], noiseY[i]); //Also get the last position lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]); //Find the difference between the two Vector2 diff = currentPos - lastPos; //Set up what the height change value will be float heightChange = diff.y / interval; //Determine the current height float currHeight = lastPos.y; //Work our way through from the last x to the current x for (int x = lastPos.x; x < currentPos.x; x++) { for (int y = Mathf.FloorToInt(currHeight); y > 0; y--) { map[x, y] = 1; } currHeight += heightChange; } } } 

A suavização é realizada da seguinte maneira:

  1. Temos a posição atual e a última
  2. Temos a diferença entre dois pontos, a informação mais importante que precisamos é a diferença ao longo do eixo y
  3. Em seguida, determinamos o quanto a alteração precisa ser feita para chegar ao ponto; isso é feito dividindo a diferença em y pela variável de intervalo.
  4. Em seguida, começamos a definir posições, indo até zero
  5. Quando atingirmos 0 no eixo y, adicione a mudança de altura à altura atual e repita o processo para a próxima posição x
  6. Terminando com cada posição entre a última e a atual, passamos para o próximo ponto

Se o intervalo for menor que um, simplesmente usaremos a função anterior, que fará todo o trabalho por nós.

  else { //Defaults to a normal Perlin gen map = PerlinNoise(map, seed); } return map; 

Vamos dar uma olhada na renderização:


Passeio aleatório


Passeio aleatório


Esse algoritmo executa um lançamento de moeda. Podemos obter um de dois resultados. Se o resultado for “águia”, subimos um bloco para cima, se o resultado for “coroa”, então movemos o bloco para baixo. Isso cria alturas, movendo-se constantemente para cima ou para baixo. A única desvantagem de um algoritmo desse tipo é o seu bloqueio muito perceptível. Vamos dar uma olhada em como isso funciona.

 public static int[,] RandomWalkTop(int[,] map, float seed) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Set our starting height int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Cycle through our width for (int x = 0; x < map.GetUpperBound(0); x++) { //Flip a coin int nextMove = rand.Next(2); //If heads, and we aren't near the bottom, minus some height if (nextMove == 0 && lastHeight > 2) { lastHeight--; } //If tails, and we aren't near the top, add some height else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) { lastHeight++; } //Circle through from the lastheight to the bottom for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the map return map; } 


Parte superior aleatória da caminhada com anti-aliasing

Essa geração nos proporciona alturas mais suaves em comparação com a geração de ruído Perlin.

Essa variação do Random Walk fornece um resultado muito mais suave em comparação com a versão anterior. Podemos implementá-lo adicionando mais duas variáveis ​​à função:

  • A primeira variável é usada para determinar quanto tempo leva para manter a altura atual. É inteiro e é redefinido quando a altura muda
  • A segunda variável é inserida na função e é usada como a largura mínima da seção para altura. Tornar-se-á mais claro quando examinarmos a função.

Agora sabemos o que adicionar. Vamos dar uma olhada na função:

 public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Determine the start position int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Used to determine which direction to go int nextMove = 0; //Used to keep track of the current sections width int sectionWidth = 0; //Work through the array width for (int x = 0; x <= map.GetUpperBound(0); x++) { //Determine the next move nextMove = rand.Next(2); //Only change the height if we have used the current height more than the minimum required section width if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth) { lastHeight--; sectionWidth = 0; } else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth) { lastHeight++; sectionWidth = 0; } //Increment the section width sectionWidth++; //Work our way from the height down to 0 for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the modified map return map; } 

Como você pode ver no gif mostrado abaixo, suavizar o algoritmo de passeio aleatório permite obter belos segmentos planos no nível.


Conclusão


Espero que este artigo o inspire a usar a geração procedural em seus projetos. Se você quiser saber mais sobre mapas gerados proceduralmente, explore os excelentes recursos do Wiki de Geração de Procedimento ou Roguebasin.com .

Na segunda parte do artigo, usaremos a geração procedural para criar sistemas de cavernas.

Parte 2


Tudo o que discutiremos nesta parte pode ser encontrado neste projeto . Você pode baixar ativos e experimentar seus próprios algoritmos processuais.


Perlin de ruído


Na parte anterior, vimos maneiras de aplicar o ruído Perlin para criar as camadas superiores. Felizmente, o ruído de Perlin também pode ser usado para criar uma caverna. Isso é realizado calculando o novo valor de ruído Perlin, que recebe os parâmetros da posição atual multiplicados pelo modificador. O modificador é um valor de 0 a 1. Quanto maior o valor do modificador, mais caótica será a geração do Perlin. Em seguida, arredondamos esse valor para o número inteiro (0 ou 1), que armazenamos na matriz do mapa. Veja como isso é implementado:

 public static int[,] PerlinNoiseCave(int[,] map, float modifier, bool edgesAreWalls) { int newPoint; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (edgesAreWalls && (x == 0 || y == 0 || x == map.GetUpperBound(0) - 1 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; //Keep the edges as walls } else { //Generate a new point using Perlin noise, then round it to a value of either 0 or 1 newPoint = Mathf.RoundToInt(Mathf.PerlinNoise(x * modifier, y * modifier)); map[x, y] = newPoint; } } } return map; } 

Usamos o modificador em vez da semente porque os resultados da geração de Perlin parecem melhores quando multiplicados por um número de 0 a 0,5. Quanto menor o valor, mais irregular será o resultado. Dê uma olhada nos resultados da amostra. O GIF começa com um valor modificador de 0,01 e atinge incrementalmente um valor de 0,25.


A partir deste gif, pode-se ver que a geração Perlin a cada incremento simplesmente aumenta o padrão.

Passeio aleatório


Na parte anterior, vimos que você pode usar um sorteio para determinar onde a plataforma se moverá para cima ou para baixo. Nesta parte, usaremos a mesma ideia, mas
com duas opções adicionais para deslocamento à esquerda e à direita. Essa variação do algoritmo Random Walk nos permite criar cavernas. Para fazer isso, selecionamos uma direção aleatória, depois movemos nossa posição e excluímos o bloco. Continuamos esse processo até atingir o número necessário de peças que precisam ser destruídas. Até agora, usamos apenas 4 direções: cima, baixo, esquerda, direita.

 public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Define our start x position int floorX = rand.Next(1, map.GetUpperBound(0) - 1); //Define our start y position int floorY = rand.Next(1, map.GetUpperBound(1) - 1); //Determine our required floorAmount int reqFloorAmount = ((map.GetUpperBound(1) * map.GetUpperBound(0)) * requiredFloorPercent) / 100; //Used for our while loop, when this reaches our reqFloorAmount we will stop tunneling int floorCount = 0; //Set our start position to not be a tile (0 = no tile, 1 = tile) map[floorX, floorY] = 0; //Increase our floor count floorCount++; 

A função começa com o seguinte:

  1. Encontre a posição inicial
  2. Calcule o número de pisos a serem excluídos.
  3. Exclua o bloco na posição inicial
  4. Adicione um ao número de peças.

Então passamos para o while . Ele criará uma caverna:

 while (floorCount < reqFloorAmount) { //Determine our next direction int randDir = rand.Next(4); switch (randDir) { //Up case 0: //Ensure that the edges are still tiles if ((floorY + 1) < map.GetUpperBound(1) - 1) { //Move the y up one floorY++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase floor count floorCount++; } } break; //Down case 1: //Ensure that the edges are still tiles if ((floorY - 1) > 1) { //Move the y down one floorY--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Right case 2: //Ensure that the edges are still tiles if ((floorX + 1) < map.GetUpperBound(0) - 1) { //Move the x to the right floorX++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Left case 3: //Ensure that the edges are still tiles if ((floorX - 1) > 1) { //Move the x to the left floorX--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; } } //Return the updated map return map; } 

O que estamos fazendo aqui?


Bem, primeiro, com a ajuda de um número aleatório, escolhemos a direção a seguir. Em seguida, verificamos a nova direção com a switch case . Nesta declaração, verificamos se a posição é uma parede. Caso contrário, exclua o elemento com o bloco da matriz. Continuamos a fazer isso até atingir a área desejada. O resultado é mostrado abaixo:


Eu também criei minha própria versão dessa função, que também inclui instruções diagonais. O código da função é bastante longo; portanto, se você quiser vê-lo, faça o download do projeto no link no início desta parte do artigo.

Túnel direcional


Um túnel direcional começa em uma borda do mapa e atinge a borda oposta. Podemos controlar a curvatura e a rugosidade do túnel passando-os para a função de entrada. Também podemos definir o comprimento mínimo e máximo das partes do túnel. Vamos dar uma olhada na implementação:

 public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) { //This value goes from its minus counterpart to its positive value, in this case with a width value of 1, the width of the tunnel is 3 int tunnelWidth = 1; //Set the start X position to the center of the tunnel int x = map.GetUpperBound(0) / 2; //Set up our random with the seed System.Random rand = new System.Random(Time.time.GetHashCode()); //Create the first part of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, 0] = 0; } 

O que está havendo?


Primeiro, definimos o valor da largura. O valor da largura passará do valor com menos para positivo. Graças a isso, obteremos o tamanho necessário. Nesse caso, usamos o valor 1, que por sua vez nos dará uma largura total de 3, porque usamos os valores -1, 0, 1.

Em seguida, definimos a posição inicial em x, para isso ocupamos o meio da largura do mapa. Depois disso, podemos colocar um túnel na primeira parte do mapa.


Agora vamos entrar no resto do mapa.

  //Cycle through the array for (int y = 1; y < map.GetUpperBound(1); y++) { //Check if we can change the roughness if (rand.Next(0, 100) > roughness) { //Get the amount we will change for the width int widthChange = Random.Range(-maxPathWidth, maxPathWidth); //Add it to our tunnel width value tunnelWidth += widthChange; //Check to see we arent making the path too small if (tunnelWidth < minPathWidth) { tunnelWidth = minPathWidth; } //Check that the path width isnt over our maximum if (tunnelWidth > maxPathWidth) { tunnelWidth = maxPathWidth; } } //Check if we can change the curve if (rand.Next(0, 100) > curvyness) { //Get the amount we will change for the x position int xChange = Random.Range(-maxPathChange, maxPathChange); //Add it to our x value x += xChange; //Check we arent too close to the left side of the map if (x < maxPathWidth) { x = maxPathWidth; } //Check we arent too close to the right side of the map if (x > (map.GetUpperBound(0) - maxPathWidth)) { x = map.GetUpperBound(0) - maxPathWidth; } } //Work through the width of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, y] = 0; } } return map; } 

Geramos um número aleatório para comparação com o valor da rugosidade e, se for maior que esse valor, a largura do caminho pode ser alterada. Também verificamos o valor para não tornar a largura muito pequena. Na próxima parte do código, percorremos o mapa. Em cada estágio, ocorre o seguinte:

  1. Geramos um novo número aleatório comparado com o valor da curvatura. Como no teste anterior, se for maior que o valor, alteramos o ponto central do caminho. Também realizamos uma verificação para não ir além do mapa.
  2. Finalmente, estamos colocando um túnel na parte recém-criada.

Os resultados desta implementação são assim:


Autômatos celulares


Autômatos celulares usam células vizinhas para determinar se a célula atual está ativada (1) ou desativada (0). A base para determinar as células vizinhas é criada com base em uma grade de células gerada aleatoriamente. Geraremos essa grade de origem usando a função C # Random.Next .

Como temos algumas implementações diferentes de autômatos celulares, escrevi uma função separada para gerar essa grade básica. A função fica assim:

 public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) { //Seed our random number generator System.Random rand = new System.Random(seed.GetHashCode()); //Initialise the map int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //If we have the edges set to be walls, ensure the cell is set to on (1) if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; } else { //Randomly generate the grid map[x, y] = (rand.Next(0, 100) < fillPercent) ? 1 : 0; } } } return map; } 

Nesta função, você também pode definir se nossa grade precisa de paredes. Em todos os outros aspectos, é bastante simples. Verificamos um número aleatório com porcentagem de preenchimento para determinar se a célula atual está ativada. Veja o resultado:


O bairro de Moore


O bairro de Moore é usado para suavizar a geração inicial de autômatos celulares. O bairro de Moore fica assim:


As seguintes regras se aplicam ao bairro:

  • Verificamos o vizinho em cada uma das direções.
  • Se o vizinho for um bloco ativo, adicione um ao número de blocos circundantes.
  • Se o vizinho é um bloco inativo, não fazemos nada.
  • Se uma célula tiver mais de 4 blocos adjacentes, ative a célula.
  • Se a célula tiver exatamente 4 blocos adjacentes, não faremos nada com ela.
  • Repita até verificarmos cada bloco do mapa.

A função de verificação de vizinhança de Moore é a seguinte:

 static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* Moore Neighbourhood looks like this ('T' is our tile, 'N' is our neighbours) * * NNN * NTN * NNN * */ int tileCount = 0; for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++) { for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++) { if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1)) { //We don't want to count the tile we are checking the surroundings of if(neighbourX != x || neighbourY != y) { tileCount += map[neighbourX, neighbourY]; } } } } return tileCount; } 

Depois de verificar o ladrilho, usamos essas informações na função de suavização. Aqui, como na geração de autômatos celulares, pode-se indicar se as bordas do mapa devem ser paredes.

 public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1))) { //Set the edge to be a wall if we have edgesAreWalls to be true map[x, y] = 1; } //The default moore rule requires more than 4 neighbours else if (surroundingTiles > 4) { map[x, y] = 1; } else if (surroundingTiles < 4) { map[x, y] = 0; } } } } //Return the modified map return map; } 

É importante observar aqui que a função possui um loop for que executa a suavização do número especificado de vezes. Graças a isso, um cartão mais bonito é obtido.


Sempre podemos modificar esse algoritmo conectando salas se, por exemplo, houver apenas dois blocos entre elas.

Bairro Von Neumann


O bairro de von Neumann é outra maneira popular de implementar autômatos celulares. Para essa geração, usamos um bairro mais simples do que na geração Moore. O bairro fica assim:


As seguintes regras se aplicam ao bairro:

  • Verificamos os vizinhos imediatos do bloco, sem considerar os diagonais.
  • Se a célula estiver ativa, adicione um à quantidade.
  • Se a célula estiver inativa, não faça nada.
  • Se a célula tiver mais de 2 vizinhos, ativamos a célula atual.
  • Se a célula tiver menos de 2 vizinhos, tornaremos a célula atual inativa.
  • Se houver exatamente 2 vizinhos, não altere a célula atual.

O segundo resultado usa os mesmos princípios que o primeiro, mas expande a área do bairro.

Verificamos vizinhos com a seguinte função:

 static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* von Neumann Neighbourhood looks like this ('T' is our Tile, 'N' is our Neighbour) * * N * NTN * N * */ int tileCount = 0; //Keep the edges as walls if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1))) { tileCount++; } //Ensure we aren't touching the left side of the map if(x - 1 > 0) { tileCount += map[x - 1, y]; } //Ensure we aren't touching the bottom of the map if(y - 1 > 0) { tileCount += map[x, y - 1]; } //Ensure we aren't touching the right side of the map if(x + 1 < map.GetUpperBound(0)) { tileCount += map[x + 1, y]; } //Ensure we aren't touching the top of the map if(y + 1 < map.GetUpperBound(1)) { tileCount += map[x, y + 1]; } return tileCount; } 

Tendo recebido o número de vizinhos, podemos prosseguir para suavizar a matriz. Como antes, precisamos de um loop forpara concluir o número de iterações de suavização passadas para a entrada.

 public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //Get the surrounding tiles int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1))) { //Keep our edges as walls map[x, y] = 1; } //von Neuemann Neighbourhood requires only 3 or more surrounding tiles to be changed to a tile else if (surroundingTiles > 2) { map[x, y] = 1; } else if (surroundingTiles < 2) { map[x, y] = 0; } } } } //Return the modified map return map; } 

Como você pode ver abaixo, o resultado final é muito mais complicado do que o bairro de Moore:


Aqui, como nas proximidades de Moore, você pode executar um script adicional para otimizar as conexões entre as partes do mapa.

Conclusão


Espero que este artigo o inspire a usar algum tipo de geração processual em seus projetos. Se você não fez o download do projeto, pode obtê-lo aqui .

Source: https://habr.com/ru/post/pt471084/


All Articles