How to make your crosshair stay in the middle of the screen, while the camera is moving(compensating movement)

Hi guys.
I am trying to replicate the effect in this post right here:

As you can see, the camera is moving, but the crosshair seems to just ignore its movement, and lock itself in world space(While still allowing camera movement from the player’s mouse).

My setup is basic camera bobbing, which i want to be ignored by the crosshair. The most logical solution for me, is:

  1. Apply the bobbing to our camera as we would normally camera.CFrame *= currentBobCFrame

  2. Make a variable called “cleanCFrame” where we store the camera CFrame WITHOUT the bobbing applied. We do that by simply doing cleanCFrame = camera.CFrame * currentBobCFrame:Inverse()

  3. We get the cleanCFrame’s lookvector. We create a world position somewhere along it, and position the UI element on it via :WorldToViewportPoint()

This all seems to work technically, but in practice i just cannot get it to work. This is the code, which i am currently using.

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera

local playerGui = player:WaitForChild("PlayerGui")
local testUI = playerGui:WaitForChild("TestUI")
local frame = testUI:WaitForChild("Frame")

local BOB_SPEED = 5.5
local BOB_AMOUNT_Y = 0.08
local BOB_AMOUNT_X = 0.1
local BOB_TILT_Z = 1.5
local BOB_TILT_X = 0.8
local LERP_ALPHA = 0.15
local REFERENCE_SPEED = 16
local INTENSITY_EXPONENT = 1.6
local MAX_SPEED_SCALE = 3

local bobTime = 0
local currentBobCFrame = CFrame.new()
local targetBobCFrame = CFrame.new()

local function getCharacterParts()
	local character = player.Character
	if not character then return nil, nil end
	local humanoid = character:FindFirstChildOfClass("Humanoid")
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	return humanoid, rootPart
end
local cleancframe
RunService:BindToRenderStep("WalkBob", Enum.RenderPriority.Camera.Value + 1, function(deltaTime)
	local humanoid, rootPart = getCharacterParts()

	if not humanoid or not rootPart or camera.CameraType ~= Enum.CameraType.Custom then
		targetBobCFrame = CFrame.new()
		currentBobCFrame = currentBobCFrame:Lerp(targetBobCFrame, LERP_ALPHA)
		camera.CFrame = camera.CFrame * currentBobCFrame
		return
	end

	local velocity = rootPart.AssemblyLinearVelocity
	local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude

	local isMoving = horizontalSpeed > 0.5
	local isGrounded = humanoid.FloorMaterial ~= Enum.Material.Air

	if isMoving and isGrounded then
		local rawScale = horizontalSpeed / REFERENCE_SPEED
		local speedScale = math.clamp(rawScale ^ INTENSITY_EXPONENT, 0, MAX_SPEED_SCALE)

		bobTime = bobTime + deltaTime * BOB_SPEED * speedScale

		local bobX = math.sin(bobTime) * BOB_AMOUNT_X * speedScale
		local bobY = math.sin(bobTime * 2) * 0.5 * BOB_AMOUNT_Y * speedScale

		local tiltZ = math.cos(bobTime) * BOB_TILT_Z * speedScale
		local tiltX = math.cos(bobTime * 2) * BOB_TILT_X * speedScale

		targetBobCFrame =
			CFrame.Angles(math.rad(tiltX) / 30, math.rad(tiltX) / 18, math.rad(tiltZ) / 1.5)
	else
		bobTime = 0
		targetBobCFrame = CFrame.new()
	end
----------------------------------IMPORTANT PART FOR DEVFORUM------------------------------------------
	currentBobCFrame = currentBobCFrame:Lerp(targetBobCFrame, LERP_ALPHA)

	camera.CFrame = camera.CFrame * currentBobCFrame

	cleancframe = camera.CFrame * currentBobCFrame:Inverse()
	

	local aheadPoint = cleancframe.Position + cleancframe.LookVector * 8
	local screenPos, onScreen = camera:WorldToViewportPoint(aheadPoint)
	frame.Visible = onScreen
	frame.Position = UDim2.fromOffset(screenPos.X, screenPos.Y)
	----------------------------------------------------------------------------
end)

game:GetService("ScriptContext")
player.CharacterRemoving:Connect(function()
	bobTime = 0
	currentBobCFrame = CFrame.new()
end)

And this is the problematic section:


----------------------------------IMPORTANT PART FOR DEVFORUM------------------------------------------
	currentBobCFrame = currentBobCFrame:Lerp(targetBobCFrame, LERP_ALPHA)

	camera.CFrame = camera.CFrame * currentBobCFrame

	cleancframe = camera.CFrame * currentBobCFrame:Inverse()
	

	local aheadPoint = cleancframe.Position + cleancframe.LookVector * 8
	local screenPos, onScreen = camera:WorldToViewportPoint(aheadPoint)
	frame.Visible = onScreen
	frame.Position = UDim2.fromOffset(screenPos.X, screenPos.Y)
	----------------------------------------------------------------------------

Please if anyone knows how to achive this crosshair effect, any help is appreciated!!!

2 Likes

Did you try not including the inverse value or unchanged value at all?

Camera.CFrame = Camera.CFrame * targetBobCFrame
		
- cleanCFrame = Camera.CFrame * targetBobCFrame:Inverse()
+ cleanCFrame = Camera.CFrame

Removing the value works for me, even with animations.

If this doesn’t solve your issue, maybe you could try being more verbose about your issue? Videos help a ton!

Place file:
fpsArmsReticle.rbxl (175.2 KB)

1 Like

The math for your cleanCFrame is still going to be off because you’re applying the bobbing to the camera CFrame before you try to invert it. If you modify camera.CFrame first, then multiplying by the inverse of the bob won’t give you the original position since the base CFrame has already changed. You should calculate your cleanCFrame before any camera.CFrame assignments happen in that step.

1 Like

Wouldn’t doing this

Camera.CFrame = Camera.CFrame * targetBobCFrame
		

cleanCFrame = Camera.CFrame

just get the camera cframe with the bob? It literally just grabs the camera cframe after the bobbing is applied.
Also in your example, i am pretty sure your reload animation uses an Animated Camera(directly in the animation Editor), which from what i know doesnt directly affect the actual Camera.CFrame, which might be the reason why just doing camera.CFrame gives u the clean one.

So basically i need to flip the order? I did that and i see a bit of opposite(i guess?) movement in the UI element, but its just a few pixels. You can see for yourself by pasting the code in a local script and adding this in starterGUI.
image


local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera

local playerGui = player:WaitForChild("PlayerGui")
local testUI = playerGui:WaitForChild("TestUI")
local frame = testUI:WaitForChild("Frame")

local BOB_SPEED = 5.5
local BOB_AMOUNT_Y = 0.08
local BOB_AMOUNT_X = 0.1
local BOB_TILT_Z = 1.5
local BOB_TILT_X = 0.8
local LERP_ALPHA = 0.15
local REFERENCE_SPEED = 16
local INTENSITY_EXPONENT = 1.6
local MAX_SPEED_SCALE = 3

local bobTime = 0
local currentBobCFrame = CFrame.new()
local targetBobCFrame = CFrame.new()

local function getCharacterParts()
	local character = player.Character
	if not character then return nil, nil end
	local humanoid = character:FindFirstChildOfClass("Humanoid")
	local rootPart = character:FindFirstChild("HumanoidRootPart")
	return humanoid, rootPart
end
local cleancframe
RunService:BindToRenderStep("WalkBob", Enum.RenderPriority.Camera.Value + 1, function(deltaTime)
	local humanoid, rootPart = getCharacterParts()

	if not humanoid or not rootPart or camera.CameraType ~= Enum.CameraType.Custom then
		targetBobCFrame = CFrame.new()
		currentBobCFrame = currentBobCFrame:Lerp(targetBobCFrame, LERP_ALPHA)
		camera.CFrame = camera.CFrame * currentBobCFrame
		return
	end

	local velocity = rootPart.AssemblyLinearVelocity
	local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude

	local isMoving = horizontalSpeed > 0.5
	local isGrounded = humanoid.FloorMaterial ~= Enum.Material.Air

	if isMoving and isGrounded then
		local rawScale = horizontalSpeed / REFERENCE_SPEED
		local speedScale = math.clamp(rawScale ^ INTENSITY_EXPONENT, 0, MAX_SPEED_SCALE)

		bobTime = bobTime + deltaTime * BOB_SPEED * speedScale

		local bobX = math.sin(bobTime) * BOB_AMOUNT_X * speedScale
		local bobY = math.sin(bobTime * 2) * 0.5 * BOB_AMOUNT_Y * speedScale

		local tiltZ = math.cos(bobTime) * BOB_TILT_Z * speedScale
		local tiltX = math.cos(bobTime * 2) * BOB_TILT_X * speedScale

		targetBobCFrame =
			CFrame.Angles(math.rad(tiltX) / 30, math.rad(tiltX) / 18, math.rad(tiltZ) / 1.5)
	else
		bobTime = 0
		targetBobCFrame = CFrame.new()
	end
	----------------------------------IMPORTANT PART FOR DEVFORUM------------------------------------------
	currentBobCFrame = currentBobCFrame:Lerp(targetBobCFrame, LERP_ALPHA)
	
	cleancframe = camera.CFrame * currentBobCFrame:Inverse()
	
	camera.CFrame = camera.CFrame * currentBobCFrame




	local aheadPoint = cleancframe.Position + cleancframe.LookVector * 800
	local screenPos, onScreen = camera:WorldToViewportPoint(aheadPoint)
	frame.Visible = onScreen
	frame.Position = UDim2.fromOffset(screenPos.X, screenPos.Y)
	----------------------------------------------------------------------------
end)

game:GetService("ScriptContext")
player.CharacterRemoving:Connect(function()
	bobTime = 0
	currentBobCFrame = CFrame.new()
end)

some footage of how it is currently

Guys i found the solution!!! Basically what the guy does here(its a bit unclear at first) is to apply the offset(e.g. bobbing) to the camera and save it as “oldOffset”. Then once the new frame comes, he subtracts the old offset from the current camera cframe(so we get the clean one, which is also set as the current camera cframe) and then apply the offset again to the current camera. Since in most cases the offset is lerped, the second frame its a bit different from the first one. This avoids acumulation bugs, and allows us to get an actually clean CFrame.
Took me a while to understand it!

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