How do I make placement rotation work for mobile

I made a placement system but I am having trouble figuring out how to make the rotation work on mobile.

local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local CollectionService = game:GetService("CollectionService")

local ClientModules = ReplicatedStorage:WaitForChild("ClientModules")
local inventory = require(ClientModules:WaitForChild("InventoryClient"))

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

local matchRotation = false
local gridSize = 1

local rotationIncrement = math.rad(15)
local tiltIncrement = math.rad(15)
local currentRotation = 0
local currentTilt = 0

local module = {}

local mobileConfirmation = script.BuildConfirmationMobile
local autoplace = false

local autoplaceOnColor = Color3.fromRGB(140, 0, 255)
local autoplaceOffColor = Color3.fromRGB(46, 0, 86)

local ghostModel
local renderConn
local inputConn
local rotConn
local tiltConn
local inputBeganConn
local enabled = false

local lastPlacementCFrame = nil

local newModels = {}

local function addBlockSound(pivot, sound)
	local soundPartPos = Instance.new("Part")
	soundPartPos.Anchored = true
	soundPartPos.Transparency = 1
	soundPartPos.CanCollide = false
	soundPartPos.Size = Vector3.new(0.01,0.01,0.01)
	local soundPartParent = Instance.new("Model")
	soundPartParent.Name = "SoundPart"
	soundPartPos.Parent = soundPartParent
	soundPartParent:PivotTo(pivot)
	soundPartParent.Parent = workspace

	sound.Parent = soundPartPos
	sound:Play()

	game.Debris:AddItem(soundPartParent, sound.TimeLength+0.1)
end

local function placeServer(model, pivot, blockId)
	local blockFunc = ReplicatedStorage.Remotes.PlaceBlockServer:InvokeServer(pivot, blockId)
	if blockFunc.Result == "Success" or blockFunc.Result == "Rejected" then
		model:Destroy()
		if blockFunc.Result == "Success" then
			inventory:PlaceBlockUpdate(blockId, blockFunc.Result)

			local sound = script.PlaceSound:Clone()
			addBlockSound(pivot, sound)
		end
		if blockFunc.Result == "Rejected" then
			inventory:PlaceBlockUpdate(blockId, blockFunc.Result)

			local sound = script.RejectPlaceSound:Clone()
			addBlockSound(pivot, sound)
		end
	end
end

local function roundToGrid(value)
	return math.round(value / gridSize) * gridSize
end

local function anchorModel(model, anchored)
	for _, part in pairs(model:GetDescendants()) do
		if part:IsA("BasePart") then
			part.Anchored = anchored
		end
	end
end

local function setModelTransparency(model, transparency)
	for _, part in pairs(model:GetDescendants()) do
		if part:IsA("BasePart") then
			part.Transparency = transparency
			part.CanCollide = false
		end
	end
end

local function getModelSize(model)
	return model:GetExtentsSize()
end

local function getPlacementCFrame()
	if not ghostModel then return nil end

	local origin = camera.CFrame.Position
	local direction = (mouse.Hit.Position - origin).Unit * 1000

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {player.Character, ghostModel}
	rayParams.FilterType = Enum.RaycastFilterType.Exclude

	local result = workspace:Raycast(origin, direction, rayParams)
	if not result then return nil end

	local hitPos = result.Position
	local normal = result.Normal
	local target = result.Instance
	if CollectionService:HasTag(target.Parent, "IgnorePlacement") then return end

	local function getOffsetInNormalDirection(modelCFrame, modelSize, normal)
		local halfSize = modelSize / 2
		local axisX = modelCFrame.RightVector * halfSize.X
		local axisY = modelCFrame.UpVector * halfSize.Y
		local axisZ = modelCFrame.LookVector * halfSize.Z

		return math.abs(normal:Dot(axisX)) + math.abs(normal:Dot(axisY)) + math.abs(normal:Dot(axisZ))
	end

	local size = getModelSize(ghostModel)

	local up = normal.Unit
	local targetLook = target.CFrame.LookVector
	local targetRight = target.CFrame.RightVector

	local forward = (targetLook - up * targetLook:Dot(up))
	if forward.Magnitude < 0.001 then
		forward = (targetRight - up * targetRight:Dot(up))
	end
	forward = forward.Unit

	local right = forward:Cross(up).Unit
	local baseCFrame = CFrame.fromMatrix(Vector3.zero, right, up)
	local rotationCFrame = CFrame.Angles(currentTilt, currentRotation, 0)

	local modelOrientationCFrame
	if matchRotation then
		modelOrientationCFrame = CFrame.fromMatrix(Vector3.zero, right, up) * CFrame.Angles(currentTilt, currentRotation, 0)
	else
		modelOrientationCFrame = CFrame.Angles(currentTilt, currentRotation, 0)
	end

	local offset = getOffsetInNormalDirection(modelOrientationCFrame, size, normal)

	local exactPos = hitPos + normal * offset
	local targetCFrame = target.CFrame
	local localPos = targetCFrame:PointToObjectSpace(exactPos)
	local localNormal = targetCFrame:VectorToObjectSpace(normal)

	local snapX = math.abs(localNormal.X) < 0.9 and roundToGrid(localPos.X) or localPos.X
	local snapY = math.abs(localNormal.Y) < 0.9 and roundToGrid(localPos.Y) or localPos.Y
	local snapZ = math.abs(localNormal.Z) < 0.9 and roundToGrid(localPos.Z) or localPos.Z
	local snappedLocalPos = Vector3.new(snapX, snapY, snapZ)
	local snappedWorldPos = targetCFrame:PointToWorldSpace(snappedLocalPos)

	local finalBaseCFrame = CFrame.fromMatrix(snappedWorldPos, right, up)
	return finalBaseCFrame * rotationCFrame

end

local function onInputBegan(input, gameProcessed)
	if gameProcessed then return end
	if not enabled then return end
	if input.UserInputType == Enum.UserInputType.Keyboard then
		if input.KeyCode == Enum.KeyCode.T then
			currentRotation = currentRotation + rotationIncrement
		elseif input.KeyCode == Enum.KeyCode.R then
			currentTilt = currentTilt + tiltIncrement
		end
	end
end

local yesConn
local noConn
local autoConn

local function placeBlock(model, pivot, blockId)
	model:PivotTo(pivot)

	if UserInputService.TouchEnabled == true then
		mobileConfirmation.Parent = player.PlayerGui

		if yesConn then yesConn:Disconnect() end
		if noConn then noConn:Disconnect() end
		if autoConn then autoConn:Disconnect() end

		yesConn = mobileConfirmation.Yes.MouseButton1Click:Connect(function()
			mobileConfirmation.Parent = script
			placeServer(model, pivot, blockId)
			if yesConn then yesConn:Disconnect() end
			if noConn then noConn:Disconnect() end
		end)

		noConn = mobileConfirmation.No.MouseButton1Click:Connect(function()
			mobileConfirmation.Parent = script
			model:Destroy()
			if yesConn then yesConn:Disconnect() end
			if noConn then noConn:Disconnect() end
		end)

		autoConn = mobileConfirmation.AutoPlace.MouseButton1Click:Connect(function()
			autoplace = not autoplace
			if autoplace then
				mobileConfirmation.AutoPlace.Stroke.Color = autoplaceOnColor
				mobileConfirmation.AutoPlace.Image.ImageColor3 = autoplaceOnColor
			else
				mobileConfirmation.AutoPlace.Stroke.Color = autoplaceOffColor
				mobileConfirmation.AutoPlace.Image.ImageColor3 = autoplaceOffColor
			end
		end)
	else
		placeServer(model, pivot, blockId)
	end
end

function module.Enable(tool)
	if enabled or not inventory.selectedBlock then return end
	enabled = true

	inventory:Toggle(true)
	
	ghostModel = inventory.selectedBlock:Clone()
	anchorModel(ghostModel, true)
	if UserInputService.TouchEnabled == false then
		setModelTransparency(ghostModel, 0.5)
	else
		setModelTransparency(ghostModel, 1)
	end	
	ghostModel.Name = "GhostModel"
	ghostModel.Parent = workspace
	mouse.TargetFilter = ghostModel

	currentRotation = 0
	currentTilt = 0

	renderConn = RunService.RenderStepped:Connect(function()
		if mobileConfirmation.Parent == script then
			local cf = getPlacementCFrame()
			if cf then
				if matchRotation then
					ghostModel:PivotTo(cf)
				else
					ghostModel:PivotTo(CFrame.new(cf.Position) * CFrame.Angles(currentTilt, currentRotation, 0))
				end
			end
			local ghostBlockId = ghostModel:GetAttribute("BLOCKID")
			if ghostBlockId and inventory.selectedBlock then
				if ghostBlockId ~= inventory.selectedBlock:GetAttribute("BLOCKID") then
					ghostModel:Destroy()
					ghostModel = inventory.selectedBlock:Clone()
					anchorModel(ghostModel, true)
					if UserInputService.TouchEnabled == false then
						setModelTransparency(ghostModel, 0.5)
					else
						setModelTransparency(ghostModel, 1)
					end	
					ghostModel.Name = "GhostModel"
					CollectionService:AddTag(ghostModel, "IgnorePlacement")
					ghostModel.Parent = workspace
					mouse.TargetFilter = ghostModel
				end
			end
		end
	end)

	inputConn = mouse.Button1Down:Connect(function()
		local cf = getPlacementCFrame()
		if cf and inventory.selectedBlock then
			if #newModels > 0 then
				for _, v in pairs(newModels) do
					local model = v
					table.remove(newModels, table.find(newModels, v))
					model:Destroy()
				end
			end
			local newModel = inventory.selectedBlock:Clone()
			table.insert(newModels, newModel)
			anchorModel(newModel, true)
			if UserInputService.TouchEnabled == false then
				setModelTransparency(newModel, 0)
			else
				setModelTransparency(newModel, 0.5)
				mouse.TargetFilter = newModel
			end
			CollectionService:AddTag(newModel, "IgnorePlacement")
			newModel.Parent = workspace

			if matchRotation then
				newModel:PivotTo(cf)
				placeBlock(newModel, cf, newModel:GetAttribute("BLOCKID"))
			else
				placeBlock(newModel, CFrame.new(cf.Position) * CFrame.Angles(currentTilt, currentRotation, 0), newModel:GetAttribute("BLOCKID"))
			end
		end
	end)
	
	rotConn = mobileConfirmation.Rotate.MouseButton1Click:Connect(function()
		currentRotation = currentRotation + rotationIncrement
		print(currentRotation)
	end)

	tiltConn = mobileConfirmation.Tilt.MouseButton1Click:Connect(function()
		currentTilt = currentTilt + tiltIncrement
		print(currentTilt)
	end)

	inputBeganConn = UserInputService.InputBegan:Connect(onInputBegan)
end

function module.Disable()
	if not enabled then return end
	enabled = false

	inventory:Toggle(false)

	if renderConn then
		renderConn:Disconnect()
		renderConn = nil
	end
	if inputConn then
		inputConn:Disconnect()
		inputConn = nil
	end
	if inputBeganConn then
		inputBeganConn:Disconnect()
		inputBeganConn = nil
	end
	if ghostModel then
		ghostModel:Destroy()
		ghostModel = nil
	end
	mobileConfirmation.Parent = script
	for _, v in pairs(newModels) do
		local model = v
		table.remove(newModels, table.find(newModels, v))
		model:Destroy()
	end
end

function module.IsEnabled()
	return enabled
end

return module

The rotation doesnt change when you click the rotation buttons but it would apply the rotation when you tap again.

I tried adding the button click events in different functions but it does the same thing.

The system is pretty much done for pc, I just need help with the rotation on mobile

I think I see the problem here; the rotation changes aren’t being reflected immediately when clicking the rotation buttons on mobile

Problem is that while you’re updating the rotation values, the ghost model isn’t being updated in real-time because the rotation connections are set up after the render connection, so you need to make sure the ghost model reflects the current rotation values in real-time

nice avatar btw ;p

1 Like