Can't figure out how to make a Vector3 relative to a ray's normal property

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? I want to make the movement part of wall climbing for my game.

  2. What is the issue? I can’t figure out the math to make the movedir variable relative to the normal of my ray.

  3. What solutions have you tried so far? I have tried to use some sources, like DevForum, ChatGPT, Google, and YouTube.

If it helps, I have my script here.

local char = script.Parent
local climbing = false
local playercanclimb = false
local playerstable = {workspace:WaitForChild("Ocean"), workspace:WaitForChild("Swim")}
local normal
local raypos = nil
local bodypos = nil
local wall = nil

game:GetService("RunService").Heartbeat:Connect(function()
	for i, v in pairs(workspace:GetChildren()) do
		if v:FindFirstChild("Humanoid") and not table.find(playerstable, v) then
			table.insert(playerstable, v)
		end
	end
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = playerstable
	params.FilterType = Enum.RaycastFilterType.Exclude
	local ray = workspace:Raycast(char.HumanoidRootPart.Position, char.HumanoidRootPart.CFrame.LookVector * 2, params)
	if ray then
		playercanclimb = true
		normal = ray.Normal
		raypos = ray.Position
		wall = ray.Instance
	else
		if climbing == true then
			print("No Longer Climbing")
		end
		
		playercanclimb = false
		climbing = false
		raypos = nil
		wall = nil
	end
end)

game:GetService("UserInputService").InputBegan:Connect(function(inp, proc)
	if proc or inp.KeyCode ~= Enum.KeyCode.Space then return end
	if playercanclimb and not climbing then
		climbing = true
		print("Climbing")
		return
	elseif climbing then
		climbing = false
		print("No Longer Climbing")
	end
end)

workspace.ChildRemoved:Connect(function(child)
	if child:FindFirstChild("Humanoid") then
		for i, v in pairs(playerstable) do
			if v == child then
				table.remove(playerstable, i)
			end
		end
	end
end)
local movedir = Vector3.new(0, 0, 0)

while task.wait() do
	
	while climbing do
		char.Humanoid.PlatformStand = true
		local forward = -normal:Cross(Vector3.new(0, 1, 0)).Unit
		local up = forward:Cross(-normal).Unit
		if char.Humanoid.MoveDirection.Magnitude ~= 0 then
			movedir = char.Humanoid.MoveDirection
			movedir = Vector3.new(movedir.X, movedir.Z, 0)
			char.HumanoidRootPart.AssemblyLinearVelocity = (movedir * 100)
		else
			char.HumanoidRootPart.AssemblyLinearVelocity = Vector3.new(0, 0, 0)
		end
		if not bodypos then
			print("Creating BodyPos")
			bodypos = Instance.new("BodyPosition")
			bodypos.D = 600
			bodypos.P = 10000
			bodypos.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
			bodypos.Parent = char:WaitForChild("HumanoidRootPart")
			print(bodypos.Parent.Name)
		end
		bodypos.Position = CFrame.fromMatrix(char.HumanoidRootPart.Position, forward, up).Position + (char.HumanoidRootPart.CFrame.LookVector * 0.1)
		char.HumanoidRootPart.CFrame = CFrame.fromMatrix(char.HumanoidRootPart.Position, forward, up)
		char.Humanoid.AutoRotate = false
		task.wait()
	end

	while not climbing do
		if bodypos ~= nil then
			print("Sigma")
			game:GetService("Debris"):AddItem(bodypos, 0)
			char.Humanoid.PlatformStand = false
			bodypos = nil
			char.HumanoidRootPart.AssemblyLinearVelocity = Vector3.new(0, 0, 0)
			movedir = Vector3.new(0, 0, 0)
			if game:GetService("UserInputService").MouseBehavior == Enum.MouseBehavior.LockCenter then
				char.Humanoid.AutoRotate = true
				print("Re-Enabling Shiftlock!")
			end
		end
		task.wait()
	end
end

If you can find a solution on any of the things that I searched on, I’m sorry. I’m very bad at finding answers to more obscure questions on the internet, and this is my first post.

1 Like

here’s a function that should work and solve a couple edge cases (fixed it from earlier)

local function transformMoveDirection(moveDirection, newVector, upVector)
	if upVector == newVector then
		return moveDirection
	elseif upVector == -newVector then
		return moveDirection - 2 * moveDirection:Dot(upVector) * upVector
	end

	return CFrame.fromAxisAngle(
		upVector:Cross(newVector).Unit, 
		math.acos(upVector:Dot(newVector), -1, 1)
	):VectorToWorldSpace(moveDirection).Unit
end

and then to implement it replace movedir = Vector3.new(movedir.X, movedir.Z, 0) on line 68 with transformMoveDirection(movedir, normal) (make sure you have the function added as well)

also I got bored and reworked your script to use better practices, I tested it and it should work

local runservice = game:GetService("RunService")
local userinputservice = game:GetService("UserInputService")
local contextactionservice = game:GetService("ContextActionService")

local char = script.Parent
local humanoid = char:WaitForChild("Humanoid")
local humanoidrootpart = char:WaitForChild("HumanoidRootPart")

local playerstable = {char, workspace:WaitForChild("Ocean"), workspace:WaitForChild("Swim")}

local climbing = false
local playercanclimb = false

local normal = Vector3.zero
local raypos = nil
local wall = nil

-- declare raycast params outside of heartbeat so you don't make a new one every frame
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = playerstable

local function transformMoveDirection(moveDirection, newVector, upVector)
	if upVector == newVector then
		return moveDirection
	elseif upVector == -newVector then
		return moveDirection - 2 * moveDirection:Dot(upVector) * upVector
	end

	return CFrame.fromAxisAngle(
		upVector:Cross(newVector).Unit, 
		math.acos(upVector:Dot(newVector), -1, 1)
	):VectorToWorldSpace(moveDirection).Unit
end

-- you can reuse the same BodyPosition, and just parent it
local bodypos = Instance.new("BodyPosition")
bodypos.D = 600
bodypos.P = 10000
bodypos.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
bodypos.Parent = script

local movedir = Vector3.zero -- looks cooler than Vector3.new(0, 0, 0)

-- use child added instead of checking every frame for new characters
workspace.ChildAdded:Connect(function(child)
	if child:FindFirstChild("Humanoid") and not table.find(playerstable, child) then
		table.insert(playerstable, child)
		params:AddToFilter(child)
	end
end)

workspace.ChildRemoved:Connect(function(child)
	if child:FindFirstChild("Humanoid") then
		if table.find(playerstable, child) then -- can use table.find instead of for i,v in pairs
			table.remove(playerstable, child)
			params.FilterDescendantsInstances = playerstable
		end
	end
end)

runservice.Heartbeat:Connect(function()
	local rootcframe = humanoidrootpart.CFrame -- get the property once
	local ray = workspace:Raycast(rootcframe.Position, rootcframe.LookVector * 2, params)

	-- moved the while loop code into heartbeat
	if climbing then
		humanoid.PlatformStand = true

		local movedirection = humanoid.MoveDirection

		if movedirection.Magnitude > 0.01 then
			movedir = transformMoveDirection(-movedirection, normal, Vector3.yAxis)
			if movedir == movedir then -- make sure it's not Vector3.new(nan, nan, nan)
				humanoidrootpart.AssemblyLinearVelocity = (movedir * 100)
			end
		else
			humanoidrootpart.AssemblyLinearVelocity = Vector3.zero
		end

		if bodypos.Parent == script then
			print("Parenting BodyPos")

			bodypos.Parent = humanoidrootpart
			print(bodypos.Parent.Name)
		end

		local forward = -normal:Cross(Vector3.yAxis).Unit
		local up = forward:Cross(-normal).Unit

		bodypos.Position = CFrame.fromMatrix(humanoidrootpart.Position, forward, up).Position + (humanoidrootpart.CFrame.LookVector * 0.1)
		humanoidrootpart.CFrame = CFrame.fromMatrix(humanoidrootpart.Position, forward, up)
		humanoid.AutoRotate = false
	else
		if bodypos.Parent ~= script then
			print("Sigma!")
			humanoid.PlatformStand = false
			humanoidrootpart.AssemblyLinearVelocity = Vector3.zero
			bodypos.Parent = script
			movedir = Vector3.zero

			if userinputservice.MouseBehavior == Enum.MouseBehavior.LockCenter then
				humanoid.AutoRotate = true
				print("Re-Enabling Shiftlock!")
			end
		end
	end

	if ray then
		playercanclimb = true
		normal = ray.Normal
		raypos = ray.Position
		wall = ray.Instance
	else
		if climbing then
			print("No Longer Climbing")
		end

		playercanclimb = false
		climbing = false
		raypos = nil
		wall = nil
	end
end)

-- use ContextActionService to just check for space and not fire every time there's a new input
contextactionservice:BindAction("Climb", function(name, state, input)
	if state == Enum.UserInputState.Begin then
		if playercanclimb and not climbing then
			climbing = true
			print("Climbing")
		elseif climbing then
			climbing = false
			print("No Longer Climbing")
		end
	end
	return Enum.ContextActionResult.Pass
end, false, Enum.KeyCode.Space)

I put comments where I changed stuff

1 Like

if both vectors are relative to world space then you can do something like:

local RayOrigin, RayPosition --normal ray
local VectorB  --vector to change

local CFrameA = CFrame.lookAt(RayOrigin, RayPosition) --changing the x,y,z axis for VectorB to follow

print((CFrameA * VectorB)-RayOrigin) --is a directional vector

This hopefully should convert VectorB to the local/object space of the ray, hope this helps! :grin:

1 Like

That actually worked! Sorry if I’m asking a bit too much, but would you know how to make the player climb up after reaching the top?

1 Like

ye, you can apply an upwards force by doing humanoidrootpart:ApplyImpulse(Vector3.yAxis * humanoidrootpart.AssemblyMass * 35), and adding it after the “Sigma” print

Thanks a lot for your help! My climbing system is practically finished now, aside from animations (which I dunno how to animate). Since I changed a bit, and wanna help other people struggling looking on this post, final script:

local char = script.Parent
local climbing = false
local playercanclimb = false
local playerstable = {workspace:WaitForChild("Ocean"), workspace:WaitForChild("Swim")}
local normal
local raypos = nil
local bodypos = nil
local wall = nil

local function transformMoveDirection(moveDirection, newVector, upVector)
	if upVector == newVector then
		return moveDirection
	elseif upVector == -newVector then
		return moveDirection - 2 * moveDirection:Dot(upVector) * upVector
	end
	
	return CFrame.fromAxisAngle(
		upVector:Cross(newVector).Unit,
		math.acos(upVector:Dot(newVector), -1, 1)
	):VectorToWorldSpace(moveDirection).Unit
end

game:GetService("RunService").Heartbeat:Connect(function()
	for i, v in pairs(workspace:GetChildren()) do
		if v:FindFirstChild("Humanoid") and not table.find(playerstable, v) then
			table.insert(playerstable, v)
		end
	end
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = playerstable
	params.FilterType = Enum.RaycastFilterType.Exclude
	local ray = workspace:Raycast(char.HumanoidRootPart.Position, char.HumanoidRootPart.CFrame.LookVector * 2, params)
	if ray then
		playercanclimb = true
		normal = ray.Normal
		raypos = ray.Position
		wall = ray.Instance
	else
		if climbing == true then
			print("No Longer Climbing")
		end
		
		playercanclimb = false
		climbing = false
		raypos = nil
		wall = nil
	end
end)

game:GetService("UserInputService").InputBegan:Connect(function(inp, proc)
	if proc or inp.KeyCode ~= Enum.KeyCode.Space then return end
	if playercanclimb and not climbing then
		climbing = true
		print("Climbing")
		return
	elseif climbing then
		climbing = false
		print("No Longer Climbing")
	end
end)

workspace.ChildRemoved:Connect(function(child)
	if child:FindFirstChild("Humanoid") then
		for i, v in pairs(playerstable) do
			if v == child then
				table.remove(playerstable, i)
			end
		end
	end
end)
local movedir = Vector3.new(0, 0, 0)

while task.wait() do
	
	while climbing do
		char.Humanoid.PlatformStand = true
		local forward = -normal:Cross(Vector3.new(0, 1, 0)).Unit
		local up = forward:Cross(-normal).Unit
		if char.Humanoid.MoveDirection.Magnitude ~= 0 then
			movedir = char.Humanoid.MoveDirection
			movedir = transformMoveDirection(-movedir, normal, Vector3.yAxis)
			char.HumanoidRootPart.AssemblyLinearVelocity = (movedir * 100)
		else
			char.HumanoidRootPart.AssemblyLinearVelocity = Vector3.new(0, 0, 0)
		end
		if not bodypos then
			print("Creating BodyPos")
			bodypos = Instance.new("BodyPosition")
			bodypos.D = 600
			bodypos.P = 10000
			bodypos.MaxForce = Vector3.new(math.huge, math.huge, math.huge)
			bodypos.Parent = char:WaitForChild("HumanoidRootPart")
			print(bodypos.Parent.Name)
		end
		bodypos.Position = CFrame.fromMatrix(char.HumanoidRootPart.Position, forward, up).Position + (char.HumanoidRootPart.CFrame.LookVector * 0.1)
		char.HumanoidRootPart.CFrame = CFrame.fromMatrix(char.HumanoidRootPart.Position, forward, up)
		char.Humanoid.AutoRotate = false
		task.wait()
	end

	while not climbing do
		if bodypos ~= nil then
			print("Sigma")
			game:GetService("Debris"):AddItem(bodypos, 0)
			char.Humanoid.PlatformStand = false
			bodypos = nil
			char.HumanoidRootPart.AssemblyLinearVelocity = Vector3.new(0, 0, 0)
			movedir = Vector3.new(0, 0, 0)
			if game:GetService("UserInputService").MouseBehavior == Enum.MouseBehavior.LockCenter then
				char.Humanoid.AutoRotate = true
				print("Re-Enabling Shiftlock!")
			end
			local rayparams = RaycastParams.new()
			rayparams.FilterDescendantsInstances = playerstable
			rayparams.FilterType = Enum.RaycastFilterType.Exclude
			rayparams.IgnoreWater = true
			local headray = workspace:Raycast(char.Head.Position, char.HumanoidRootPart.CFrame.LookVector * 2, rayparams)
			local legsray = workspace:Raycast((char["Left Leg"].Position + char["Right Leg"].Position) / 2, char.HumanoidRootPart.CFrame.LookVector * 2, rayparams)
			if legsray and not headray then
				char.HumanoidRootPart:ApplyImpulse(Vector3.yAxis * char.HumanoidRootPart.AssemblyMass * 35)
			end
		end
		task.wait()
	end
end

Hold on, I realized one more thing. Do you know any methods I can use to swap walls while climbing?