Custom ripple mouse cursor

Hello Devforum! I have a tutorial on how to make a custom ripple mouse cursor that looks very cool!

Before we get started, create a localscript under StarterPlayerScripts and name it whatever you’d like.
Without further ado let’s get started!

Variables and setup

Before we can get to the exciting part, we have to define some things first.

local Players = game:GetService("Players") -- The players in the game
local UserInputService = game:GetService("UserInputService") -- Let's the game handle your inputs
local TweenService = game:GetService("TweenService") -- allows for smooth animations
local RunService = game:GetService("RunService") -- Lets things run continuously.

local player = Players.LocalPlayer -- This is your player(Since it's local it's for everyone!)
local cursorColor = Color3.fromRGB(12, 255, 166) -- This will be the color of the cursor and ripple, feel free to change this to whatever!

Now you have the basics done, now we can move onto UI creation :arrow_lower_left:

UI creation

Now we can move onto creating the UI!
Beneath the basic variables you created, paste this.

local gui = Instance.new("ScreenGui") -- Creates the cursor GUI.
gui.Name = "SuperCoolCursor" -- Just the name of your cursorUI
gui.IgnoreGuiInset = true -- If it can override certain core elements
gui.ResetOnSpawn = false -- If the gui is reset when your character OOFS!
gui.ZIndexBehavior = Enum.ZIndexBehavior.Global -- Let's the line below able to override the other elements.
gui.DisplayOrder = 1_000_000 -- Makes the gui above every other element(unless you have something higher)
gui.Parent = player:WaitForChild("PlayerGui") -- Clones the GUI to a safe spot that makes it visible.

UserInputService.MouseIconEnabled = false -- This disables the default mouse ensuring that the mouse won't overlap your custom one.

Now we can move onto the inner circle of your mouse that will show if it is over any UI objects.

local dot = Instance.new("Frame") -- The instance of the circle
dot.Size = UDim2.new(0, 6, 0, 6) -- Sets the size of the 2 dimensional UI object.
dot.BackgroundColor3 = cursorColor -- Sets the color to the one you defined earlier!
dot.BackgroundTransparency = 0 -- Makes sure the cursor is visible.
dot.BorderSizePixel = 0 -- Sets the size of the border(I recommend 0)
dot.AnchorPoint = Vector2.new(0.5, 0.5) -- Anchors the dot to a specific location if inactive.
dot.Position = UDim2.new(0, 0, 0, 0) -- The starting location of your mouse(when you join)
dot.ZIndex = 10001 -- The ability to overlap other UI elements below this ZIndex.
dot.Parent = gui -- Places the frame under the gui we created.

Now we have to make the circle… A circle, obviously!

local dotCorner = Instance.new("UICorner") -- Gives UI roundness
dotCorner.CornerRadius = UDim.new(1, 0) -- Sets the distance from the center of a circle to its border, making it's circle shape.
dotCorner.Parent = dot -- Makes the corner under the dot we created.

Now it’s time for the circle.

local circle = Instance.new("Frame") -- The circle instance
circle.Size = UDim2.new(0, 14, 0, 14) -- Over the original dot, making it larger.
circle.BackgroundTransparency = 1 -- Sets it to transparent until over a UI element
circle.BorderSizePixel = 0 -- Border set at 0
circle.AnchorPoint = Vector2.new(0.5, 0.5) -- Same as dot.
circle.Position = UDim2.new(0, 0, 0, 0) -- Along with the dot
circle.ZIndex = 10002 -- Over all other UI
circle.Parent = gui -- Inside of the gui.

-- Circlefy the ripple
local circleCorner = Instance.new("UICorner")
circleCorner.CornerRadius = UDim.new(1, 0)
circleCorner.Parent = circle

local circleStroke = Instance.new("UIStroke") -- Gives UI thickness
circleStroke.Color = cursorColor -- Makes the circle the color of the cursor.
circleStroke.Thickness = 2 -- Makes it decently thick(you can edit this if you want it wider)
circleStroke.Parent = circle -- Under the circle

Now, if you were to try this out ingame you’d just have no mouse :frowning: that’s why we need to run the mouse :arrow_lower_left:

Continuously running the mouse

Now we have to run the mouse,

RunService.RenderStepped:Connect(function() -- Always running
	local mousePos = UserInputService:GetMouseLocation() -- The position of your mouse using the `UserInputService`
	local x, y = mousePos.X, mousePos.Y -- Graphs a point on your screen.

	dot.Position = UDim2.new(0, x, 0, y) -- Moves the mouse according to the point created.
	circle.Position = UDim2.new(0, x, 0, y) -- Moves along with the mouse

	local guisAtPos = player.PlayerGui:GetGuiObjectsAtPosition(x, y) -- Gets the gui objects at the location of the mouse.

	local overGui = false -- A variable to decide if the mouse is over another gui object or not.
	for _, guiElement in ipairs(guisAtPos) do -- Gets all the UI elements under the mouse
		if guiElement.Visible and guiElement:IsA("GuiButton") or guiElement:IsA("TextLabel") or guiElement:IsA("ImageLabel") then
			overGui = true -- Get's all UI elements and if the element is a `GuiButton` , `TextLabel` or `ImageLabel` then `overgui` is set to true.
			break
		end
	end

	dot.Visible = not overGui -- Dot visible not over UI elements.
	circle.Visible = overGui -- Circle visible if over UI elements.
end)

Now we have a working mouse! go try it ingame but we still don’t have a ripple, let’s make that now! :arrow_lower_left:

The ripple🤫
	for i = 1, 1 do -- Running continuously
		local ripple = Instance.new("Frame") -- The frame of the ripple
		ripple.Size = UDim2.new(0, 0, 0, 0) -- Set at 0 for invisibility.
		ripple.Position = UDim2.new(0, x, 0, y) -- At the pos of the mouse cursor
		ripple.AnchorPoint = Vector2.new(0.5, 0.5) -- Anchor point
		ripple.BackgroundTransparency = 1 -- Double check for invisibility.
		ripple.BorderSizePixel = 0
		ripple.ZIndex = 9999 -- Above UI elements other than the mouse and circle.
		ripple.Parent = gui -- Under the GUI

		local corner = Instance.new("UICorner") -- Circle ripple
		corner.CornerRadius = UDim.new(1, 0)
		corner.Parent = ripple

		local stroke = Instance.new("UIStroke") - Thick circle
		stroke.Color = cursorColor -- Color of ripple 
		stroke.Thickness = 3 -- Adjustable thickness
		stroke.Transparency = 0 -- Makes the stroke visible.
		stroke.Parent = ripple -- Gives the ripple the stroke.

		local targetSize = 30 + (i * 10) -- The size adjusted to your screen.
		local expand = TweenService:Create(ripple, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
			Size = UDim2.new(0, targetSize, 0, targetSize)
		}) -- Uses TweenService to create a tweeninfo with the EasingStyle(smooth animation) of Sine and EasingDirection to make it start and end.

		local fade = TweenService:Create(stroke, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
			Transparency = 1
		}) -- Similar to the previous one but tweens the transparency to 1.

		expand:Play() -- Plays the expand tween
		fade:Play() -- Plays the fade tween

		task.delay(0.8, function() -- Waits 0.8 seconds
			ripple:Destroy() -- Destroys the temporary ripple
		end)
	end
end

Finally, time to create the ripple whenever you Click or touch. You can add aditional ones for VR, PlayStation, etc.

UserInputService.InputBegan:Connect(function(input, gpe)
	if gpe then return end -- Won't work with `GamePadEnabled`
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.Gamepad2 then
-- Multiple input types to trigger the ripple.
		local pos = UserInputService:GetMouseLocation() -- The mouse location
		createRipple(pos.X, pos.Y) -- Ripples at the position of the mouse
	end
end)

If you have followed the tutorial, you now have an awesome mouse cursor you can use in your game!

Final Product

For those who couldn’t follow the tutorial, here is the finished product!

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

local player = Players.LocalPlayer
local cursorColor = Color3.fromRGB(12, 255, 166)

local gui = Instance.new("ScreenGui")
gui.Name = "SuperAwesomeMouse"
gui.IgnoreGuiInset = true
gui.ResetOnSpawn = false
gui.ZIndexBehavior = Enum.ZIndexBehavior.Global
gui.DisplayOrder = 1_000_000
gui.Parent = player:WaitForChild("PlayerGui")

UserInputService.MouseIconEnabled = false

local dot = Instance.new("Frame")
dot.Size = UDim2.new(0, 6, 0, 6)
dot.BackgroundColor3 = cursorColor
dot.BackgroundTransparency = 0
dot.BorderSizePixel = 0
dot.AnchorPoint = Vector2.new(0.5, 0.5)
dot.Position = UDim2.new(0, 0, 0, 0)
dot.ZIndex = 10001
dot.Parent = gui

local dotCorner = Instance.new("UICorner")
dotCorner.CornerRadius = UDim.new(1, 0)
dotCorner.Parent = dot

local circle = Instance.new("Frame")
circle.Size = UDim2.new(0, 14, 0, 14)
circle.BackgroundTransparency = 1
circle.BorderSizePixel = 0
circle.AnchorPoint = Vector2.new(0.5, 0.5)
circle.Position = UDim2.new(0, 0, 0, 0)
circle.ZIndex = 10000
circle.Parent = gui

local circleCorner = Instance.new("UICorner")
circleCorner.CornerRadius = UDim.new(1, 0)
circleCorner.Parent = circle

local circleStroke = Instance.new("UIStroke")
circleStroke.Color = cursorColor
circleStroke.Thickness = 2
circleStroke.Parent = circle

RunService.RenderStepped:Connect(function()
	local mousePos = UserInputService:GetMouseLocation()
	local x, y = mousePos.X, mousePos.Y

	dot.Position = UDim2.new(0, x, 0, y)
	circle.Position = UDim2.new(0, x, 0, y)

	local guisAtPos = player.PlayerGui:GetGuiObjectsAtPosition(x, y)

	local overGui = false
	for _, guiElement in ipairs(guisAtPos) do
		if guiElement.Visible and guiElement:IsA("GuiButton") or guiElement:IsA("TextLabel") or guiElement:IsA("ImageLabel") then
			overGui = true
			break
		end
	end

	dot.Visible = not overGui
	circle.Visible = overGui
end)

local function createRipple(x, y)
	for i = 1, 1 do
		local ripple = Instance.new("Frame")
		ripple.Size = UDim2.new(0, 0, 0, 0)
		ripple.Position = UDim2.new(0, x, 0, y)
		ripple.AnchorPoint = Vector2.new(0.5, 0.5)
		ripple.BackgroundTransparency = 1
		ripple.BorderSizePixel = 0
		ripple.ZIndex = 9999
		ripple.Parent = gui

		local corner = Instance.new("UICorner")
		corner.CornerRadius = UDim.new(1, 0)
		corner.Parent = ripple

		local stroke = Instance.new("UIStroke")
		stroke.Color = cursorColor
		stroke.Thickness = 3
		stroke.Transparency = 0
		stroke.Parent = ripple

		local targetSize = 30 + (i * 10)
		local expand = TweenService:Create(ripple, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
			Size = UDim2.new(0, targetSize, 0, targetSize)
		})

		local fade = TweenService:Create(stroke, TweenInfo.new(0.8, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
			Transparency = 1
		})

		expand:Play()
		fade:Play()

		task.delay(0.8, function()
			ripple:Destroy()
		end)
	end
end

UserInputService.InputBegan:Connect(function(input, gpe)
	if gpe then return end
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.Gamepad2 then
		local pos = UserInputService:GetMouseLocation()
		createRipple(pos.X, pos.Y)
	end
end)

Thanks to Home | Spark Universe - Minecraft Partner for this entire idea(their mouse cursor is super duper awesome too)
I hope this tutorial helped you create awesome with your games too! Have a great development experience :saluting_face:

3 Likes

This is pretty cool, for anyone wondering, this is how it looks:

1 Like

That’s a very nice tutorial!

However I noticed that your script is a bit unoptimized, the gui hovering were offset by half on Y position, and your cursor gui take way more instances than needed.

Therefore, I took some times to makes a few adjustements. I removed most instances, fixed the gui hovering issue, and optimized the script performance by around 150% (1.5% - 3%) >> (0.3% - 1.1%) of script activity.

Here is the new script:

local PlayerService = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")

local Player = PlayerService.LocalPlayer
local PlayerGui = Player:WaitForChild("PlayerGui")

local RippleTweenInfo = TweenInfo.new(0.8, Enum.EasingStyle.Sine)
local InputList = {Enum.UserInputType.MouseButton1, Enum.UserInputType.Touch, Enum.UserInputType.Gamepad2}
local HoverGuiList = {"TextButton", "ImageButton", "TextLabel", "ImageLabel"}

local CursorGui = Instance.new("ScreenGui")
CursorGui.Name = "SuperAwesomeMouse"
CursorGui.IgnoreGuiInset = true
CursorGui.ResetOnSpawn = false
CursorGui.ZIndexBehavior = Enum.ZIndexBehavior.Global
CursorGui.DisplayOrder = 999
CursorGui.Parent = PlayerGui

local DotFrame = Instance.new("Frame")
DotFrame.Size = UDim2.fromOffset(6, 6)
DotFrame.BackgroundColor3 = Color3.fromRGB(12, 255, 166)
DotFrame.BackgroundTransparency = 0
DotFrame.AnchorPoint = Vector2.new(0.5, 0.5)
DotFrame.ZIndex = 999
DotFrame.Parent = CursorGui

local DotCorner = Instance.new("UICorner")
DotCorner.CornerRadius = UDim.new(1, 0)
DotCorner.Parent = DotFrame

local DotStroke = Instance.new("UIStroke")
DotStroke.Color = Color3.fromRGB(12, 255, 166)
DotStroke.Thickness = 2
DotStroke.Enabled = false
DotStroke.Parent = DotFrame

local function TweenRipple()
	local Ripple = DotFrame:Clone()
	local Stroke = Ripple:FindFirstChildWhichIsA("UIStroke")
	
	Ripple.Parent = CursorGui
	Ripple.BackgroundTransparency = 1
	Stroke.Thickness = 3
	Stroke.Enabled = true
	
	TweenService:Create(Ripple, RippleTweenInfo, {Size = UDim2.fromOffset(40, 40)}):Play()
	TweenService:Create(Stroke, RippleTweenInfo, {Transparency = 1}):Play()
	Debris:AddItem(Ripple, 1.25)
end

local function SetCursorAtMouse()
	local Mouse = Player:GetMouse()
	local MousePos = UserInputService:GetMouseLocation()
	local GuisAtPos = PlayerGui:GetGuiObjectsAtPosition(Mouse.X, Mouse.Y)
	local HoverGuiState = false

	for _, GuiElement in GuisAtPos do
		if GuiElement.Visible and table.find(HoverGuiList, GuiElement.ClassName) then
			HoverGuiState = true
			break
		end
	end
	
	DotFrame.Position = UDim2.fromOffset(MousePos.X, MousePos.Y)
	if HoverGuiState == true and DotStroke.Enabled == false then
		DotFrame.Size = UDim2.fromOffset(16, 16)
		DotFrame.BackgroundTransparency = 1
		DotStroke.Enabled = true
	elseif HoverGuiState == false and DotStroke.Enabled == true then
		DotFrame.Size = UDim2.fromOffset(6, 6)
		DotFrame.BackgroundTransparency = 0
		DotStroke.Enabled = false
	end
end

UserInputService.MouseIconEnabled = false
RunService.Heartbeat:Connect(SetCursorAtMouse)
UserInputService.InputBegan:Connect(function(Input, Processing)
	if not Processing and table.find(InputList, Input.UserInputType) then
		TweenRipple()
	end
end)
1 Like

I see where I messed up! Optimizing this script was a much needed adjustment. Replacing ZIndex with display order was also helpful, thanks!

1 Like

And now datastores and avatar services are down on studio… Great
EDIT: I got banned for 6 months :weary: