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.
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)