Placement system shaking

I want to make a simple placement system but the CurrentBlock is shaking. I know the cause which is SnapOnTop function.

Code:

--!nonstrict

--Grandpa mod
--Placement stuff

--Error messages
local ERROR_MESSAGE_INVAILD_BLOCK = "Given block called %s is invaild (%s does not exist)."
local ERROR_MESSAGE_NO_ACTIVATION = "Forgot to run :Activate() thus can't place block or preview placing block."
local ERROR_MESSAGE_REINIT = "Cannot re-initialize singleton class 'PlacementSystem'"

--Numbers
local GRID_SIZE = 4
local LERP_SIZE = 0

--Strings
local CANCEL_KEY = "C"

--Services
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")

--Instances
local Blocks = script.Parent:WaitForChild("Blocks")
local Camera = workspace.CurrentCamera

--Variables
local MousePosition
local Placing = false
local Deleting = false
local CurrentBlock

local function SnapToGrid(Number)
	--Grid snapping

	local x = (Number / GRID_SIZE) + 0.5

	return math.floor(x) * GRID_SIZE
end

local function SnapOnTop(Y)
	--Snap on top of parts
	
	assert(CurrentBlock, ERROR_MESSAGE_NO_ACTIVATION)
	
	local YPos = Y
	
	if #CurrentBlock.PrimaryPart:GetTouchingParts() > 0 then
		for index, part in pairs(CurrentBlock.PrimaryPart:GetTouchingParts()) do
			YPos += (2 * part.Position.Y) --Same thing as (part.Position.Y + part.Position.Y) but shorter.
		end
	end
	
	return YPos
end

local function GetMouseWorldPosition(MousePosition)
	--Gets a Vector2 mouse position and spits out a Vector3 one.
	--First we get a UnitRay using Camera:ViewportPointToRay.
	--Then we raycast using the UnitRay and we return the raycast position

	local UnitRay = Camera:ViewportPointToRay(MousePosition.X, MousePosition.Y + 36, 0)
	local Raycast = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * 500)
	
	if Raycast then
		return Raycast.Position
	else
		return Vector3.new(0, 0.5, 0)
	end
end


local PlacementSystem = {}

local function KeyCancelBind(ActionName, InputState)
	if ActionName ~= "CancelPlacement" then
		return
	end

	if InputState == Enum.UserInputState.End then
		PlacementSystem:Deactivate()
	end
end


function PlacementSystem.Initialize(cancelKey, gridSize, lerpSize)
	CANCEL_KEY = cancelKey
	GRID_SIZE = gridSize
	LERP_SIZE = lerpSize
	
	PlacementSystem.Initialize = function() error(ERROR_MESSAGE_REINIT) end
	
	ContextActionService:BindAction("CancelPlacement", KeyCancelBind, false, Enum.KeyCode[CANCEL_KEY])
end

function PlacementSystem:Activate(blockName)
	--Turns on placing mode

	local block = Blocks:FindFirstChild(blockName)

	assert(block, ERROR_MESSAGE_INVAILD_BLOCK:format(blockName, blockName))
	block = block:Clone()

	Placing = true
	CurrentBlock = block

	block.Parent = workspace:WaitForChild("PlacementBlocks")
end

function PlacementSystem:Deactivate()
	--Turns off placing mode and destroys the block
	--ONLY SHOULD BE USED WHEN CANCELING A PLACEMENT!!!!
	
	if Deleting then
		return
	end

	local block = CurrentBlock --Simpler and less error-prone
	assert(block, ERROR_MESSAGE_NO_ACTIVATION)
	
	Deleting = true
	
	block.PrimaryPart.Delete:Play()
	block.PrimaryPart.Delete.Ended:Wait()

	Placing = false
	CurrentBlock = nil

	block:Destroy()
	
	Deleting = false
end

local function UpdatePlacing(dt) --A better method name	
	--Update the position of the CurrentBlock
	if not Placing or not CurrentBlock then --[[If we're placing but CurrentBlock is nil, surely something is wrong?]] 
		return 
	end

	local MousePosition = UserInputService:GetMouseLocation()
	MousePosition = GetMouseWorldPosition(MousePosition)
	
	local PosX, PosY, PosZ = SnapToGrid(MousePosition.X), SnapOnTop(0.5), SnapToGrid(MousePosition.Z)
	CurrentBlock:PivotTo(CurrentBlock.PrimaryPart.CFrame:Lerp(CFrame.new(PosX, PosY, PosZ), LERP_SIZE))
end

RunService:BindToRenderStep("Placing", Enum.RenderPriority.Input.Value, UpdatePlacing)


return PlacementSystem

robloxapp-20221230-1513184.wmv (708.5 KB)

UPDATE:It seems that in lower fps rate via device hardware, it makes the CurrentBlock even more shaker. This can affect users from mobile to xbox which can make the experience terrible for the player.

Looks like the block isn’t anchored, causing your system to fight for control with the physics system that constantly accelerates the block downwards.

The block is anchored.

gfdhdgfshfgsdrsfdggsfdagsdfa

Updated script:

--!nonstrict

--Grandpa mod
--Placement stuff

--Error messages
local ERROR_MESSAGE_INVAILD_BLOCK = "Given block called %s is invaild (%s does not exist)."
local ERROR_MESSAGE_NO_ACTIVATION = "Forgot to run :Activate() thus can't place block or preview placing block."
local ERROR_MESSAGE_REINIT = "Cannot re-initialize singleton class 'PlacementSystem'"

--Numbers
local GRID_SIZE = 4
local LERP_SIZE = 0
local ADD_ROTATION = 0
local PER_ADD_ROTATION = 0

--Strings
local CANCEL_KEY = "C"

--Services
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")

--Instances
local Blocks = script.Parent:WaitForChild("Blocks")
local Remotes = script.Parent:WaitForChild("Remotes")
local Camera = workspace.CurrentCamera

--Variables
local MousePosition
local Placing = false
local Deleting = false
local CurrentBlock

local function SnapToGrid(Number)
	--Grid snapping

	local x = (Number / GRID_SIZE) + 0.5

	return math.floor(x) * GRID_SIZE
end

local function SnapOnTopX(Y)
	--Snap on top of parts
	--The problem with this is it makes the part when stacking shakey

	assert(CurrentBlock, ERROR_MESSAGE_NO_ACTIVATION)

	local YPos = Y

	if #CurrentBlock.PrimaryPart:GetTouchingParts() > 0 then
		for index, part in pairs(CurrentBlock.PrimaryPart:GetTouchingParts()) do
			if part.Parent ~= CurrentBlock then
				YPos += (2 * part.Position.Y) --Same thing as (part.Position.Y + part.Position.Y) but shorter.
			end
		end
	end

	return YPos
end

local function SnapOnTop(Y, Target)
	--Snap on top of parts

	assert(CurrentBlock, ERROR_MESSAGE_NO_ACTIVATION)

	local YPos = Y
	
	if not Target then
		--If it has no target then use the old version
		YPos = Y
		print(Y)
	else
		--YPos += (Target.Position.Y / 2 + CurrentBlock.PrimaryPart.Position.Y / 2)
		YPos += Target.Size.Y
	end
	
	print(Target)
	return YPos
end

local function GetMouseTarget(MousePosition)
	--Returns the mouse target

	local parms = RaycastParams.new()
	parms.FilterType = Enum.RaycastFilterType.Blacklist
	parms.FilterDescendantsInstances = {CurrentBlock, workspace.Baseplate}

	local UnitRay = Camera:ViewportPointToRay(MousePosition.X, MousePosition.Y, 0)
	local Raycast = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * 500, parms)

	if Raycast then
		return Raycast.Instance
	end

	return nil
end

local function GetMouseWorldPosition(MousePosition)
	--Gets a Vector2 mouse position and spits out a Vector3 one.
	--First we get a UnitRay using Camera:ViewportPointToRay.
	--Then we raycast using the UnitRay and we return the raycast position

	local UnitRay = Camera:ViewportPointToRay(MousePosition.X, MousePosition.Y, 0)
	local Raycast = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * 500)

	if Raycast then
		return Raycast.Position
	else
		return Vector3.new(0, SnapOnTop(0.5, GetMouseTarget(UserInputService:GetMouseLocation())), 0)
	end
end

local PlacementSystem = {}

local function KeyCancelBind(ActionName, InputState)
	if ActionName ~= "CancelPlacement" then
		return
	end

	if InputState == Enum.UserInputState.End then
		PlacementSystem:Deactivate()
	end
end

local function KeyRotationBind(ActionName, InputState)
	if ActionName ~= "RotateBlock" or not CurrentBlock then
		return
	end

	if InputState == Enum.UserInputState.Begin then
		ADD_ROTATION += PER_ADD_ROTATION
	end
end

local function KeyPlaceBind(ActionName, InputState)
	if ActionName ~= "PlaceBlock" or not CurrentBlock then
		return
	end

	if InputState == Enum.UserInputState.Begin then
		Remotes.Place:FireServer(CurrentBlock.Name, CurrentBlock:GetPivot())
	end
end

function PlacementSystem.Initialize(cancelKey, gridSize, lerpSize, addRotation)
	CANCEL_KEY = cancelKey
	GRID_SIZE = gridSize
	LERP_SIZE = lerpSize
	PER_ADD_ROTATION = addRotation

	PlacementSystem.Initialize = function() error(ERROR_MESSAGE_REINIT) end

	ContextActionService:BindAction("CancelPlacement", KeyCancelBind, false, Enum.KeyCode[CANCEL_KEY])
	ContextActionService:BindAction("RotateBlock", KeyRotationBind, false, Enum.KeyCode.R)
	ContextActionService:BindAction("PlaceBlock", KeyPlaceBind, false, Enum.KeyCode.E)
end

function PlacementSystem:Activate(blockName)
	--Turns on placing mode

	local block = Blocks:FindFirstChild(blockName)

	assert(block, ERROR_MESSAGE_INVAILD_BLOCK:format(blockName, blockName))
	block = block:Clone()

	Placing = true
	CurrentBlock = block

	block.Parent = workspace:WaitForChild("PlacementBlocks")
end

function PlacementSystem:Deactivate()
	--Turns off placing mode and destroys the block
	--ONLY SHOULD BE USED WHEN CANCELING A PLACEMENT!!!!

	if Deleting then
		return
	end

	local block = CurrentBlock --Simpler and less error-prone
	assert(block, ERROR_MESSAGE_NO_ACTIVATION)

	Deleting = true
	Placing = false

	block.PrimaryPart.Delete:Play()
	block.PrimaryPart.Delete.Ended:Wait()

	CurrentBlock = nil

	block:Destroy()

	Deleting = false
end

local function UpdatePlacing(dt) --A better method name	
	--Update the position of the CurrentBlock
	if not Placing or not CurrentBlock then --[[If we're placing but CurrentBlock is nil, surely something is wrong?]]
		return 
	end

	local MousePosition = UserInputService:GetMouseLocation()
	MousePosition = GetMouseWorldPosition(MousePosition)

	local PosX, PosY, PosZ = SnapToGrid(MousePosition.X), SnapOnTop(0.5, GetMouseTarget(UserInputService:GetMouseLocation())), SnapToGrid(MousePosition.Z)
	CurrentBlock:PivotTo(CurrentBlock.PrimaryPart.CFrame:Lerp(CFrame.new(PosX, PosY, PosZ) * CFrame.Angles(0, math.rad(ADD_ROTATION), 0), LERP_SIZE * dt))
end

RunService:BindToRenderStep("Placing", Enum.RenderPriority.Input.Value, UpdatePlacing)


return PlacementSystem

This is a temporary solution, SnapOnTopX is the one I want fixed.

Okay yeah I see, I could have looked more carefully sorry about that ^.^

I’m not sure but I think this calculation makes more sense:

local function SnapOnTopX() --The current Y coordinate does not affect which Y coordinate we want - it's not part of the calculation
	--Snap on top of parts
	assert(CurrentBlock, ERROR_MESSAGE_NO_ACTIVATION)
	local Y

	--Removed the if statement checking if #TouchingParts > 0, because the loop won't run if there's 0 touching parts anyway
	for index, part in pairs(CurrentBlock.PrimaryPart:GetTouchingParts()) do
	    if part:IsDescendantOf(CurrentBlock) then
			continue
		end
		
		--Center of target part + half the height of target part = top of target part
		--top of target part + half the height of CurrentBlock = center of CurrentBlock
		Y = part.Position.Y + part.Size.Y / 2 + CurrentBlock.PrimaryPart.Size.Y / 2
	end

	return Y
end

Its better but creates this 2007 physics effect:
robloxapp-20221231-1343373.wmv (278.8 KB)

Found out doing this fixes the problem:

local function SnapOnTop(Y, Target)
	--Snap on top of parts

	assert(CurrentBlock, ERROR_MESSAGE_NO_ACTIVATION)

	local YPos = Y
	
	if not Target then
		--If it has no target then use the old version
		YPos = Y
		print(Y)
	else
		YPos += Target.Position.Y + Target.Size.Y / 2 --+ CurrentBlock.PrimaryPart.Size.Y / 2
		--YPos += Target.Size.Y
	end
	
	print(Target)
	return YPos
end

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