Help Disabling SurfaceGui's Facing Other Block Sides/Normals

Hey everyone,

My partner @Erikost3 and I have been working hard to enhance the performance of our building game. Specifically, we’re tackling the challenge of only displaying SurfaceGui elements that are actually visible based on geometry and disabling SurfaceGuis where its normal/side is facing another block or wall. We’ve made some progress in the past, but recently encountered performance issues related to raycasting from each block in the game.

One crucial part of our code that we’re struggling with involves this “or” statement:

or (v2 and v2.Instance and checkDistance(v2.Instance.Position, v.Position, 4, function(a, b) return a < b end)) then

However, we’ve come to realize that this solution needs further refinement. The whole caching mechanism has become quite convoluted, and there are some oddities in its behavior. We’re considering a fresh approach by recoding the entire section.

We’re encouraging your participation, offering a reward of $R2000 for effective code snippets that provide a working solution.

Your efforts could greatly benefit the Voxel game community. I’m planning to share the final open-source snippet so that others can also benefit from this solution. We’ve seen some previous problem-solving for this problem in the past on the forum, and so this time we’re hoping on leaving this thread full of valuable information.

Here’s what we’re aiming for:

1. We want the process of enabling and disabling SurfaceGuis to be self-contained, so that when a block is removed on the server, the associated SurfaceGuis are updated automatically.
2. Improved performance is a top priority. The current code is causing performance bottlenecks, and we’re keen on making the game smoother and more enjoyable for everyone.
3. Of course, the ultimate goal is a code solution that works seamlessly. We’re determined to overcome this challenge.

Feel free to experiment with the code yourselves. For testing purposes, simply add a 4x4x4 Anchored block to ReplicatedStorage, attach six SurfaceGui elements to it, each facing different normals, and name them accordingly. You can customize the ImageLabels as you see fit.

Your insights and expertise would be immensely valuable in helping us achieve our optimization goals. Thank you in advance for your time and efforts. :heart:

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera

local GridSize = 4

local buildMode = false
local previewBlock = game:GetService("ReplicatedStorage")["Glass"]:Clone()

local function SnapToGrid(position)
	local snappedPosition = Vector3.new(
		math.floor(position.X / GridSize + 0.5) * GridSize,
		math.floor(position.Y / GridSize + 0.5) * GridSize,
		math.floor(position.Z / GridSize + 0.5) * GridSize
	)
	return snappedPosition
end

local function CanPlaceBlock(position)
	return player:DistanceFromCharacter(position) < 30 
end

mouse.Button1Down:Connect(function()
	if buildMode then
		local hit = mouse.Target
		if hit and hit.Parent then
			local hitPosition = SnapToGrid(hit.Position - hit.CFrame.UpVector * (GridSize / 2))
			if CanPlaceBlock(hitPosition) then
				local newBlock = previewBlock:Clone()
				newBlock.Position = previewBlock.Position
				newBlock.BrickColor = BrickColor.random()
				newBlock.Parent = game.Workspace
			end
		end
	end
end)



local function getScreenPointRay(): Ray
	return camera:ScreenPointToRay(mouse.X, mouse.Y, 1)
end

local function addToIgnore(params: RaycastParams, _instance: Instance)
	local ignore = params.FilterDescendantsInstances or {}
	table.insert(ignore, _instance)
	params.FilterDescendantsInstances = ignore
end

local RaycastParameters = RaycastParams.new()
RaycastParameters.FilterType = Enum.RaycastFilterType.Exclude
addToIgnore(RaycastParameters, previewBlock)
addToIgnore(RaycastParameters, player.Character)

local RaycastParameters2 = RaycastParams.new()
RaycastParameters2.FilterType = Enum.RaycastFilterType.Include
addToIgnore(RaycastParameters2, previewBlock)

game.Players.ChildAdded:Connect(function(child)
	if child:IsA("Player") then
		addToIgnore(RaycastParameters, child.Character or child.CharacterAdded:Wait())
	end
end)

local function CastSurfaceGuisRays(Frame: CFrame, Length: number)
	
	Length = Length or 4
	
	local surfaces = {
		Back = -Frame.LookVector;
		Front = Frame.LookVector;
		Top = Frame.UpVector;
		Bottom = -Frame.UpVector;
		Right = Frame.RightVector;
		Left = -Frame.RightVector;
	}

	for Num, Surface in surfaces do
		surfaces[Num] = workspace:Raycast(Frame.Position, Surface * Length, RaycastParameters2) or false
	end
	
	return surfaces
end

local updatedCache: {} = {}
local function UpdateSurfaceGuis2(Block: BasePart, Depth: number, MaxDepth: number)
	
	local BlockRays = CastSurfaceGuisRays(Block.CFrame)
	MaxDepth = MaxDepth or 2
	Depth = Depth or 0
	
	for i, v in pairs(BlockRays) do
		local SurfaceGui:SurfaceGui = Block:FindFirstChild(i)
		
		if not SurfaceGui then
			continue
		end
		
		SurfaceGui.Enabled = not (v and v.Instance)
		--print(`{SurfaceGui.Name}:`, not (v and v.Instance))
		
		if v and v.Instance and Block.Transparency == v.Instance.Transparency and Depth < MaxDepth then
			if Depth == 0 and not updatedCache[v.Instance] then
				conn = game["Run Service"].Stepped:Connect(function()
					print("first")
					if (v.Instance.Position - Block.Position).Magnitude > 4 then
						UpdateSurfaceGuis2(v.Instance, Depth + 1, 0)
						updatedCache[v.Instance] = nil
						conn:Disconnect()
					end
				end)
				updatedCache[v.Instance] = conn
			end
			UpdateSurfaceGuis2(v.Instance, Depth + 1, MaxDepth)
		end
	end	
end

local function checkDistance(a: Vector3, b: Vector3, c: number, d)
	
	local diff = a-b
	
	return d((diff.x^2)+(diff.Y^2)+(diff.Z^2), c^2)
end

local function UpdateSurfaceGuis()
	
	for i, v in pairs(workspace:GetDescendants()) do
		if v:IsA("BasePart") and v:FindFirstChildOfClass("SurfaceGui") then
			local BlockRays = CastSurfaceGuisRays(v.CFrame)
			
			
			for i2, v2 in pairs(BlockRays) do
				local SurfaceGui:SurfaceGui = v:FindFirstChild(i2)
				
				if not SurfaceGui or (v2 and v2.Instance and checkDistance(v2.Instance.Position, v.Position, 4, function(a, b) return a < b end)) then
					continue
				end

				SurfaceGui.Enabled = not (v2 and v2.Instance)
				--print(`{SurfaceGui.Name}:`, not (v and v.Instance))
			end
		end
	end
end



local function UpdateBlock()
	if buildMode then
		--mouse.TargetFilter = previewBlock
		local hitPosition = mouse.Hit.p
		local ScreenPoint = getScreenPointRay()
		local _Ray:RaycastResult = workspace:Raycast(ScreenPoint.Origin, ScreenPoint.Direction * 1000, RaycastParameters)
		
		if _Ray and _Ray.Position then
			previewBlock.Parent = workspace
			local snappedPosition = SnapToGrid(_Ray.Position + (_Ray.Normal * 2))
			previewBlock.Position = snappedPosition
			
			UpdateSurfaceGuis(previewBlock)
			if CanPlaceBlock(snappedPosition) then
				previewBlock.Color = script:GetAttribute("PlaceColor")
			else
				previewBlock.Color = script:GetAttribute("NotPlaceColor")
			end
		else
			previewBlock.Parent = nil
		end
	end
end

-- Detect the "B" key press to toggle build mode
local function onKeyPress(input)
	if input.KeyCode == Enum.KeyCode.B then
		buildMode = not buildMode
		if buildMode then
			previewBlock.Parent = game.Workspace
		else
			previewBlock.Parent = nil
		end
	end
end

game:GetService("UserInputService").InputBegan:Connect(onKeyPress)
game:GetService("UserInputService").InputChanged:Connect(UpdateBlock)
1 Like