Bubble click effect module

What does this module do?

I made a module couple weeks ago and I thought I would share it with the community. This is an easy to use module that provides the functionality of adding a “bubble click” effect to any GUI object(demonstrations will be shown later in the post). The module works by calling it’s method “BindObjs” which takes as argument a table containing the GUI object(s) and then a connection is created to detect when the user clicks or taps any of the binded GUI object(s)

Methods of the module:

function module:BindObjs(objs:{[Instance]: boolean|{Color:Color3?, Gpe:boolean?, Tinfo:TweenInfo?, nilIdentifier:any?, ImageTransparency:number?, Image:string?, Callback:(GuiObject)->()?}}, SecondaryFilterClasses:{string}?, blacklist:boolean?)
:BindObjs()

This method is very flexible which makes it easy to use. This method allows to bind GUI object(s) in one of of 6 ways which includes the ability to add optional arguments to specific GUI objects!

Current optional arguments available:

Keep in mind that the default behavior can be edited in the module itself

Color”: a color3 object the click effect image will be for the specific GUI object (The color of the click effect image is black by default)

Gpeaka the GameProcessedEvent argument of UserInputService.InputBegan: a boolean indicating what the GameProcessedEvent argument needs to be in order for the click effect to be triggered for that specific GUI object (It doesn’t take into account the GameProcessedEvent argument of UserInputService.InputBegan by default)

Callback”: a callback function that is called when the click effect is done tweening on the GUI object (there is no callback by default)

nilIdentifier”: The purpose of this is to act as a substitute for nil since it can’t be stored in a normal table so you can use specified nilIdentifier or the default when you want to set a custom setting as nil ("nil" is the default identifier)

ImageTransparency”: This indicates what the transparency of the image used for the click effect should start as. (0 is the default)

Image”: The indicates what the “Image” property of the click effect image should be (the possibilities of the effects you can create are basically endless!). (“rbxassetid://6193144138” is the default (basically a white circle))
white circle


  1. Binding a single GUI object:
local object = *GUI object*
module:BindObjs(object)

  1. Binding GUI objects in a simple array like form:
local object1 = *GUI object*
local object2 = *GUI object*
local object3 = *GUI object*

module:BindObjs({object1,object2,object3,object...})

  1. Binding the children/descendants of a GUI and letting the module do the filtering for you! (This can be used with the second argument “SecondaryFilterClasses” and third argument “blacklist” to further filter out GUI object(s) you don’t/do want to apply the effect to!) (an example is a ScreenGui)
local ScreenGui = *GUI*

module:BindObjs(ScreenGui:GetChildren(), {"TextButton", "ImageButton"}) --Only gui objects that are TextButtons and ImageButtons will have the effect applied!


*OR*


local ScreenGui = *GUI*

module:BindObjs(ScreenGui:GetChildren(), {"TextButton", "ImageButton"}, true) --All GUI objects but TextButtons and ImageButtons will have the effect applied!



  1. Binding GUI objects in a dictionary like form:
local object1 = *GUI object*
local object2 = *GUI object*
local object3 = *GUI object*

module:BindObjs({
   [object1] = true,
   [object2]= true,
   [object3] = true,
   [object...] = true,
})

  1. Binding GUI objects in dictionary like form with custom settings!
local object1 = *GUI object*
local object2 = *GUI object*

local function OnClickEffectTweened(object:GuiObject)
   print(object.Name.. " finished it's click effect!")
end

module:BindObjs({
   [object1] = {Color = Color3.new(1,1,1), Tinfo = TweenInfo.new(0.1, Enum.EasingStyle.Sine), Gpe = true, Callback = OnClickEffectTweened},
   [object2] = {Color = Color3.new(1,0,1), Tinfo = TweenInfo.new(0.5, Enum.EasingStyle.Quart), Gpe = false, Callback = OnClickEffectTweened},
   object... = ...,
})

  1. Simply updating the custom settings for a specific GUI object! (keep in mind that previous settings of a GUI object will remain the same if you don’t specify when updating it’s custom settings)
local object1 = *GUI object*
local object2 = *GUI object*

local function OnClickEffectTweened(object:GuiObject)
   print(object.Name.. " finished it's click effect!")
end

module:BindObjs({
   [object1] = {Color = Color3.new(1,1,1), Tinfo = TweenInfo.new(0.1, Enum.EasingStyle.Sine), Gpe = true, Callback = OnClickEffectTweened},
   [object2] = {Color = Color3.new(1,0,1), Tinfo = TweenInfo.new(0.5, Enum.EasingStyle.Quart), Gpe = false, Callback = OnClickEffectTweened},
   [object...] = ...,
})

*later on in script*

module:BindObjs({
   [object1] = {Color = Color3.new(.5,.2,.9), Tinfo = TweenInfo.new(0.75, Enum.EasingStyle.Bounce), Gpe = "nil", Callback = "nil"},
   [object...] = ...,
})



function module:UnbindObjs(objs:{GuiObject} | {[GuiObject]: any} | GuiObject, SecondaryFilterClasses:{string}?, blacklist:boolean?)
:UnbindObjs()

This method is used is unbind GUI object(s) from the effect temporarily so you can call :BindObjs() on them later on in your script. You can unbind GUI object(s) in one of 4 ways.

  1. Unbinding a single GUI object (Might as well use :RemoveObjs() since the main purpose of this method is to temporarily disable the effect while still saving it’s custom setting and you can’t do that with single objects without using a table)
local object = *GUI object*

module:BindObjs(object)

*later in the script*

module:Unbind(object)

  1. Unbinding an array of GUI objects
local object1 = *GUI object*
local object2 = *GUI object*
local object3 = *GUI object*

module:BindObjs({
[object1] = ...,
[object2] = ...,
[object3] = ...,
[object...] = ...,
})

*later on in the script*

module:UnbindObjs({object1,object2,object3,object...})

  1. Unbinding the children/descendants of a GUI and letting the module do the filtering for you! (This can also be used with the second argument “SecondaryFilterClasses” and third argument “blacklist” to further filter out GUI object(s) you don’t/do want to unbind!) (an example is a ScreenGui)
local ScreenGui = *GUI*

module:BindObjs(ScreenGui:GetChildren())

Later in the script

module:UnbindObjs(ScreenGui:GetChildren(), {"TextButton", "ImageButton"}, true) --All GUI objects but TextButtons and ImageButtons will be unbinded!


*OR*


module:UnbindObjs(ScreenGui:GetChildren(), {"TextButton", "ImageButton"}) --Only GUI objects that are TextButtons and ImageButtons will be unbinded!


  1. Unbinding a dictionary of GUI objects
local object1 = *GUI object*
local object2 = *GUI object*
local object3 = *GUI object*


module:BindObjs({
[object1] = ...,
[object2] = ...,
[object3] = ...,
[object...] = ...,
})

*later on in the script*

module:UnbindObjs({
   [object1] = true,
   [object2]= true,
   [object3] = true,
   [object...] = true,
})



function BubbleClick:RemoveObjs(objs:{GuiObject}|{[GuiObject]: boolean}|GuiObject, SecondaryFilterClasses:{string}?, blacklist:boolean?)
:RemoveObjs()

The same as “:UnbindObjs” except it’s rather permanent than temporary (useful when you don’t longer need the effect on a GUI object)




function module:SetSettings(UpdatedSettings:{DefaultTinfo:TweenInfo?, DefaultColor:Color3?, DefaultGpeBehavior:boolean?, DefaultCallback:(obj:GuiObject)->()?, DefaultnilIdentifier:any?, DefaultImageTransparency:number?, DefaultImage:string?})
:SetSettings()

This method is used when you want to update specific default settings (this is useful when you want different default behaviors for different scripts)




function module:Simulate(v:GuiObject, Pos:Vector2, ObjSettings:{Color:Color3?, Tinfo:TweenInfo?, Callback:()->()?, ImageTransparency:number?, Image:string?}?)
:Simulate()

This is a method you won’t really interact with directly often but is there incase you wanted to manually trigger the effect on a GUI object without the need of user interaction.
Explaining the argument

v”: The GUI object you want to apply the effect on
Pos”: A Vector2 object containing the X and Y scale where the effect should originate from relative the object’s position
ObjSettings”: An optional argument for custom settings for the effect




function module:InBound(obj:GuiObject,x:number,y:number):Vector2?
:InBound()

This function returns a Vector2 object containing the X and Y scale relative to “obj” if the x and y pixel coordinates are within bound of “obj”


Code used to achieve the above (num1, num1B):

local gui = script.Parent

local rep = game:GetService("ReplicatedStorage")
local module = require(rep:WaitForChild("ClickEffectModule"))

repeat task.wait() until #gui:GetChildren() == #game:GetService("StarterGui").ScreenGui:GetChildren()
--
local objs = {}

local index = 0
local cols = {
	Color3.fromRGB(0,0,0),
	Color3.fromRGB(255,0,0),
	Color3.fromRGB(0,255,0),
	Color3.fromRGB(0,0,255),
	Color3.fromRGB(255,255,0),
	Color3.fromRGB(0,255,255),
	Color3.fromRGB(255,0,255),
	Color3.fromRGB(255, 85, 0),
	Color3.fromRGB(85, 0, 0),
	Color3.fromRGB(170, 85, 255),
}


for _,v in pairs(gui:GetChildren()) do
	if v:IsA("GuiObject") then
		local col = cols[(index%#cols)+1]
		index += 1
		objs[v] = {Color = col}
	end
end


module:BindObjs(objs)

Code used to achieve the above(num2):

local gui = script.Parent

local rep = game:GetService("ReplicatedStorage")
local module = require(rep:WaitForChild("ClickEffectModule"))

repeat task.wait() until #gui:GetChildren() == #game:GetService("StarterGui").ScreenGui:GetChildren()
--

while true do
	task.wait(.5)
	module:Simulate(gui.TextButton, Vector2.new(.5,.5))
end

Code used to achieve the above(num3):

local gui = script.Parent

local rep = game:GetService("ReplicatedStorage")
local module = require(rep:WaitForChild("ClickEffectModule"))

repeat task.wait() until #gui:GetChildren() == #game:GetService("StarterGui").ScreenGui:GetChildren()
--

while true do
	task.wait(.5)
	for _,v in pairs(gui:GetChildren()) do
		if v:IsA("GuiObject") then
			module:Simulate(v, Vector2.new(.5,.5))
		end
	end
	
end


Code used to achieve the above(num4):

local gui = script.Parent

local rep = game:GetService("ReplicatedStorage")
local module = require(rep:WaitForChild("ClickEffectModule"))

repeat task.wait() until #gui:GetChildren() == #game:GetService("StarterGui").ScreenGui:GetChildren()
--

while true do
	task.wait(.5)
	for _,v in pairs(gui:GetChildren()) do
		if v:IsA("GuiObject") then
			module:Simulate(v, Vector2.new(.5,.5), {Color = Color3.fromRGB(math.random(0,255), math.random(0,255), math.random(0,255))})
		end
	end
	
end

Code used to achieve the above(num5):

local gui = script.Parent

local rep = game:GetService("ReplicatedStorage")
local module = require(rep:WaitForChild("ClickEffectModule"))

repeat task.wait() until #gui:GetChildren() == #game:GetService("StarterGui").ScreenGui:GetChildren()
--

module:SetSettings({DefaultImage = "rbxassetid://378834872"})
while true do
	task.wait(.5)
	for _,v in pairs(gui:GetChildren()) do
		if v:IsA("GuiObject") then
			module:Simulate(v, Vector2.new(.5,.5), {Color = Color3.fromRGB(math.random(0,255), math.random(0,255), math.random(0,255))})
		end
	end
	
end

Code used to achieve the above(num6):

module:SetSettings({DefaultTinfo = TweenInfo.new(1.5, Enum.EasingStyle.Bounce), DefaultImage = "rbxassetid://378834872"})
while true do
	task.wait(.5)
	for _,v in pairs(gui:GetChildren()) do
		if v:IsA("GuiObject") then
			module:Simulate(v, Vector2.new(.5,.5), {Color = Color3.fromRGB(math.random(0,255), math.random(0,255), math.random(0,255))})
		end
	end
	
end

Code used to achieve the above(num7):

module:SetSettings({DefaultImage = "rbxassetid://1249690853"})
while true do
	task.wait(.5)
	for _,v in pairs(gui:GetChildren()) do
		if v:IsA("GuiObject") then
			module:Simulate(v, Vector2.new(.5,.5), {Color = Color3.fromRGB(math.random(0,255), math.random(0,255), math.random(0,255))})
		end
	end
	
end

Link to get the module: BubbleClickEffectModule - Roblox

Module Source Code!
local BubbleClick = {}
local Binded = {}

--Services
local Players = game:GetService("Players")
local UIS = game:GetService("UserInputService")
local Tservice = game:GetService("TweenService")

--Player related stuff
local plr = Players.LocalPlayer
local Mouse = plr:GetMouse()


--Preset
local BubbleEffect = script:WaitForChild("BubbleEffectPreset")


--local function
local function Coroutine(func:()->(),...)
	--coroutine.resume(coroutine.create(func), ...)
	coroutine.resume(coroutine.create(function(...)
		local suc,err = pcall(func,...)
		if not suc then warn("[Bubble Click Effect] please report:", err) end
	end),...)

	--[[You can also do:
	  task.spawn(func, ...)
	--]]
end




--Settings ref
local Settings = {
	DefaultTinfo = TweenInfo.new(1,Enum.EasingStyle.Linear, Enum.EasingDirection.Out),
	DefaultColor = Color3.new(),
	DefaultGpeBehavior = nil,
	DefaultCallback = nil,
	DefaultnilIdentifier = "nil",
	DefaultImageTransparency = 0,
	DefaultImage = "rbxassetid://6193144138",
}


local acceptedClasses = {
	Frame          = true;
	ImageButton    = true;
	ImageLabel     = true;
	TextButton     = true;
	TextLabel      = true;
	ScrollingFrame = true;
	ViewportFrame  = true;
}
local emptyTab = {}


local connection:RBXScriptConnection = nil

local function Disconnect()
	if connection and connection.Connected then
		connection:Disconnect()
		connection = nil
	end
end 
local function CreateConnection()
	connection = UIS.InputBegan:Connect(function(input, gpe)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			local x,y = Mouse.X,Mouse.Y
			for obj,v in pairs(Binded) do	
				if v.Paused then continue end
				
				
				local objGpeBehavior = v.Gpe
				
				
				if (objGpeBehavior and gpe ~= objGpeBehavior) or (Settings.DefaultGpeBehavior ~= nil and objGpeBehavior == nil and gpe ~= Settings.DefaultGpeBehavior) then continue end

				if not obj.Visible then continue end
				local Pos = BubbleClick:InBound(obj,x,y)

				if Pos then
					BubbleClick:Simulate(obj, Pos, v)
				end
			end
		end
	end)
end





function BubbleClick:InBound(obj:GuiObject,x:number,y:number):Vector2?
	local AbsPos = obj.AbsolutePosition
	local AbsSize = obj.AbsoluteSize

	return (x>AbsPos.X and x<AbsPos.X+AbsSize.X and y>AbsPos.Y and y<AbsPos.Y+AbsSize.Y and Vector2.new((x-AbsPos.X)/AbsSize.X,(y-AbsPos.Y)/AbsSize.Y)) or nil
end


function BubbleClick:Simulate(v:GuiObject, Pos:Vector2, ObjSettings:{Color:Color3?, Tinfo:TweenInfo?, Callback:()->()?, ImageTransparency:number?, Image:string?}?)
	Coroutine(function()
		ObjSettings = ObjSettings or {}
		local AbsSize = v.AbsoluteSize

		local BubbleEffectClone = BubbleEffect:Clone()
		BubbleEffectClone.Position = UDim2.new(0,0,0,0)
		BubbleEffectClone.Size = UDim2.new(1,0,1,0)

		BubbleEffectClone.Image.Image = ObjSettings.Image or Settings.DefaultImage
		BubbleEffectClone.Image.ImageTransparency = ObjSettings.ImageTransparency or Settings.DefaultImageTransparency
		BubbleEffectClone.Image.ImageColor3 = ObjSettings.Color or Settings.DefaultColor
		BubbleEffectClone.Image.AnchorPoint = Vector2.new(0.5,0.5)
		BubbleEffectClone.Image.Position = UDim2.new(Pos.X, 0, Pos.Y, 0)
		BubbleEffectClone.Parent = v

		local Magnitude = (Vector2.one*0.5 - Pos).Magnitude
		local SizeRatio =  math.max(AbsSize.X,AbsSize.Y) / math.min(AbsSize.X,AbsSize.Y)
		local Size = UDim2.fromScale(2 * (1+Magnitude) * SizeRatio, 2 * (1+Magnitude) * SizeRatio)
		local Tween = Tservice:Create(BubbleEffectClone.Image, ObjSettings.Tinfo or Settings.DefaultTinfo, {ImageTransparency = 1, Size = Size})
		Tween:Play()
		Tween.Completed:Wait()
		BubbleEffectClone:Destroy()

		if type(ObjSettings.Callback) == "function" then
			ObjSettings.Callback(v)
		elseif type(Settings.DefaultCallback) == "function" then
			Settings.DefaultCallback(v)
		end
	end)
end


function BubbleClick:BindObjs(objs:{[Instance]: boolean|{Color:Color3?, Gpe:boolean?, Tinfo:TweenInfo?, nilIdentifier:any?, ImageTransparency:number?, Image:string?, Callback:(GuiObject)->()?}}, SecondaryFilterClasses:{string}?, blacklist:boolean?)
	objs = (typeof(objs) == "Instance" and {objs=true}) or (typeof(objs) == "table" and objs) or nil
	if not objs then return end
	
	for i,v in pairs(objs) do
		local indexType = type(i)
		local obj = (indexType == "number" or indexType == "string") and v or i
		local val = v 

		if typeof(obj) == "Instance" and acceptedClasses[obj.ClassName] then
			if SecondaryFilterClasses then
				local found = table.find(SecondaryFilterClasses, obj.ClassName)
				if ((not blacklist and not found) or  (blacklist and found)) then continue end
			end
			
			
			local objSettings = Binded[obj] or {}
			local UpdatedObjSettings = type(v) == "table" and v or emptyTab
			
			for j,k in pairs(UpdatedObjSettings) do
				if j ~= "nilIdentifier" then
					if k == objSettings.nilIdentifier or k == Settings.DefaultnilIdentifier then
						objSettings[j] = nil 
					end
				end
			end
		
		
			local objFinalSettings = {
				Color = objSettings.Color,
				Callback = objSettings.Callback,
				Gpe = objSettings.Gpe,
				Tinfo = objSettings.Tinfo,
				ImageTransparency = objSettings.ImageTransparency,
				Image = objSettings.Image,
				Paused = nil,
			}

			Binded[obj] = objFinalSettings
		end
	end
	if not connection then
		CreateConnection()
	end
end

function BubbleClick:UnbindObjs(objs:{GuiObject} | {[GuiObject]: any} | GuiObject, SecondaryFilterClasses:{string}?, blacklist:boolean?)
	objs = (typeof(objs) == "Instance" and {objs}) or (typeof(objs) == "table" and objs) or nil
	if not objs then return end

	for i,v in pairs(objs) do
		local indexType = type(i)
		local obj = (indexType == "number" or indexType == "string") and v or i
		
		if SecondaryFilterClasses then
			local found = table.find(SecondaryFilterClasses, obj.ClassName)
			if ((not blacklist and not found) or  (blacklist and found)) then continue end
		end
		
		
		if Binded[obj] then
			Binded[obj].Paused = true
		end
	end


	for _,_ in pairs(Binded) do return end
	Disconnect()
end

function BubbleClick:RemoveObjs(objs:{GuiObject}|{[GuiObject]: boolean}|GuiObject, SecondaryFilterClasses:{string}?, blacklist:boolean?)
	objs = (typeof(objs) == "Instance" and {objs}) or (typeof(objs) == "table" and objs) or nil
	if not objs then return end

	for i,v in pairs(objs) do
		local indexType = type(i)
		local obj = (indexType == "number" or indexType == "string") and v or i
		
		if SecondaryFilterClasses then
			local found = table.find(SecondaryFilterClasses, obj.ClassName)
			if ((not blacklist and not found) or  (blacklist and found)) then continue end
		end


		Binded[obj] = nil
	end


	for _,_ in pairs(Binded) do return end
	Disconnect()
end




function BubbleClick:SetSettings(UpdatedSettings:{DefaultTinfo:TweenInfo?, DefaultColor:Color3?, DefaultGpeBehavior:boolean?, DefaultCallback:(obj:GuiObject)->()?, DefaultnilIdentifier:any?, DefaultImageTransparency:number?, DefaultImage:string?})
	Settings.DefaultnilIdentifier = UpdatedSettings.DefaultnilIdentifier or Settings.DefaultnilIdentifier
	local nilIdentifier = Settings.DefaultnilIdentifier
	for i,v in pairs(UpdatedSettings) do
		if i ~= "DefaultnilIdentifier" and Settings[i] then
			local val = v ~= nilIdentifier and v or nil 
			Settings[i] = val
		end
	end
end


return BubbleClick

Sorry if the video doesn’t load, not much I can do about it

I am sorry for not documenting my code. This is because I spent a lot of time explaining and giving examples in this post (I might update the module to add some though). Also, this only supports Pc and mobile/tablet as I do not own an Xbox which makes making codes Xbox compatible hard! If requested though, I will look into it!

Poll Time!

How likely are you to use this module?

  • Very likely
  • Likely
  • I will consider
  • maybe?
  • Not likely
  • Other (comment your reason please)

0 voters

If you have any question/concern/feedback/request, please feel free to reply to this post!

33 Likes

The videos wont load for me at all! Can you fix that please??

1 Like

this is just a better version of what I made! GUI Ripple Effect

I have uploaded all the videos to a playlist on YouTube so you should be able to view them!

that look good module i like it.

i’m glad you could find my resource useful after 2 years!

1 Like

very handy resource - thanks!! :slightly_smiling_face:

I’m glad you could find a use for my resource!

1 Like