Detecting where a ray hits smooth terrain

I’m trying to make a tool that let’s people place down plants for a sandbox style game I’m working on. I need the plant to place only if the ray hits smooth terrain, and it needs to be placed where the ray hits. I’ve never really tried raycasting before now so I have no idea if this is easily done or not, but this is what I’ve come up from just looking at the ROBLOX wiki:

local tool = script.Parent
local player = game:GetService("Players").LocalPlayer
local mouse = player:GetMouse()


	
mouse.Button1Down:connect(function()
	local plant = game.Players.LocalPlayer.PlayerGui.ScreenGui.selectedPlant.Value
	print("Mouse pressed!")
	local ray = Ray.new(tool.Handle.CFrame.p, (mouse.Hit.p - tool.Handle.CFrame.p).unit * 500)
	local part, position = workspace:FindPartOnRay(ray, player.Character, true, false)
	
	

	if part == game.Workspace.Terrain then
		local clone = game.ReplicatedStorage.Flora[plant]:Clone()
		clone.Parent = workspace
		clone.PrimaryPart.CFrame = part.CFrame
	end

end)

What edits will I need to make to get this to work?

Instead of setting the clone’s position to the terrain’s, set it to the position variable returned from the raycast. You already have the variable available, but you don’t use it.

I believe I tried that too, but it didn’t work either. I’ll try it again just to be sure

This line seems to be the issue

if part == game.Workspace.Terrain then

the lines inside the if statement never even get run, and yes I’m clicking the smooth terrain. There are no errors in the output.

I’ve done something like this once. I’m pretty sure if you do part.Name == "Terrain" then it will work.

Now I get this error message whenever I cast a ray:

Players.TheIMevolved.Backpack.Plant Placer.LocalScript:16: attempt to index local ‘part’ (a nil value)

This only happens when I click terrain

This is one of the few cases where you want to check the validity of the arguments you’re receiving, rather than assuming they’re valid.

local tool = script.Parent
local player = game:GetService("Players").LocalPlayer
local mouse = player:GetMouse()

mouse.Button1Down:connect(function()
	local plant = game.Players.LocalPlayer.PlayerGui.ScreenGui.selectedPlant.Value
	print("Mouse pressed!")
	local ray = Ray.new(tool.Handle.CFrame.p, (mouse.Hit.p - tool.Handle.CFrame.p).unit * 500)
	local part, position = workspace:FindPartOnRay(ray, player.Character, true, false)
	
	

	if part and (part:IsA("Terrain") or part.Name:lower() == "terrain") then
		local clone = game.ReplicatedStorage.Flora[plant]:Clone()
		clone.Parent = workspace
		clone.PrimaryPart.CFrame = part.CFrame
	end
end)

On the check line, I personally wouldn’t recommend checking by part name since BaseParts can also be named Terrain. If you must check by name, change the operator to and.

There are a few other comments I’d like to make on this code, but that’s not the point of this thread. :slightly_smiling_face:

Make sure to also inform the user of successful or failed placements for the sake of user experience. It may be frustrating if things don’t get placed where they click, as players may think your system is broken.

1 Like

This is very helpful, thank you! However 2 new issues have arisen.

  1. No matter where I click on the terrain, the plants only get sent to the very center of the terrain base (I’m testing this on a giant grass base)

  2. After a few seconds of using it, the tool get’s deleted seemingly for no reason. I tried moving the cloning bit over to the server side assuming it was some kind of anti exploit measure but it still gets deleted. (sorry if this one is off topic, I might have to look around for some threads or start a different one for this)

EDIT: Second issue is now resolved

The first issue is because you’re setting the CFrame of the plant to the part’s CFrame. In the case of setting CFrames, if you set the CFrame of one object to another, it puts itself in the center. This is also so because you’re not using offset.

You’ll want to convert the position, which is the second argument you get from FindPartOnRay, into a CFrame, then set the plant’s CFrame to that. I think you can just do

plant.CFrame = CFrame.new(position)
1 Like