Help with voxel / grid-based building system

Before you read any of this, please note that I’m not cloning Minecraft. I just need the building system for a game I’m making.

So I’m making a building system “similar” to that of Minecraft.

I have run into a problem that I don’t know how to properly raycast to get both the position of the selection box and the position of the part to place.

This is my current code for raycasting and snapping:

function Snap(Position, Offset)
	if Position then
		return Vector3.new(math.floor(Position.X / TileSize + 0.5) * TileSize, math.floor(Position.Y / TileSize) * TileSize, math.floor(Position.Z / TileSize + 0.5) * TileSize) + Offset
	end
end

function HitPosition(Offset)
	local RayParams = RaycastParams.new()
	UpdateRaycastBlacklist()
	RayParams.FilterDescendantsInstances = RaycastBlacklist
	local RayDirection = ((Camera.CFrame.Position - Mouse.Hit.Position).Unit * -12.5)
	local Result = workspace:Raycast(Camera.CFrame.Position, RayDirection, RayParams)
	if Result and Result.Position then
		return Snap(Result.Position, Offset)
	else
		return Vector3.new(0, -100, 0)
	end
end

The grid selection has clear problems with it, and I dont even know where to get started for block placement I did think of using the Raycast normal, but I don’t think that would work.

Selection Box incorrect positions:
(In both of these screenshots, the box should be on the stone brick block)

  • For this one, I’m aware that the Offset is causing this, however the Offset is needed for the Selection box to not clip through the wall and be off grid with everything else (Plus, I think i would still be having this problem without the Offset)
    image
  • And this one, I dont even know whats causing this. Both screenshots are of the same block being hovered over.
    image

The way the box is probably fully correct according to code, but I want it to be pretty much 1:1 with Minecraft, and same with the building.

Saying this again, I’m not cloning Minecraft. I just need the building system for a game I’m making, and I would prefer if it wasn’t jank.

Edit: My grid size is 2.5 if you’re wondering

Edit 2: Managed to fix the Y snapping being off, updated the supplied code. X or Z offset (second image) is still a problem though.

Edit 3: Y snapping is wrong when you look at the floor, selection box is not on the block. Same problem as Image 2.

2 Likes

instead of passing Result.Position try passing Result.Instance.Position to get the actual block position instead of the position on the end of the block

Is the offset of the position, maybe just increase the size of the hitbox instead.

The offset I’m passing through is Vector3.new(0, 1.25, 0), which should only affect the Y axis, which to be fair it does.

My problems are just that the box isn’t being placed in the correct spot half the time.

It should be on the block, same position and all, but it isn’t only when you’re looking certain directions.

I’ll supply a video of the problem I have:

Its probably an easy fix that’s just going right over my head

Forgot to show in the video that it works as intended when looking up at a block:

I think what you suggested could work. You can get the raycast normal, multiply it by a small value like 0.1 and add it to Result.Position, then snap that position to the grid.
If you’re just selecting a block, you should inverse the normal so that it goes in the ground, not above. I made a quick diagram to show what I mean. The dotted line is our ray, the red arrow is the normal, the green one is the modified version of the normal. And the blue dot is Snap(Result.Position + Result.Normal * 0.1) for the first one, Snap(Result.Position + -Result.Normal * 0.1) for the second one

Also math.round() exists so you dont have to do math.floor(x + 0.5)

3 Likes

I have made a minecraft game before, I essentially got the face from the raycast and offset the part accordingly. Rather than messing around with the camera.

placeEV.OnServerEvent:Connect(function(plr,blockPlaced,surface,blockType)
	if blockPlaced ~= nil then
		if blockPlaced.Parent.Name == "Blocks" and blockPlaced.Name ~= "Ghost" then
			plr.Stats.blocksPlaced.Value = plr.Stats.blocksPlaced.Value+1
			local clone = blockPlaced:Clone()
			clone.Parent = workspace.Blocks
			if blockType == "Stone" then
				clone.Name = "Stone"
				clone.Material = Enum.Material.Slate
				clone.Transparency = 0
				clone.BrickColor = BrickColor.DarkGray()
			elseif blockType == "Dirt" then
				clone.Name = "Dirt"
				clone.Material = Enum.Material.Grass
				clone.Transparency = 0
				clone.Color = Color3.fromRGB(86, 66, 54)
			elseif blockType == "Glass" then
				clone.Name = "Glass"
				clone.Material = Enum.Material.Glass
				clone.Transparency = 0.5
				clone.Color = Color3.fromRGB(0, 255, 255)
			elseif blockType == "Glow" then
				clone.Name = "Glow"
				clone.Material = Enum.Material.Neon
				clone.Transparency = 0
				clone.Color = Color3.fromRGB(255, 255, 0)
				local Light = Instance.new("PointLight",clone)
				Light.Brightness = 8
			elseif blockType == "Wood" then
				clone.Name = "Wood"
				clone.Material = Enum.Material.WoodPlanks
				clone.Transparency = 0
				clone.Color = Color3.fromRGB(108, 88, 75)
			end
			if surface == Enum.NormalId.Top then
				clone.Position = Vector3.new(blockPlaced.Position.X,blockPlaced.Position.Y+5,blockPlaced.Position.Z)
			elseif surface == Enum.NormalId.Bottom then
				clone.Position = Vector3.new(blockPlaced.Position.X,blockPlaced.Position.Y-5,blockPlaced.Position.Z)
			elseif surface == Enum.NormalId.Left then
				clone.Position = Vector3.new(blockPlaced.Position.X-5,blockPlaced.Position.Y,blockPlaced.Position.Z)
			elseif surface == Enum.NormalId.Right then
				clone.Position = Vector3.new(blockPlaced.Position.X+5,blockPlaced.Position.Y,blockPlaced.Position.Z)
			elseif surface == Enum.NormalId.Front then
				clone.Position = Vector3.new(blockPlaced.Position.X,blockPlaced.Position.Y,blockPlaced.Position.Z-5)
			elseif surface == Enum.NormalId.Back then
				clone.Position = Vector3.new(blockPlaced.Position.X,blockPlaced.Position.Y,blockPlaced.Position.Z+5)
			end
			local sound = script.BlockBreak:Clone()
			sound.Parent = clone
			local random = math.random(1,3)
			if random == 1 then
				sound.PitchShiftSoundEffect.Octave = 1.1
			elseif random == 2 then
				sound.PitchShiftSoundEffect.Octave = 1
			elseif random == 3 then
				sound.PitchShiftSoundEffect.Octave = 0.9
			end
			sound:Play()
			wait(0.504)
			sound:Destroy()
		end
	end
end)

This may help, you could just do this but with your hitbox rather than placing.

Well damn, it did work! Thanks so much!

I just gotta script block placement now.

Working code if for whatever reason anyone wants it (also im not bothered switching to math.round :P)

function Snap(Position, Offset, NormalAffected, Normal)
	if NormalAffected then
		Position += -Normal
	else
		Position += Normal
	end
	if Position then
		return Vector3.new(math.floor(Position.X / TileSize + 0.5) * TileSize, math.floor(Position.Y / TileSize) * TileSize, math.floor(Position.Z / TileSize + 0.5) * TileSize) + Offset
	end
end

-- NormalAffected is a bool that gets sent through, true means the snapped position gets set in
function HitPosition(Offset, NormalAffected)
	local RayParams = RaycastParams.new()
	UpdateRaycastBlacklist()
	RayParams.FilterDescendantsInstances = RaycastBlacklist
	local RayDirection = ((Camera.CFrame.Position - Mouse.Hit.Position).Unit * -12.5)
	local Result = workspace:Raycast(Camera.CFrame.Position, RayDirection, RayParams)
	if Result and Result.Position then
		return Snap(Result.Position, Offset, NormalAffected, Result.Normal)
	else
		return Vector3.new(0, -100, 0)
	end
end
1 Like

I’ll probably be scripting placement myself, as I have a few more complicated blocks to script in, such as logs (Having them rotate depending what way the player is looking), torches, glass panes and eventually doors too.
I mostly have them figured out though, but thanks for the help!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.