Hmm. Oops. That didn’t work like what I thought it would. That’ll teach me to test stuff before making claims. Sorry about that.
Here’s a test that’ll work on tilted planes. I was running it from a Script in ReplicatedStorage with RunContext set to client. It’s just a client-side test so no Remote Events to make the parts vis on the server.
It uses the mouse to world ray for the facing vector rather than the player’s hrp. It’s a bit hacky, much pasted from example code from the Roblox docs for raycast and mouse input. Can throw a few rotated Parts around a place and just click to place (it makes a little horseshoe barrier). The direction gets confused if you try placing on the underside of things, but otherwise it may give you some ideas for things to try.
--[[
Client-side only example
]]
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local character = player.Character
if not character or character.Parent == nil then
character = player.CharacterAdded:Wait()
end
local hrp = character:WaitForChild("HumanoidRootPart")
local directionVector
local MAX_MOUSE_DISTANCE = 1000
local FIRE_RATE = 0.3
local timeOfPreviousPlacement = 0
local rand = Random.new()
local function makeBarrier()
local offset = CFrame.new(0, 0, 6)
local model = Instance.new("Model")
local part = Instance.new("Part")
part.Size = Vector3.new(5, 1, 2)
part.Anchored = true
local fn = function()
local step = 5
for i = 0, step-1 do
local c = part:Clone()
local rot = i * math.pi/(step-1)
local cf = CFrame.Angles(0, rot, 0)
c.CFrame = cf:ToWorldSpace(offset)
c.Size = c.Size * Vector3.new(1,1+3*rand:NextNumber(),1)
c.Position = Vector3.new(c.Position.X, c.Size.Y/2, c.Position.Z)
c.Parent = model
end
local extSize = model:GetExtentsSize()
model.WorldPivot -= Vector3.new(0, extSize.Y/2, 0)
model.WorldPivot *= CFrame.Angles(0, math.pi/4, 0)
model.Parent = workspace
end
fn()
return model
end
-- Check if enough time has passed
local function canPlaceObject()
local currentTime = tick()
if currentTime - timeOfPreviousPlacement < FIRE_RATE then
return false
end
return true
end
local function getWorldRaycast()
local mouseLocation = UserInputService:GetMouseLocation()
-- Create a ray from the 2D mouse location
local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
-- The unit direction vector of the ray multiplied by a maximum distance
directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
-- Raycast from the ray's origin towards its direction
local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
if raycastResult then
return raycastResult
else
-- No object was hit
return nil
end
end
local function getSurfaceAlignedCf(surfaceNormal, itemPosition, desiredForward)
-- Calculate the right vector (perpendicular to both forward and up vectors)
local rightVector = surfaceNormal:Cross(desiredForward).Unit
-- Recalculate the forward vector to ensure orthogonality
local forwardVector = rightVector:Cross(surfaceNormal).Unit
-- Create the new CFrame with the position and orientation
local newCFrame = CFrame.fromMatrix(
itemPosition,
rightVector,
surfaceNormal,
forwardVector
)
return newCFrame
end
local function placeItem()
if not canPlaceObject() then
return
end
local rcResult = getWorldRaycast()
if rcResult then
local model = makeBarrier()
local rcSurfNorm = rcResult.Normal
local rcPosition = rcResult.Position
--local upVector = Vector3.new(0,1,0)
local lookVector = Vector3.new(directionVector.X, 0, directionVector.Z).Unit
local cf = getSurfaceAlignedCf(rcSurfNorm, rcPosition, lookVector)
--local cf = CFrame.lookAlong(rcPosition, lookVector, sfNorm)
model:PivotTo(cf)
end
end
UserInputService.InputBegan:Connect(function(input, _gameProcessedEvent)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
placeItem()
end
end)