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:
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áveis∗2
, 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
SpriteRendererParte 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 tipoNó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 .
CorEu 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);
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)
ExemploEste 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);
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:
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:
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 .