Gerador de caverna bidimensional aleatório

Prefácio


Se você está com preguiça de cuidar do seu tempo, fazendo um nível para o seu jogo, então você veio ao lugar certo.

Este artigo mostrará em detalhes como você pode usar um dos muitos outros métodos de geração usando o exemplo de terras altas e cavernas. Vamos considerar o algoritmo Aldous-Broder e como tornar a caverna gerada mais bonita.

No final da leitura do artigo, você deve obter algo parecido com isto:

Sumário


Teoria


Montanha


Para ser honesto, a caverna pode ser gerada a partir do zero, mas será de alguma maneira feia? No papel de "plataforma" para a colocação de minas, escolhi uma cadeia de montanhas.
Essa montanha é gerada de maneira simples: tenhamos uma matriz bidimensional e uma altura variável , inicialmente igual à metade do comprimento da matriz na segunda dimensão; nós apenas passamos pelas colunas e preenchemos algo com todas as linhas da coluna para um valor de altura variável , alterando-o com uma chance aleatória para cima ou para baixo.

Caverna


Para gerar as próprias masmorras, escolhi - como me pareceu - um excelente algoritmo. Em termos simples, pode ser explicado da seguinte forma: mesmo se tivermos duas (talvez dez) variáveis X e Y e uma matriz bidimensional de 50 por 50, atribuímos a essas variáveis ​​valores aleatórios em nossa matriz, por exemplo, X = 26 e Y = 28 . Depois disso, realizamos as mesmas ações várias vezes: obtemos um número aleatório de zero a

Númerodevariáveis2

, no nosso caso, até quatro ; e então, dependendo do número que caiu, mudamos
nossas variáveis:

switch (Random.Range(0, 4)) { case 0: X += 1; break; case 1: X -= 1; break; case 2: Y += 1; break; case 3: Y -= 1; break; } 

Então, é claro, verificamos se alguma variável caiu fora dos limites do nosso campo:

  X = X < 0 ? 0 : (X >= 50 ? 49 : X); Y = Y < 0 ? 0 : (Y >= 50 ? 49 : Y); 

Após todas essas verificações, fazemos algo nos novos valores X e Y para nossa matriz (por exemplo: adicione um ao elemento) .

 array[X, Y] += 1; 

Preparação


Para simplificar a implementação e a visualização de nossos métodos, desenharemos os objetos resultantes? Estou tão feliz que você não se importa! Faremos isso com o Texture2D .

Para funcionar, precisamos apenas de dois scripts:
ground_libray é o que o artigo irá girar em torno. Aqui nós geramos, limpamos e desenhamos
ground_generator é o que nosso ground_libray usará
Deixe o primeiro ser estático e não herdará nada:

 public static class ground_libray 

E o segundo é normal, apenas não precisaremos do método Update .

Além disso, vamos criar um objeto de jogo no palco, com o componente SpriteRenderer

Parte prática


Em que consiste?


Para trabalhar com dados, usaremos uma matriz bidimensional. Você pode levar uma variedade de tipos diferentes, de byte ou int , para Color , mas acredito que isso será melhor:

Novo tipo
Nós escrevemos isso em ground_libray .

 [System.Serializable] public class block { public float[] color = new float[3]; public block(Color col) { color = new float[3] { col.r, col.g, col.b }; } } 


Explicarei isso pelo fato de nos permitir salvar nossa matriz e modificá- la, se necessário.

Maciço


Antes de começarmos a gerar a montanha, designemos o local onde a armazenaremos .

No script ground_generator, escrevi isso:

  public int ground_size = 128; ground_libray.block[,] ground; Texture2D myT; 

ground_size - o tamanho do nosso campo (ou seja, a matriz será composta por 16384 elementos).
ground_libray.block [,] ground - este é o nosso campo de geração.
O Texture2D myT é o que basearemos .

Como vai funcionar?
O princípio do trabalho conosco será o seguinte - chamaremos alguns métodos de ground_libray de ground_generator , fornecendo o primeiro campo de base .

Vamos criar o primeiro método no script ground_libray:

Fabricação de montanha
  public static float mount_noise = 0.02f; public static void generate_mount(ref block[,] b) { int h_now = b.GetLength(1) / 2; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < h_now; y++) { b[x, y] = new block(new Color(0.7f, 0.4f, 0)); h_now += Random.value > (1.0f - mount_noise) ? (Random.value > 0.5 ? 1 : -1) : 0; } } 

E imediatamente tentaremos entender o que está acontecendo aqui: como eu disse, apenas examinamos as colunas de nossa matriz b , ao mesmo tempo alterando a variável de altura h_now , que originalmente era igual à metade 128 (64) . Mas ainda há algo novo - mount_noise . Essa variável é responsável pela chance de alterar o h_now , porque se você alterar a altura com muita frequência, a montanha parecerá um pente .

Cor
Eu imediatamente defini uma cor levemente acastanhada , que seja pelo menos um pouco - no futuro não precisaremos dela.

Agora vamos para ground_generator e escreva isso no método Start :

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); 

Inicializamos a variável ground, uma vez que precisava ser feita .
Depois, sem explicação, envie-o para ground_libray .
Então nós geramos a montanha.

Por que não consigo ver minha montanha?


Vamos agora desenhar o que temos!

Para desenhar, escreveremos o seguinte método em nossa ground_libray :

Desenhando
  public static void paint(block[,] b, ref Texture2D t) { t = new Texture2D(b.GetLength(0), b.GetLength(1)); t.filterMode = FilterMode.Point; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) { t.SetPixel(x, y, new Color(0, 0, 0, 0)); continue; } t.SetPixel(x, y, new Color( b[x, y].color[0], b[x, y].color[1], b[x, y].color[2] ) ); } t.Apply(); } 

Aqui não daremos mais a alguém nosso campo, apenas daremos uma cópia dele (embora, por causa da palavra classe, demos um pouco mais do que apenas uma cópia) . Também daremos nosso Texture2D a esse método.

As duas primeiras linhas: criamos nossa textura do tamanho do campo e removemos a filtragem .

Depois disso, percorremos todo o campo da matriz e onde não criamos nada (a classe precisa ser inicializada) - desenhamos uma caixa vazia, caso contrário, se não estiver vazia - desenhamos o que salvamos no elemento.

E, é claro, quando terminar, vamos para ground_generator e adicionamos isso:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); //   ground_libray.paint(ground, ref myT); GetComponent<SpriteRenderer>().sprite = Sprite.Create(myT, new Rect(0, 0, ground_size, ground_size), Vector3.zero ); 

Mas não importa o quanto desenhemos nossa textura, no jogo podemos ver apenas colocando essa tela em algo:

O SpriteRenderer não aceita o Texture2D em nenhum lugar , mas nada nos impede de criar um sprite a partir dessa textura - Sprite.Create ( textura , retângulo com as coordenadas do canto inferior esquerdo e superior direito , a coordenada do eixo ).

Essas linhas serão chamadas de as mais recentes, adicionaremos o restante acima do método de pintura !

Mina


Agora precisamos preencher nossos campos com cavernas aleatórias. Para tais ações, também criaremos um método separado em ground_libray . Gostaria de explicar imediatamente os parâmetros do método:

 ref block[,] b -     . int thick -    int size -         Color outLine -   

Caverna
  public static void make_cave(ref block[,] b, int thick, int size, Color outLine) { int xNow = Random.Range(0, b.GetLength(0)); int yNow = Random.Range(0, b.GetLength(1) / 2); for (int i = 0; i < size; i++) { b[xNow, yNow] = null; make_thick(ref b, thick, new int[2] { xNow, yNow }, outLine); switch (Random.Range(0, 4)) { case 0: xNow += 1; break; case 1: xNow -= 1; break; case 2: yNow += 1; break; case 3: yNow -= 1; break; } xNow = xNow < 0 ? 0 : (xNow >= b.GetLength(0) ? b.GetLength(0) - 1 : xNow); yNow = yNow < 0 ? 0 : (yNow >= b.GetLength(1) ? b.GetLength(1) - 1 : yNow); } } 

Para começar, declaramos nossas variáveis X e Y , mas eu as chamei de xNow e yNow, respectivamente.

O primeiro, ou seja , xNow , obtém um valor aleatório de zero ao tamanho do campo na primeira dimensão.

E o segundo - agora - também recebe um valor aleatório: de zero ao meio do campo na segunda dimensão. Porque Geramos nossa montanha do meio, a chance de ela crescer até o "teto" não é grande . Com base nisso, não considero relevante gerar cavernas no ar.

Depois disso, um loop ocorre imediatamente, cujo número de ticks depende do parâmetro de tamanho . A cada tick, atualizamos o campo nas posições xNow e yNow e somente então os atualizamos (as atualizações de campo podem ser colocadas no final - você não sentirá a diferença)

Existe também um método make_thick , nos parâmetros pelos quais passamos nosso campo , a largura do curso da caverna , a posição atual de atualização da caverna e a cor do curso :

Stroke
  static void make_thick (ref block[,] b, int t, int[] start, Color o) { for (int x = (start[0] - t); x < (start[0] + t); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - t); y < (start[1] + t); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) continue; b[x, y] = new block(o); } } } 

O método utiliza a coordenada inicial passada para ele e, à sua volta, rep-tura todos os blocos na cor o - tudo é muito simples!


Agora vamos adicionar esta linha ao nosso ground_generator :

 ground_libray.make_cave(ref ground, 2, 10000, new Color(0.3f, 0.3f, 0.3f)); 

Você pode instalar o script ground_generator como um componente em nosso objeto e verificar como ele funciona!



Mais sobre as cavernas ...
  • Para criar mais cavernas, você pode chamar o método make_cave várias vezes (use um loop)
  • Alterar o parâmetro de tamanho nem sempre aumenta o tamanho da caverna, mas geralmente se torna maior
  • Ao alterar o parâmetro thick , você aumenta significativamente o número de operações:
    se o parâmetro for 3, o número de quadrados em um raio de 3 será 36 ; portanto, com o tamanho do parâmetro = 40.000 , o número de operações será 36 * 40.000 = 1440000


Correção de caverna




Você já reparou que, nessa visão, a caverna não parece a melhor? Muitos detalhes extras (talvez você pense diferente) .

Para se livrar das inclusões de alguns # 4d4d4d , escreveremos este método em ground_libray :

Limpador
  public static void clear_caves(ref block[,] b) { for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) continue; if (solo(b, 2, 13, new int[2] { x, y })) b[x, y] = null; } } 

Mas será difícil entender o que está acontecendo aqui se você não souber o que a função solo faz:

  static bool solo (block[,] b, int rad, int min, int[] start) { int cnt = 0; for (int x = (start[0] - rad); x <= (start[0] + rad); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - rad); y <= (start[1] + rad); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) cnt += 1; else continue; if (cnt >= min) return true; } } return false; } 

Nos parâmetros desta função, nosso campo , o raio da verificação do ponto , o "limiar de destruição" e as coordenadas do ponto a ser verificado devem estar presentes.
Aqui está uma explicação detalhada do que essa função faz:
int cnt é o contador do "limite" atual
A seguir, dois ciclos que verificam todos os pontos ao redor daquele cujas coordenadas são passadas para iniciar . Se houver um ponto vazio , adicionamos um ao cnt . Ao atingirmos o "limiar da destruição" , retornamos a verdade - o ponto é supérfluo . Caso contrário, não a tocamos.

Defino o limite de destruição como 13 pontos vazios e o raio de verificação é 2 (ou seja, ele verifica 24 pontos, sem incluir o central)
Exemplo
Este permanecerá ileso, pois existem apenas 9 pontos vazios.



Mas este não teve sorte - cerca de 14 pontos vazios



Uma breve descrição do algoritmo: examinamos todo o campo e verificamos todos os pontos para ver se são necessários.

Em seguida, basta adicionar a seguinte linha ao nosso ground_generator :

 ground_libray.clear_caves(ref ground); 

Sumário


Como podemos ver, a maioria das partículas desnecessárias simplesmente desapareceu.

Adicione um pouco de cor


Nossa montanha parece muito monótona, acho chata.

Vamos adicionar um pouco de cor. Adicione o método level_paint ao ground_libray :

Pintura sobre as montanhas
  public static void level_paint(ref block[,] b, Color[] all_c) { for (int x = 0; x < b.GetLength(0); x++) { int lvl_div = -1; int counter = 0; int lvl_now = 0; for (int y = b.GetLength(1) - 1; y > 0; y--) { if (b[x, y] != null && lvl_div == -1) lvl_div = y / all_c.Length; else if (b[x, y] == null) continue; b[x, y] = new block(all_c[lvl_now]); lvl_now += counter >= lvl_div ? 1 : 0; lvl_now = (lvl_now >= all_c.Length) ? (all_c.Length - 1) : lvl_now; counter = counter >= lvl_div ? 0 : (counter += 1); } } } </ <cut />source>           .    ,       ,   .       ,      .          <b>Y </b>  ,      . </spoiler>     <b>ground_generator </b> : <source lang="cs"> ground_libray.level_paint(ref ground, new Color[3] { new Color(0.2f, 0.8f, 0), new Color(0.6f, 0.2f, 0.05f), new Color(0.2f, 0.2f, 0.2f), }); 

Escolhi apenas três cores: verde , vermelho escuro e cinza escuro .
Obviamente, você pode alterar o número de cores e os valores de cada uma. Aconteceu assim:

Sumário


Mas ainda parece muito rigoroso adicionar um pouco de aleatoriedade às cores, escreveremos esta propriedade em ground_libray :

Cores aleatórias
  public static float color_randomize = 0.1f; static float crnd { get { return Random.Range(1.0f - color_randomize, 1.0f + color_randomize); } } 

E agora nos métodos level_paint e make_thick , nas linhas em que atribuímos cores, por exemplo, em make_thick :

 b[x, y] = new block(o); 

Vamos escrever isso:

 b[x, y] = new block(o * crnd); 

E em level_paint

 b[x, y] = new block(all_c[lvl_now] * crnd); 


No final, tudo deve ficar assim:

Sumário



Desvantagens


Suponha que tenhamos um campo de 1024 a 1024, precisamos gerar 24 cavernas, cuja espessura das bordas será 4 e o tamanho é 80.000.

1024 * 1024 + 24 * 64 * 80.000 = 5.368.832.000.000 de operações.

Este método é adequado apenas para gerar pequenos módulos para o mundo do jogo, é impossível gerar algo muito grande por vez .

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


All Articles