Um dos artigos mais populares do meu site é dedicado à
geração de mapas poligonais (
tradução em Habré). Criar tais cartões requer muito esforço. Mas não comecei com isso, mas com uma tarefa
muito mais simples, que descreverei aqui. Essa técnica simples permite criar esses cartões em menos de 50 linhas de código:
Não vou explicar como
comprar tais cartões: depende do idioma, biblioteca gráfica, plataforma etc. Vou apenas explicar como
preencher a matriz com dados do mapa.
O barulho
A maneira padrão de gerar mapas 2D é usar o ruído com uma banda de frequência limitada como um componente básico, como o ruído Perlin ou o ruído simplex. Aqui está a aparência da função de ruído:
Atribuímos um número de 0,0 a 1,0 a cada ponto no mapa. Nesta imagem, 0.0 é preto e 1.0 é branco.
Veja como definir a cor de cada ponto da grade na sintaxe de um idioma semelhante ao C:
for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } }
O loop funcionará da mesma maneira em Javascript, Python, Haxe, C ++, C #, Java e na maioria das outras linguagens populares, portanto, mostrarei isso em uma sintaxe do tipo C para que você possa convertê-lo para o idioma necessário. No restante do tutorial, mostrarei como o corpo do ciclo muda (
value[y][x]=…
linha
value[y][x]=…
) ao adicionar novas funções. A demonstração mostrará um exemplo completo.
Em algumas bibliotecas, será necessário alterar ou multiplicar os valores resultantes para retorná-los no intervalo de 0,0 a 1,0.
Altura
O ruído em si é apenas uma coleção de números. Precisamos dar
sentido a isso. A primeira coisa que você pode pensar é vincular o valor do ruído à altura (isso é chamado de "mapa de altura"). Vamos pegar o ruído mostrado acima e desenhá-lo como altura:
O código permaneceu quase o mesmo, exceto pelo loop interno. Agora fica assim:
elevation[y][x] = noise(nx, ny);
Sim, e é isso. Os dados do mapa permaneceram os mesmos, mas agora vou chamá-los de
elevation
(altura), não
value
.
Temos muitas colinas, mas nada mais. O que está errado?
Frequência
O ruído pode ser gerado em qualquer
frequência . Até agora, escolhi apenas uma frequência. Vamos ver como isso afeta.
Tente alterar o valor com o controle deslizante (no artigo original) e veja o que acontece em diferentes frequências:
Apenas muda a escala. A princípio, isso não parece muito útil, mas não é. Eu tenho mais um
tutorial (
tradução em Habré), que explica a
teoria : conceitos como frequência, amplitude, oitavas, ruído rosa e azul, e assim por diante.
elevation[y][x] = noise(freq * nx, freq * ny);
Às vezes, também é útil recordar o
comprimento de onda , que é o recíproco da magnitude. Quando a frequência é duplicada, o tamanho é reduzido apenas pela metade. Dobrar o comprimento de onda todos os dobra. Comprimento de onda é a distância medida em pixels / ladrilhos / metros ou em qualquer outra unidade que você selecionou para mapas. Está relacionado à frequência:
wavelength = map_size / frequency
.
Oitavas
Para tornar o mapa de altura mais interessante,
adicionaremos ruído com diferentes frequências :
elevation[y][x] = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 2 * ny);
Vamos misturar grandes colinas de baixa frequência com pequenas colinas de alta frequência em um mapa.
Mova o controle deslizante (no artigo original) para adicionar pequenas colinas à mistura:
Agora é muito mais parecido com o relevo fractal que precisamos! Podemos ter colinas e montanhas irregulares, mas ainda não temos planícies. Para fazer isso, você precisa de outra coisa.
Redistribuição
A função de ruído fornece valores entre 0 e 1 (ou de -1 a +1, dependendo da biblioteca). Para criar planícies planas, podemos
elevar a altura a uma potência .
Mova o controle deslizante (no artigo original) para obter diferentes graus.
e = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 4 * ny); elevation[y][x] = Math.pow(e, exponent);
Valores altos
abaixam as alturas médias das planícies e valores baixos elevam as alturas médias para os picos das montanhas. Precisamos omiti-los. Uso funções de poder porque são mais simples, mas você pode usar qualquer curva; Eu tenho uma
demonstração mais complicada.
Agora que temos um mapa de elevação realista, vamos adicionar biomas!
Biomas
O barulho dá números, mas precisamos de um mapa com florestas, desertos e oceanos. A primeira coisa que você pode fazer é transformar pequenas alturas em água:
function biome(e) { if (e < waterlevel) return WATER; else return LAND; }
Uau, isso já está se tornando um mundo processualmente gerado! Temos água, grama e neve. Mas e se precisarmos de mais? Vamos fazer uma sequência de água, areia, grama, floresta, savana, deserto e neve:
Alívio baseado na altura function biome(e) { if (e < 0.1) return WATER; else if (e < 0.2) return BEACH; else if (e < 0.3) return FOREST; else if (e < 0.5) return JUNGLE; else if (e < 0.7) return SAVANNAH; else if (e < 0.9) return DESERT; else return SNOW; }
Uau, isso parece ótimo! Para o seu jogo, você pode alterar os valores e os biomas. Crysis terá muito mais selva; Skyrim tem muito mais gelo e neve. Mas não importa como você altere os números, essa abordagem é bastante limitada. Os tipos de relevo correspondem a alturas, portanto formam faixas. Para torná-los mais interessantes, precisamos escolher biomas baseados em outra coisa. Vamos criar um
segundo mapa de ruído para umidade.
Acima está o barulho das alturas; ruído de umidade inferiorAgora vamos usar altura e umidade
juntos . Na primeira imagem mostrada abaixo, o eixo y é a altura (tirada da imagem acima) e o eixo x é a umidade (a segunda imagem é mais alta). Isso nos dá um mapa atraente:
Alívio baseado em dois valores de ruídoPequenas alturas são oceanos e costas. Grandes alturas são rochosas e com neve. No meio, obtemos uma ampla gama de biomas. O código fica assim:
function biome(e, m) { if (e < 0.1) return OCEAN; if (e < 0.12) return BEACH; if (e > 0.8) { if (m < 0.1) return SCORCHED; if (m < 0.2) return BARE; if (m < 0.5) return TUNDRA; return SNOW; } if (e > 0.6) { if (m < 0.33) return TEMPERATE_DESERT; if (m < 0.66) return SHRUBLAND; return TAIGA; } if (e > 0.3) { if (m < 0.16) return TEMPERATE_DESERT; if (m < 0.50) return GRASSLAND; if (m < 0.83) return TEMPERATE_DECIDUOUS_FOREST; return TEMPERATE_RAIN_FOREST; } if (m < 0.16) return SUBTROPICAL_DESERT; if (m < 0.33) return GRASSLAND; if (m < 0.66) return TROPICAL_SEASONAL_FOREST; return TROPICAL_RAIN_FOREST; }
Se necessário, você pode alterar todos esses valores de acordo com os requisitos do seu jogo.
Se não precisarmos de biomas, os gradientes suaves (consulte
este artigo ) podem criar cores:
Para biomas e gradientes, um único valor de ruído não fornece variabilidade suficiente, mas dois são suficientes.
Clima
Na seção anterior, usei a
altitude como substituto da
temperatura . Quanto maior a altura, menor a temperatura. No entanto, a latitude geográfica também afeta as temperaturas. Vamos usar a altura e a latitude para controlar a temperatura:
Perto dos pólos (grandes latitudes) o clima é mais frio, e no topo das montanhas (grandes alturas) o clima também é mais frio. Até agora, resolvi isso não muito difícil: para a abordagem correta desses parâmetros, você precisa de muitas configurações sutis.
Há também mudanças climáticas
sazonais . No verão e no inverno, os hemisférios norte e sul ficam mais quentes e frios, mas no equador a situação não muda muito. Muito também pode ser feito aqui, por exemplo, é possível simular os ventos e as correntes oceânicas predominantes, o efeito dos biomas no clima e o efeito médio dos oceanos nas temperaturas.
As ilhas
Em alguns projetos, eu precisava que as bordas do mapa fossem de água. Isso transforma o mundo em uma ou mais ilhas. Existem várias maneiras de fazer isso, mas usei uma solução bastante simples em meu gerador de mapas de polígonos: Alterei a altura como
e = e + a - b*d^c
, onde
d
é a distância do centro (em uma escala de 0-1). Outra opção é alterar
e = (e + a) * (1 - b*d^c)
. A constante
a
eleva tudo,
b
abaixa as bordas e
c
controla a taxa de declínio.
Não estou completamente satisfeito com isso e ainda há muito a ser explorado. Deveria ser Manhattan ou distância euclidiana? Deveria depender da distância ao centro ou da distância até a borda? A distância deve ser ao quadrado, ou linear, ou ter algum outro grau? Deveria ser adição / subtração, multiplicação / divisão ou qualquer outra coisa? No artigo original,
tente Adicionar, a = 0,1, b = 0,3, c = 2,0 ou
tente Multiplicar, a = 0,05, b = 1,00, c = 1,5. As opções adequadas a você dependem do seu projeto.
Por que manter as funções matemáticas padrão? Como contei no meu
artigo sobre danos no RPG (
tradução em Habré), todos (inclusive eu) usam funções matemáticas, como polinômios, distribuições exponenciais, etc., mas no computador não podemos nos limitar a eles. Podemos pegar
qualquer função de formação e usá-la aqui, usando a tabela de pesquisa
e = e + height_adjust[d]
. Até agora, não estudei esse assunto.
Ruído espetado
Em vez de elevar a altura a uma potência, podemos usar o valor absoluto para criar picos agudos:
function ridgenoise(nx, ny) { return 2 * (0.5 - abs(0.5 - noise(nx, ny))); }
Para adicionar oitavas, podemos variar as amplitudes das altas frequências para que apenas as montanhas recebam o ruído adicional:
e0 = 1 * ridgenoise(1 * nx, 1 * ny); e1 = 0.5 * ridgenoise(2 * nx, 2 * ny) * e0; e2 = 0.25 * ridgenoise(4 * nx, 4 * ny) * (e0+e1); e = e0 + e1 + e2; elevation[y][x] = Math.pow(e, exponent);
Como não tenho muita experiência com essa técnica, preciso experimentar para aprender como usá-la bem. Também pode ser interessante misturar ruídos pontiagudos de baixa frequência com ruídos não pontiagudos de alta frequência.
Terraços
Se arredondarmos a altura para os próximos n níveis, obteremos terraços:
Este é o resultado da aplicação da função de redistribuição de altura no formato
e = f(e)
. Acima, usamos
e = Math.pow(e, exponent)
para afiar os picos das montanhas; aqui usamos
e = Math.round(e * n) / n
para criar terraços. Se você usar uma função que não seja degrau, os terraços poderão ser arredondados ou ocorrer apenas em determinadas alturas.
Posicionamento em árvore
Geralmente usamos ruído fractal para altura e umidade, mas também pode ser usado para colocar objetos espaçados de maneira desigual, como árvores e pedras. Para altura, usamos amplitudes altas com baixas frequências ("ruído vermelho"). Para colocar objetos, você precisa usar altas amplitudes com altas frequências ("ruído azul"). À esquerda, há um padrão de ruído azul; à direita, são locais onde o ruído é maior que os valores adjacentes:
for (int yc = 0; yc < height; yc++) { for (int xc = 0; xc < width; xc++) { double max = 0;
Escolhendo R diferente para cada bioma, podemos obter uma densidade variável de árvores:
É ótimo que esse ruído possa ser usado para colocar árvores, mas outros algoritmos geralmente são mais eficazes e criam uma distribuição mais uniforme: pontos Poisson, ladrilhos Van ou pontilhamento gráfico.
Para o infinito e além
Os cálculos do bioma na posição (x, y) são independentes dos cálculos de todas as outras posições. Essa
computação local possui duas propriedades convenientes: pode ser calculada em paralelo e pode ser usada para terrenos sem fim.
Coloque o cursor do mouse no minimapa (no artigo original) à esquerda para gerar o mapa à direita. Você pode gerar qualquer parte do cartão sem gerar (e mesmo sem armazenar) o cartão inteiro.


Implementação
Usar o ruído para gerar terreno é uma solução popular e, na Internet, você encontra tutoriais para diversos idiomas e plataformas. O código para gerar cartões em diferentes idiomas é aproximadamente o mesmo. Aqui está o loop mais simples em três idiomas diferentes:
- Javascript:
let gen = new SimplexNoise(); function noise(nx, ny) {
- C ++:
module::Perlin gen; double noise(double nx, double ny) {
- Python:
from opensimplex import OpenSimplex gen = OpenSimplex() def noise(nx, ny):
Todas as bibliotecas de ruído são muito parecidas. Tente
opensimplex para Python , ou
libnoise para C ++ , ou
simplex-noise para Javascript. Para os idiomas mais populares, existem muitas bibliotecas de ruído. Ou você pode aprender como funciona o ruído Perlin ou perceber o ruído você mesmo.
Eu não fiz isso.Em diferentes bibliotecas de ruído para o seu idioma, os detalhes do aplicativo podem variar um pouco (alguns números de retorno no intervalo de 0,0 a 1,0, outros no intervalo de -1,0 a +1,0), mas a idéia básica é a mesma. Para um projeto real, pode ser necessário agrupar a função de
noise
e o objeto
gen
em uma classe, mas esses detalhes são irrelevantes, então eu os tornei globais.
Para um projeto tão simples, não importa o ruído usado: ruído Perlin, ruído simplex, ruído OpenSimplex, ruído de valor, deslocamento do ponto médio, algoritmo de diamante ou a transformada inversa de Fourier. Cada um deles tem seus prós e contras, mas para um gerador de cartões semelhante, todos criam mais ou menos os mesmos valores de saída.
A renderização do mapa depende da plataforma e do jogo, então eu não o implementei; esse código é necessário apenas para gerar alturas e biomas, cuja renderização depende do estilo usado no jogo. Você pode copiar, portar e usá-lo em seus projetos.
Os experimentos
Eu olhei para misturar oitavas, elevar graus a uma potência e combinar alturas com umidade para criar um bioma.
Aqui você pode estudar um gráfico interativo que permite experimentar todos esses parâmetros, o que mostra em que consiste o código:
Aqui está um código de exemplo:
var rng1 = PM_PRNG.create(seed1); var rng2 = PM_PRNG.create(seed2); var gen1 = new SimplexNoise(rng1.nextDouble.bind(rng1)); var gen2 = new SimplexNoise(rng2.nextDouble.bind(rng2)); function noise1(nx, ny) { return gen1.noise2D(nx, ny)/2 + 0.5; } function noise2(nx, ny) { return gen2.noise2D(nx, ny)/2 + 0.5; } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var nx = x/width - 0.5, ny = y/height - 0.5; var e = (1.00 * noise1( 1 * nx, 1 * ny) + 0.50 * noise1( 2 * nx, 2 * ny) + 0.25 * noise1( 4 * nx, 4 * ny) + 0.13 * noise1( 8 * nx, 8 * ny) + 0.06 * noise1(16 * nx, 16 * ny) + 0.03 * noise1(32 * nx, 32 * ny)); e /= (1.00+0.50+0.25+0.13+0.06+0.03); e = Math.pow(e, 5.00); var m = (1.00 * noise2( 1 * nx, 1 * ny) + 0.75 * noise2( 2 * nx, 2 * ny) + 0.33 * noise2( 4 * nx, 4 * ny) + 0.33 * noise2( 8 * nx, 8 * ny) + 0.33 * noise2(16 * nx, 16 * ny) + 0.50 * noise2(32 * nx, 32 * ny)); m /= (1.00+0.75+0.33+0.33+0.33+0.50); } }
Existe uma dificuldade: para ruídos de altura e umidade, é necessário usar uma semente diferente; caso contrário, elas serão as mesmas e as cartas não parecerão tão interessantes. Em Javascript, eu uso
a biblioteca prng-parkmiller ; em C ++, você pode usar dois
objetos linear_congruential_engine separados; no Python, você pode criar duas instâncias separadas de uma
classe random.Random .
Pensamentos
Eu gosto dessa abordagem para mapear a geração por sua
simplicidade . É rápido e requer muito pouco código para produzir resultados decentes.
Não gosto das limitações dele nessa abordagem. Cálculos locais significam que cada ponto é independente de todos os outros. Áreas diferentes do mapa
não estão
conectadas entre si . Cada lugar no mapa "parece" o mesmo. Não há restrições globais, por exemplo, “deve haver de 3 a 5 lagos no mapa” ou recursos globais, como um rio que flui do topo do pico mais alto para o oceano. Além disso, não gosto do fato de que, para obter uma boa imagem, você precisa configurar os parâmetros por um longo tempo.
Por que eu recomendo? Penso que este é um bom ponto de partida, especialmente para jogos indie e compotas de jogos. Dois dos meus amigos escreveram a versão inicial de
Realm of the Mad God em apenas 30 dias para um
concurso de jogos . Eles me pediram para ajudar a criar mapas. Eu usei essa técnica (além de mais alguns recursos que acabaram não sendo muito úteis) e fiz um mapa para eles. Alguns meses depois, depois de receber feedback dos jogadores e estudar cuidadosamente o design do jogo, criamos um gerador de mapas mais avançado, baseado nos polígonos de Voronoi, descritos
aqui (
tradução em Habré). Este gerador de cartões não usa as técnicas descritas neste artigo. Ele usa ruído para criar mapas de uma maneira completamente diferente.
Informações Adicionais
Há
muitas coisas legais que você pode fazer com as funções de ruído. Se você pesquisar na Internet, poderá encontrar opções como turbulência, ondulação, multifractal sulcado, amortecimento de amplitude, terraços, ruído voronoi, derivados analíticos, distorção de domínio e outros. Você pode usar
esta página como fonte de inspiração. Não os considero aqui, meu artigo se concentra na simplicidade.
Este projeto foi influenciado pelos meus projetos anteriores de geração de mapas:
- Usei o ruído geral de Perlin no meu primeiro gerador de cartões Reino do Deus Louco . Nós o usamos nos primeiros seis meses de teste alfa e, em seguida, o substituímos por um gerador de mapas nos polígonos Voronoi , criado especialmente para os requisitos de jogabilidade que determinamos durante o teste alfa. Os biomas e suas cores para o artigo são retirados desses projetos.
- Ao estudar o processamento de sinais de áudio, escrevi um tutorial sobre ruído que explica conceitos como frequência, amplitude, oitavas e a "cor" do ruído. Os mesmos conceitos que funcionam para o som também se aplicam à geração de cartões com base em ruído. Naquele momento, criei uma geração bruta de alívio de demonstração , mas não a terminei.
- Às vezes, experimento encontrar limites. Eu queria saber quanto código é minimamente necessário para criar mapas atraentes. Neste mini-projeto, cheguei a zero linha de código - tudo é feito com filtros de imagem (turbulência, limites, gradientes de cores). Isso me deixou feliz e triste. Até que ponto a geração de mapas pode ser realizada por filtros de imagem? Em grande o suficiente. Tudo o que foi descrito acima sobre o "esquema de gradientes de cores suaves" é retirado desta experiência. A camada de ruído é um filtro de imagem de turbulência; oitavas são imagens sobrepostas; A ferramenta de graduação é chamada de "correção de curva" no Photoshop.
O que me incomoda um pouco é que a maior parte do código que os desenvolvedores de jogos escrevem para a geração de terreno com base em ruído (incluindo deslocamento do ponto médio) acaba sendo a mesma que nos filtros de som e imagem. Por outro lado, ele cria resultados bastante decentes em apenas algumas linhas de código, e foi por isso que escrevi este artigo. Este é um
ponto de referência rápido e fácil . Normalmente, não uso essas cartas por muito tempo, mas as substituo por um gerador de mapas mais complexo assim que descobrir quais tipos de cartas são mais adequados ao design do jogo. Para mim, esse é um padrão padrão: começar com algo extremamente simples e substituí-lo somente depois que eu entender melhor o sistema com o qual estou trabalhando.
Há
muito mais coisas que podem ser feitas com ruído, no artigo que mencionei apenas algumas. Experimente o
Noise Studio para testar interativamente vários recursos.