How would I make a camera follow the player's mouse? (Enter the Gungeon)

  1. What do you want to achieve?
    Something similar to this effect games such as Enter the Gungeon uses. Noticeably, the way the camera drifts over to the cursor’s position without letting the character get out of sight.

  2. What is the issue?
    I simply don’t know what to do!

  3. What solutions have you tried so far?
    I tried looking for a few solutions already on the DevForum, but I’ve come up empty handed.

If anyone could point me into the right direction on how to do this, I’d be grateful!

The camera in this case is positioned based on the difference between the cursor position and character position. The camera is also being slid on a curve to make it smooth. You can use :Lerp() for that.

I’m not sure how to calculate the position based on the difference, and even then, I’m not sure if using Position is a good idea, since Height becomes a factor at play then.

You can remove the height component from your calculation. The difference is just literally the difference, so subtract the cursor position from the character position. Actually it looks like the position is measured from the center of the screen and not the character, which should be even easier since now those are both 2D.

I don’t think I’m understanding what you mean, because pure subtraction would create a result like this.

HRP Position = (-80, 3, 40)
Cursor Position = (-110, 1, 25)

HRP Position - Cursor Position = 30, 2, 15

I believe you can just get the average of the vector3/vector2 values

local Cursor = Vector2.new(2, 1)
local Character = Vector2.new(1, 1)

local Center = (Cursor + Character) / 2

I used vector2s here, but vector3s work fine

I don’t believe that to be the case, since that’ll just retrieve the center between both. In Gungeon, however, you can see that isn’t the case for the camera; it’s always closer to the player.
image

I’ve found that simply taking the average of the average is good enough for me, but now I have no idea how to adapt it into a Camera script. No clear ideas on how to limit the distance of the camera or how to prevent it from going nuts from pointing at the sky or something similar.

Anyone have anything to add?

You could use the mouse position

I already am. Here’s the formula I used for it.

local mousePos = mouse.Hit.Position
local rootPos = root.Position + Vector3.new(0, 2, 0)
local camPos = (((rootPos + mousePos) / 2) + rootPos) / 2

In the video it looks like the camera is animating and there is like a border that stops the camera from moving. I will try to test it out in studio

What do you mean? There’s no noticeable border, other than the limits of my screen.

I meant you can’t keep moving the camera up forever, it stops at a certain point. For the formula recommend you use game:GetService(“UserInputService”):GetMouseLocation()

Yea, it stops when it hits the border of my screen. There isn’t any invisible pre-programmed barrier around my screen I can’t see.

Also, game:GetService(“UserInputService”):GetMouseLocation() returns a Vector2 offset. In other words, not a 3D position, which I am using.

Oh okay, for the camera animation maybe you can use tween service?

I wouldn’t need to do that, since the script updates the Camera’s position every frame. There’s nothing to Tween.

Oh, well then that would be the best option.

What if you define the lower and upper bound of where the camera can go and then interpolate it depending on the cursor’s position from the top-left corner of the screen? From your video it looks like each axis is interpolated individually.

I think I was able to come up with something similar:

local mouse = game:GetService('Players').LocalPlayer:GetMouse()

local camera = workspace.CurrentCamera

local DUNGEON_PART = workspace:WaitForChild('DungeonPart')
local CAMERA_OFFSET = Vector3.new(0, 1000, 0) -- needs to be this high since FOV is only 1

camera.CameraType = Enum.CameraType.Scriptable
camera.FieldOfView = 1
camera.CFrame = CFrame.lookAt(DUNGEON_PART.Position + CAMERA_OFFSET, DUNGEON_PART.Position, CFrame.identity.LookVector) -- look at dungeon part from dungeon part + offset

local lowerBound = DUNGEON_PART.CFrame * CFrame.new(-DUNGEON_PART.Size / 2).Position -- if you change the / 2 part in these 2 lines, you can configure how much you want the camera to be able to move. axes are relative to the size of the part
local upperBound = DUNGEON_PART.CFrame * CFrame.new( DUNGEON_PART.Size / 2).Position

local right, up, look = camera.CFrame.XVector, camera.CFrame.YVector, camera.CFrame.ZVector

local function inverseLerp(a, b, t) -- get percent where is t between a and b
	return (t - a) / (b - a)
end

local function lerp(a, b, alpha) -- get value at alpha between a and b
	return a + (b - a) * alpha
end

local function getCameraCFrame()
	local maxX, maxY = camera.ViewportSize.X, camera.ViewportSize.Y
	local mouseX, mouseY = mouse.X, mouse.Y
	local alphaX, alphaY = inverseLerp(maxX, 0, mouseX), inverseLerp(maxY, 0, mouseY) -- we need to switch max and min so the camera moves in the same direction as the cursor relative to the part
	return CFrame.fromMatrix(
		Vector3.new(
			lerp(lowerBound.X, upperBound.X, alphaX),
			DUNGEON_PART.Position.Y + CAMERA_OFFSET.Y,
			lerp(lowerBound.Z, upperBound.Z, alphaY)
		), right, up, look -- since right up and look vectors should remain constant, we don't need to recalculate them w/ CFrame.lookAt
	)
end

game:GetService('RunService').RenderStepped:Connect(function(delta)
	camera.CFrame = getCameraCFrame()
end)

Camera.rbxl (42.6 KB)

1 Like

Sorry for the late reply, but I don’t think this goes well with what I’m trying to do. Having room limits isn’t feasible, and even if it were, I think this system would be limited to small rooms.

I managed to get a system working out! However, I still have an issue, shown in the video below.

This is without a doubt caused by the fact I’m using the Mouse’s Position in this equation… Does anyone know a work-around or an alternative? Other than, you know, huge invisible walls…

Here’s my code:

-- yea, i picked this up from roblox guide. too lazy to rewrite it entirely
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local mouse = player:GetMouse()

local CAMERA_DEPTH = 24
local HEIGHT_OFFSET = 2
local function updateCamera()
	local character = player.Character
	if character then
		local root = character:FindFirstChild("HumanoidRootPart")
		if root then
			local mousePos = mouse.Hit.Position
			local rootPos = root.Position + Vector3.new(0, HEIGHT_OFFSET, 0)
			local camPos = (((rootPos + mousePos) / 2) + rootPos) / 2
			
			local cameraPosition = Vector3.new(camPos.X, camPos.Y, rootPos.Z+CAMERA_DEPTH)
			camera.CFrame = CFrame.new(cameraPosition)
		end
	end
end
RunService:BindToRenderStep("SidescrollingCamera", Enum.RenderPriority.Camera.Value + 1, updateCamera)