../

pt-br en

Diffusion Automata v1

A ideia desse Automata é simular a dispersão de um liquido em um meio solido permeável, como solo, areia, etc. Ele foi criado como protótipo de um sistema de dispersão de água subterrânea para meu jogo.

Como vai perceber, não existe muita complexidade nesse código, essa é a magica desse tipo de algoritmo, com regras simples é possível construir comportamentos complexos. Aqui, simulamos a interação entre líquidos e sólidos no tempo e espaço usando apenas aritmética básica.

Nosso espaço será representado como um 2d array de vetores, mas no nosso caso eles servem apenas como uma forma conveniente de agrupar nossos dois valores relevantes:

  • Umidade
    • A quantidade de liquido contido naquela célula
  • Impermeabilidade
    • A resistência que o material daquela célula apresenta ao movimento de liquido

Os campos Rocks e Rain servem para demonstrar, respectivamente, células totalmente impermeáveis e células que sao fontes de umidade.

type HumidityBoard struct {
	values      [][]mgl32.Vec2 // [humidity, impermeability]
	Rocks, Rain [][]bool
	hvrX, hvrY  int
}

A lógica para determinar a umidade de uma célula é bastante simples, entendendo que ao longo do tempo o liquido tende a se espalhar uniformemente pelo espaço, assumimos que o nosso valor de umidade da célula é a média aritmética entre o seu valor atual e a das suas vizinhas. Por simplicidade assumimos apenas 4 vizinhos conforme esse diagrama onde v0 é a célula atual:

  v3  
v1 v0 v2
  v4  

Para considerar a permeabilidade, utilizamos uma média ponderada onde o peso de cada termo é definido pelo valor de impermeabilidade daquela célula. O detalhe é que consideramos o reciproco(1/valor) como peso das células vizinhas, dessa forma conseguimos o efeito de que uma célula altamente impermeável tende a não perder umidade ao mesmo tempo que resiste à absorção de mais liquido. Além disso, verificamos se a célula excede o nosso limite de 1024 e ajustamos então o valor para esse limite.

func (ba *HumidityBoard) Update() error {
	// Armazenamos o estado inicial do espaço para servir de referencia
	m0 := make([][]mgl32.Vec2, len(ba.values))
	copy(m0, ba.values)

	for x, row := range ba.values {
		for y, v0 := range row {
			// Pulamos o calculo de fontes de umidade e células com alto impermeabilidade
			if ba.Rain[x][y] || v0[1] >= (math.MaxFloat32/5)*4 {
				continue
			}

			// Assumimos que as bordas são células secas e impermeáveis
			v1 := mgl32.Vec2{0, math.MaxFloat32}
			v2 := mgl32.Vec2{0, math.MaxFloat32}
			v3 := mgl32.Vec2{0, math.MaxFloat32}
			v4 := mgl32.Vec2{0, math.MaxFloat32}

			// Verificamos se o vizinho existe e aplicamos os valores corretos
			if x > 0 {
				v1 = m0[x-1][y]
			}
			if x < len(m0)-1 {
				v2 = m0[x+1][y]
			}
			if y > 0 {
				v3 = m0[x][y-1]
			}
			if y < len(m0)-1 {
				v4 = m0[x][y+1]
			}

			// Calculamos a média aritmética ponderada
			r := ((v0[0] * (v0[1])) + (v1[0] / v1[1]) + (v2[0] / v2[1]) + (v3[0] / v3[1]) + (v4[0] / v4[1])) / (v0[1] + (1 / v1[1]) + (1 / v2[1]) + (1 / v3[1]) + (1 / v4[1]))

			// Limitamos os valores a um máximo de 1024
			if r > 1024 {
				r = 1024
			}

			// Atualizamos espaço com novo valor de umidade
			ba.values[x][y][0] = r
		}
	}
	return nil
}

O resultado no fim não é perfeito, há parâmetros que não são levados em consideração como velocidade e densidade do liquido, mas para o nosso caso já é suficiente. Outra limitação é quanto a conservação de massa do sistema. Aos poucos o volume total de umidade cai e isso causa o efeito de umidade desaparecendo espontaneamente, que é fisicamente impossível.

Como dito, esse é um protótipo e limitações como essa não são necessariamente problemas para aplicação em jogos. Vale lembrar que esse algoritmo foi escrito de forma síncrona, mas é totalmente possível adapta-lo para operar de forma paralelizada.