How can I make a dynamic cursor?

Hi, so I’ve been seeing a couple of games using this kind of cursor. The most notable game is 3008 (by uglyburger0)

if you don’t know what kind of cursor I mean, here’s a video I took to demonstrate

I haven’t seen any tutorials or devforum posts for these kinds of things, so I figured I’d make the first one. I’d assume I can use image labels and tweens to obtain this effect alongside :getMouse() but I’m not sure

Thank you in advance

3 Likes

Looks like they used raycasting and and a billboard gui cursor

i’m not sure about raycasting but the billboard GUI makes sense, how can I achieve the same effect?

I personally use raycasting but I guess you could use mouse.Target. With raycasting you can accuratly position the cursor on the part, this is the result of that:

1 Like

I’m not very familiar with raycasting or any mouse-related stuff aside from changing the cursor image. What would be the easiest way to approach this?

1 Like

Haven’t tested it but something like this should work:


-- services --

local runService = game:GetService('RunService') -- you could bindtorenderstep but I'm gonna use heartbeat:Wait() so it's easier to understand
local players = game:GetService('Players')

-- constants --

local PLAYER = players.LocalPlayer
local CHARACTER = PLAYER.Character
local CURSOR_PART = workspace:WaitForChild('CursorPart') -- part that holds the billboard gui
local HEARTBEAT = runService.Heartbeat

local CURSOR_RANGE = 1000

-- loop --

while HEARTBEAT:Wait() do
	local rayResult = workspace:Raycast(CHARACTER.HumanoidRootPart.Position, CHARACTER.HumanoidRootPart.CFrame.LookVector * CURSOR_RANGE)
	if rayResult then
		CURSOR_PART.Position = rayResult.Position
	end
end
1 Like

not exactly sure what i’m supposed to do in order for this to function, could you guide me through it?

1 Like

Yeah sure, I’m gonna modify it a bit so it works how you want it to work, give me a few minutes

1 Like

Just make a LocalScript inside of StarterCharacterScripts and put this inside of it:


-- services --

local runService = game:GetService('RunService') -- you could bindtorenderstep but I'm gonna use heartbeat:Wait() so it's easier to understand
local players = game:GetService('Players')

-- constants --

local CHARACTER = script.Parent
local HRP = CHARACTER:WaitForChild('HumanoidRootPart')
local PLAYER = players.LocalPlayer
local CAMERA = workspace.CurrentCamera
local CURSOR_PART = workspace:WaitForChild('CursorPart') -- part that holds the billboard gui
local HEARTBEAT = runService.Heartbeat

local BLACKLIST = RaycastParams.new()
BLACKLIST.FilterDescendantsInstances = {CHARACTER, CURSOR_PART}
BLACKLIST.FilterType = Enum.RaycastFilterType.Blacklist

local CURSOR_SIZE = 0.1
local CURSOR_RANGE = 1000

-- functions --

local function adjustSize()
	local distanceBetween = (CAMERA.CFrame.Position - CURSOR_PART.Position).Magnitude
	CURSOR_PART.BillboardGui.Size = UDim2.new(CURSOR_SIZE * distanceBetween, 0 , CURSOR_SIZE * distanceBetween, 0)
end

-- loop --

while HEARTBEAT:Wait() do
	local rayResult = workspace:Raycast(CAMERA.CFrame.Position, CAMERA.CFrame.LookVector * CURSOR_RANGE, BLACKLIST)
	if rayResult then
		CURSOR_PART.Position = rayResult.Position
		adjustSize()
	end
end

Keep in mind this is only for first person games because that’s what you showed on the video. Make sure you create a CursorPart which should be anchored and cancollide set to false.


Edit: added size scaling based on distance

this isn’t really what I intend to do, my intent is to make it a GUI, not a part that acts like one.

Ehh, you put the gui inside of the part and set the part’s transparency to 1

i don’t think it’d work though, considering how it could just go on forever and be hard for it to display

kind of sounds like you want him to make the entire thing for you

3 Likes

There I made an example video, it works with gui just fine

Here’s the place for anyone that may need it:
Custom FirstPerson Cursor.rbxl (36.4 KB)

All you’d need to do is check the mouse’s target, this can be achieved via the legacy mouse object or raycasting (if you prefer).

local Game = game
local RunService = Game:GetService("RunService")
local Players = Game:GetService("Players")
local Player = Players.LocalPlayer
local Mouse = Player:GetMouse()

local function OnRenderStep()
	local Target = Mouse.Target
	if not Target then return end
	print(Target.Name)
	--Check the target and change the mouse's cursor icon here.
end

RunService.RenderStepped:Connect(OnRenderStep)
2 Likes

Try this example using RayCasting and UserInputService to get mouse position instead of Player:GetMouse() which is probably undergoing deprecation at some point. The only reason it hasn’t happened is it’s widespread use in many games. I nabbed some images from the Toolbox to show it working, they are not my images.

[EDIT] Sorry forgot to add RaycastParam to the raycaster.

Code below:

--=================================================================================
local UserInputService = game:GetService("UserInputService");
local RunService = game:GetService("RunService");
local Icons =
	{
		"rbxassetid://4344827440",
		"rbxassetid://490658593",
		"rbxassetid://398096640"
	};

--=================================================================================
local RaycastParam = RaycastParams.new();
RaycastParam.FilterType = Enum.RaycastFilterType.Whitelist;
RaycastParam.FilterDescendantsInstances = {workspace.Targets};
RaycastParam.IgnoreWater = true;
local Camera = game.Workspace.CurrentCamera;
local MouseCursor = script.Parent.Cursor;
local Range = 1000;

--=================================================================================
RunService.RenderStepped:Connect(function()
	
	local mouseLocation = UserInputService:GetMouseLocation();
	local mouseRay = Camera:ViewportPointToRay(mouseLocation.X,mouseLocation.Y);
	local raycastResult = workspace:Raycast(mouseRay.Origin,mouseRay.Direction*Range,RaycastParam);
	if raycastResult then

		local hitTarget = raycastResult.Instance;
		print(hitTarget.Name);
		MouseCursor.Visible = true;
		UserInputService.MouseIconEnabled = false;
		MouseCursor.Position = UDim2.fromOffset(mouseLocation.X,mouseLocation.Y);
		if hitTarget.Parent.Name == "FirstType" then
			
			MouseCursor.Image = Icons[1];
			return;
			
		elseif hitTarget.Parent.Name == "SecondType" then
			
			MouseCursor.Image = Icons[2];
			return;
			
		elseif hitTarget.Parent.Name == "ThirdType" then
			
			MouseCursor.Image = Icons[3];
			return;
			
		end
		
	end
	
		
	UserInputService.MouseIconEnabled = true;
	MouseCursor.Visible = false;
	
	
end)
--=================================================================================
-- EOF...
--=================================================================================

Example place:
Mouse_Baseplate.rbxl (36.9 KB)

Hello, Im just reading through this trying to learn a little about raycasts, Im just wondering what the reason for the return is at the end of the function. I’ve always been confused with the use of ‘return’ and whatnot. It would be greatly appreciated

edit: also when I tested, I found that when you hover over one of the objects my fps drops by a lot, what would you do to fix this?

1 Like

Yeah actually you could lose that and put an else in, this was before I edited the post to put the RaycastParams in because at the time it wasn’t working as I expected. I’ll not edit the original because I made a single use case that can just be dropped into PlayerStarterScripts so I’ll add that here instead and make the changes in logic you suggested.

Single use case here without the need for Instances existing like in the example place.

--=================================================================================
local UserInputService = game:GetService("UserInputService");
local RunService = game:GetService("RunService");
local Players = game:GetService("Players");
local Icons =
	{
		"rbxassetid://4344827440",
		"rbxassetid://490658593",
		"rbxassetid://398096640"
	};

--=================================================================================
local LocalPlayer = Players.LocalPlayer;
local GUI = Instance.new("ScreenGui");
GUI.Parent = LocalPlayer.PlayerGui;
GUI.IgnoreGuiInset = true;
GUI.Enabled = true;

--=================================================================================
local MouseCursor = Instance.new("ImageLabel");
MouseCursor.Size = UDim2.fromScale(0.05,0.05);
MouseCursor.AnchorPoint = Vector2.new(0.5,0.5);
MouseCursor.SizeConstraint = Enum.SizeConstraint.RelativeYY;
MouseCursor.BackgroundTransparency = 1;
MouseCursor.Visible = false;
MouseCursor.Parent = GUI;
MouseCursor.BorderSizePixel = 0;

--=================================================================================
local RaycastParam = RaycastParams.new();
RaycastParam.FilterType = Enum.RaycastFilterType.Whitelist;
RaycastParam.FilterDescendantsInstances = {workspace.Targets};
RaycastParam.IgnoreWater = true;
local Camera = game.Workspace.CurrentCamera;
local Range = 1000;

--=================================================================================
RunService.RenderStepped:Connect(function()
	
	local mouseLocation = UserInputService:GetMouseLocation();
	local mouseRay = Camera:ViewportPointToRay(mouseLocation.X,mouseLocation.Y);
	local raycastResult = workspace:Raycast(mouseRay.Origin,mouseRay.Direction*Range,RaycastParam);
	if raycastResult then

		local CurrentTarget = raycastResult.Instance;
		MouseCursor.Visible = true;
		UserInputService.MouseIconEnabled = false;
		MouseCursor.Position = UDim2.fromOffset(mouseLocation.X,mouseLocation.Y);
		if CurrentTarget.Parent.Name == "FirstType" then
			
			MouseCursor.Image = Icons[1];
			
		elseif CurrentTarget.Parent.Name == "SecondType" then
			
			MouseCursor.Image = Icons[2];
			
		elseif CurrentTarget.Parent.Name == "ThirdType" then
			
			MouseCursor.Image = Icons[3];
			
		end
		
	else
		
		UserInputService.MouseIconEnabled = true;
		MouseCursor.Visible = false;
		
	end
	
end)
--=================================================================================
-- EOF...
--=================================================================================

ok so sorry i’m just a little confused, when I remove the returns in the first script you wrote out, it no longer changes the logo. could you explain what changed in the new script that you no longer need the returns?