Issues with a terrain generation system that uses Hexagons

I’m relatively new to the DevForum, so I may have grammar or other issues!

  1. What do you want to achieve? I want to make a terrain generator similar to Minecraft, but using Hexagons!

  2. What is the issue? My generator script makes an error, namely:

attempt to compare number <= nil

  1. What solutions have you tried so far? I am using @ThanksRoBama
    s script from this post.

I modified their script so that it could work with hexagons, but it always return an error. this is the script:

local FractalNoise = require(game.ServerStorage.FractalNoise)
local heightmapNoise = FractalNoise(nil, 0.1, 2, 2, nil, nil) --These are just magic numbers
local swirlNoise = FractalNoise(nil, .01, 5, 2) --These are just magic numbers

function mapSet(map, x, y, z, value)
	map[x] = map[x] or {}
	map[x][y] = map[x][y] or {}
	map[x][y][z] = value
end

function mapGet(map, x, y, z)
	if map[x] then
		if map[x][y] then
			return map[x][y][z]
		end
	end
end

function twirlCoordinates(x, y, z, power)
	local power = power or 1
	local tX, tY, tZ = 	
		swirlNoise(x, y, z),
		swirlNoise(x+1000, y, z), --Don't want the *same* twirl on each axis
		swirlNoise(x, y+1000, z)

	return x + tX * power, y + tY * power, z + tZ * power
end

function heightMap(x, z)
	return heightmapNoise(x, 0, z)
end

function density(x, y, z)
	--If you twirl with power 0, you'll just get a plain heightmap
	local tX, tY, tZ = twirlCoordinates(x, y, z, 1)
	tZ = tZ / (1 + y)
	tX = tX / (1 + y)
	local densityOffset = 0.5 + heightMap(tX, tZ) - y --Add 0.5 density so that there's a guaranteed bottom layer
	return densityOffset
end

function generateChunk(mapSize, offset)
	local yMax = 8
	local yMin = 1
	print(offset)

	local x
	local y
	local z

	local densityMap = {}
	for x = 1, mapSize do
		for y = yMin, yMax do
			for z = 1, mapSize do
				mapSet(densityMap, x*offset["x"], y, z*offset["z"], density((x*offset["x"])/mapSize, y/(yMax), (z*offset["z"])/mapSize))
			end
		end
	end

	x, y, z = 0,0,0

	for x = 1, mapSize do
		for y = yMin, yMax do
			for z = 1, mapSize do
				local d = mapGet(densityMap, x*offset["x"], y, z*offset["z"]) 
				--print(d, x*offset["x"], y, z*offset["z"])

				if d >= 0 and d <= 0.14 then
					local block = workspace.blockBase.Base_Plane:Clone()
					--local block = Instance.new("Part", workspace)
					--block.Size = Vector3.new(4, 4, 4)

					if z % 2 == 0 then
						x += 3.732 * 0.5
					end
					z *= 3.732 * 0.75

					block.Anchored = true
					block.CFrame = CFrame.new((x*offset["x"])*4, y*4, (z*offset["z"])*3.732)
					block.Color = Color3.fromHSV(((y/mapSize)%1), .75, 1 - (y/mapSize))
					block.Parent = game.Workspace
				end

			end

		end
		game["Run Service"].Heartbeat:Wait()
	end
	x, y, z = 0,0,0
end


generateChunk(16,
	["x"] = 1,
	["z"] = 1
})

If you need the module, just get it from the link I gave.

Also, if you have questions, feel free to ask! :happy1:

5 Likes

Can you send the complete error message?

3 Likes

Here’s the error message. I’m sorry I’m late to reply!

ServerScriptService.Script:68: attempt to compare number <= nil - Server - Script:68
Stack Begin - Studio
Script ‘ServerScriptService.Script’, Line 68 - function generateChunk - Studio - Script:68
Script ‘ServerScriptService.Script’, Line 93 - Studio - Script:93
Stack End - Studio

If you want the file, I can give it to you here: terrain_generator_with_hexagons.rbxl (59.6 KB)

3 Likes

mapGet returns nil when x/y/z is a float and when i tested thats what happens, returns nil when x is 2.866

or when somehow it accesses an index higher than the map has

1 Like

so i dont know if you figured it out yet, but in case you didnt:
in generateChunk, in the if z % 2 == 0 then replace x += … with z +=

1 Like

I’ll try than now! Thanks for the tip!

1 Like

That helped, but it makes weird terrain. I’ll post an image of it.


Better camera angle:

1 Like

can you show how you want it to look like? draw an image maybe?

1 Like


This, but with more variation (terrain features)

1 Like

i guess, try changing the offsets so they are more connected

1 Like

That didn’t work. I tried. Anything I did messed up the positions of the hexagons.

1 Like

i don’t know, i tried making the offsets 0.5x smaller or so and they looked more realistic, i guess you need to experiment with sizes maybe?

1 Like

I did get it to do something, but it’s not enough.

The script is now:

local FractalNoise = require(game.ServerStorage.FractalNoise)
local heightmapNoise = FractalNoise(nil, 0.1, 2, 2, nil, nil) --These are just magic numbers
local swirlNoise = FractalNoise(nil, .01, 5, 2) --These are just magic numbers

function mapSet(map, x, y, z, value)
	map[x] = map[x] or {}
	map[x][y] = map[x][y] or {}
	map[x][y][z] = value
end

function mapGet(map, x, y, z)
	if map[x] then
		if map[x][y] then
			return map[x][y][z]
		end
	end
end

function twirlCoordinates(x, y, z, power)
	local power = power or 1
	local tX, tY, tZ = 	
		swirlNoise(x, y, z),
		swirlNoise(x+1000, y, z), --Don't want the *same* twirl on each axis
		swirlNoise(x, y+1000, z)

	return x + tX * power, y + tY * power, z + tZ * power
end

function heightMap(x, z)
	return heightmapNoise(x, 0, z)
end

function density(x, y, z)
	--If you twirl with power 0, you'll just get a plain heightmap
	local tX, tY, tZ = twirlCoordinates(x, y, z, 1)
	tZ = tZ / (1 + y)
	tX = tX / (1 + y)
	local densityOffset = 0.5 + heightMap(tX, tZ) - y --Add 0.5 density so that there's a guaranteed bottom layer
	return densityOffset
end

function generateChunk(mapSize, offset)
	local yMax = 8
	local yMin = 1
	print(offset)

	local x
	local y
	local z

	local densityMap = {}
	for x = 1, mapSize do
		for y = yMin, yMax do
			for z = 1, mapSize do
				mapSet(densityMap, x*offset["x"], y, z*offset["z"], density((x*offset["x"])/mapSize, y/(yMax), (z*offset["z"])/mapSize))
			end
		end
	end

	x, y, z = 0,0,0

	for x = 1, mapSize do
		for y = yMin, yMax do
			for z = 1, mapSize do
				local d = mapGet(densityMap, x*offset["x"], y, z*offset["z"]) 
				--print(d, x*offset["x"], y, z*offset["z"])

				if d >= 0 and d <= 0.14 then
					local block = workspace.blockBase.Base_Plane:Clone()
					--local block = Instance.new("Part", workspace)
					--block.Size = Vector3.new(4, 4, 4)

					if z % 2 == 0 then
						z += 3.732 * 0.5
					end
					z *= 3.732 * 0.25

					block.Anchored = true
					block.CFrame = CFrame.new((x*offset["x"])*4, y*4, (z*offset["z"])*3.732)
					block.Color = Color3.fromHSV(((y/mapSize)%1), .75, 1 - (y/mapSize))
					block.Parent = game.Workspace
				end

			end

		end
		game["Run Service"].Heartbeat:Wait()
	end
	x, y, z = 0,0,0
end


generateChunk(16, {
	["x"] = 1,
	["z"] = 1
})
1 Like

i just realized something, if z % 2 == 0 will make the offset 0.5 times less, and then also 0.25 times less, is that intended?

1 Like

I don’t know. However, I have another script that generates terrain like I want, but with no terrain features (no noise, 3d noise, etc.)

Here’s the script:
oldScript.lua (3.8 KB)

local tile = workspace.blockBase

--! change these values to match the X and Z size of your tile mesh.
local tileWidth = 3.732
local tileHeight = 4
local renderDistance = 1
local fogScreenSize = 16

local biomes = { Plains = {
	name = "Plains",
	resolution = 100,
	frequency = 0.5,
	amplitude = 10
},
Mountains = {
	name = "Mountains",
	resolution = 75,
	frequency = 2.5,
	amplitude = 10
}
}

local seed = -67865454874585

function getIfLife(x: number, z: number, y: number, biome, typeOfTerrain: string)
	--fix generator
	local xinter = (x/(biome["resolution"]*biome["frequency"]))
	local xincrement = x
	
	local zinter = (z/(biome["resolution"]*biome["frequency"]))
	local zincrement = z
	
	local noiseMap1 = math.clamp(math.noise(xincrement, zincrement, 1), -0.5, 0.5)*300
	--local noiseMap2 = math.clamp(math.noise(xincrement*-4, -zincrement*4, seed)/4, -0.5, 0.5)*300
	--local noiseMap3 = math.clamp(math.noise(xincrement*-16, -zincrement*16, seed)/16, -0.5, 0.5)*300
	
	--3d noise
	local noise3d1 = math.noise(xinter, zinter, y/biome["resolution"]*biome["frequency"])*biome["amplitude"]
	local noise3d2 = math.noise(xinter*2, zinter*2, y/biome["resolution"]*biome["frequency"]*2)*biome["amplitude"]*0.5
	local noise3d3 = math.noise(xinter*4, zinter*4, y/biome["resolution"]*biome["frequency"]*4)*biome["amplitude"]*0.25
	local noise3d4 = math.noise(xinter*8, zinter*8, y/biome["resolution"]*biome["frequency"]*8)*biome["amplitude"]*0.125
	
	local noise3dFull = noise3d1 * noise3d2 * noise3d3 * noise3d4
	
	local noise = math.clamp(((noiseMap1*noise3dFull or 0) + (noiseMap2 or 0) + (noiseMap3 or 0))/3,-1,1)
	
	local noise2 = math.noise(x*y,z*y,seed*y)
	print(noise, math.clamp(noise*2,-0.1,0.1))
	local output = math.clamp(noise*2,-0.1,0.1) > 0
	--print(noise, noise2, output, typeOfTerrain)
	return output
end


function AddTile(
	xPosition: number,
	zPosition: number,
	location: Model,
	ypos: number,
	hasHeight: boolean,
	isDirt: boolean,
	offsets,
	biome,
	isCaves
)
	xPosition *= tileWidth

	--? we offset the second row slightly by 1/2 the tile's width.
	if zPosition % 2 == 0 then
		xPosition += tileWidth * 0.5
	end
	zPosition *= tileHeight * 0.75

	local tile = tile:Clone()
	local terrainType = nil
	if isCaves then
		terrainType = "Cave"
	else
		terrainType = "Ground"
	end
	
	local lives = getIfLife(xPosition,zPosition,ypos,biome, terrainType)
	
	if lives == false then
		tile:Destroy()
		return end

	tile:SetPrimaryPartCFrame(CFrame.new(Vector3.new(xPosition, ypos, zPosition))* CFrame.Angles(0,math.rad(90),0))

	tile.Parent = location
end

function generateLayer(xoffset: number, y: number, zoffset: number, location: Model, biome, isCave: boolean)
	local xPos1
	local zPos1
	for zPos = 1, fogScreenSize do
		zPos1 = zPos
		for xPos = 1, fogScreenSize do
			xPos1 = xPos
			AddTile(xPos+(xoffset*fogScreenSize), zPos+(zoffset*fogScreenSize), location, (y+8)*tileHeight, true, true, {["X"] = xoffset, ["Z"] = zoffset}, biome, isCave)
		end
		game:GetService("RunService").Heartbeat:Wait()
	end

end

local zPos1
local xPos1

function generateChunk(xoffset: number, zoffset: number, biome)
	local location = Instance.new("Model")
	location.Parent = workspace.FogScreenObjects
	location.Name = "(" .. xoffset .. ", " .. zoffset .. ")"

	location:SetAttribute("Biome", biome.name)
	for i = 1, 16 do
		generateLayer(xoffset, i, zoffset, location, biome, true)
	end
	for i = 1, 4 do
		generateLayer(xoffset, i+16, zoffset, location, biome, false)
	end
end
for i = 1, renderDistance do
	for o = 1, renderDistance do
		local noise = math.noise(i,o,seed)
		if math.clamp(noise, 0, 1) == 1 then
			generateChunk(i,o, biomes.Plains)
		else
			generateChunk(i,o, biomes.Mountains)
		end
	end
end
1 Like

well you can try changing that to

if z % 2 == 0 then
    z += 3.732 * 0.5
else
    z += 3.732 * 0.25 --or maybe 0.75?
end

and see the results

1 Like

It is already set to that one. (The image is the result)

1 Like

no i mean, add the else condition

1 Like

Oh. My bad! :frowning_face: I’m crazy sometimes!

1 Like

That kind of helps. But it, since I implemented this new version of my placement system, has always put two Hexagons in one spot (that is why rows are skipped).


1 Like