[v1.1] Easily create a floating gui with this module! (Beginner friendly)

Were you ever playing games like PHIGHTING! [ALPHA] or Jujutsu Infinite and really liked their GUI because it was uniquely floating? Have you tried recreating their unique GUI but failed?

Well this module is an easy way to implement a uniquely floating GUI into your game!

Examples:

Creating a floating gui.

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local ScreenGui = PlayerGui:WaitForChild('ScreenGui')

local Gui = FloatingGui.new(ScreenGui, {
    -- These are all the options you can provide:

	Distance = 0.2, -- Distance in studs in between the camera and the floating gui.
	Speed = 0.9, -- Speed from 0 to 1 that represents the follow speed of the floating gui.
	Angle = 3, -- Rotation of the floating gui in degrees.
	Offset = Vector2.new(0, 0) -- Offset of the floating gui based on scale.
})


Changing an existing floating gui after two seconds.

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local ScreenGui = PlayerGui:WaitForChild('ScreenGui')

local Gui = FloatingGui.new(ScreenGui, {
	Distance = 1, -- Distance in studs in between the camera and the floating gui.
})

wait(2)

Gui:Change({
	Distance = 0.2, -- Distance in studs in between the camera and the floating gui.
	Speed = 0.9, -- Speed from 0 to 1 that represents the follow speed of the floating gui.
	Angle = 3, -- Rotation of the floating gui in degrees.
})


Removing an existing floating gui after two seconds.

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local ScreenGui = PlayerGui:WaitForChild('ScreenGui')

local Gui = FloatingGui.new(ScreenGui)

wait(2)

Gui:Remove()


Enabling and disabling a floating gui.

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local ScreenGui = PlayerGui:WaitForChild('ScreenGui')

local Gui = FloatingGui.new(ScreenGui)

Gui:Enable(false)

wait(2)

Gui:Enable(true) -- Gui:Enable(true) or Gui:Enable()


Creating multiple floating guis.

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local ScreenGui = PlayerGui:WaitForChild('ScreenGui')
local ScreenGui2 = PlayerGui:WaitForChild('ScreenGui2')

local Gui1 = FloatingGui.new(ScreenGui, {
	Distance = 0.2, -- Distance in studs in between the camera and the floating gui.
	Speed = 0.9, -- Speed from 0 to 1 that represents the follow speed of the floating gui.
	Angle = 3, -- Rotation of the floating gui in degrees.
	Offset = Vector2.new(0, 0) -- Offset of the floating gui based on scale.
})

local Gui2 = FloatingGui.new(ScreenGui2, {
	Distance = 0.2, -- Distance in studs in between the camera and the floating gui.
	Speed = 0.9, -- Speed from 0 to 1 that represents the follow speed of the floating gui.
	Angle = -3, -- Rotation of the floating gui in degrees.
	Offset = Vector2.new(0, 0) -- Offset of the floating gui based on scale.
})


A real example.


My Explorer:
image


My health bar and stamina bar:



My code (StarterGui.FloatingGui):

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local HealthGui = PlayerGui:WaitForChild('HealthGui')
local StaminaGui = PlayerGui:WaitForChild('StaminaGui')

local Gui1 = FloatingGui.new(HealthGui, {
	Distance = 0.2, -- Distance in studs in between the camera and the floating gui.
	Speed = 0.9, -- Speed from 0 to 1 that represents the follow speed of the floating gui.
	Angle = 3, -- Rotation of the floating gui in degrees.
	Offset = Vector2.new(0, 0) -- Offset of the floating gui based on scale.
})

local Gui2 = FloatingGui.new(StaminaGui, {
	Distance = 0.2, -- Distance in studs in between the camera and the floating gui.
	Speed = 0.9, -- Speed from 0 to 1 that represents the follow speed of the floating gui.
	Angle = -3, -- Rotation of the floating gui in degrees.
	Offset = Vector2.new(0, 0) -- Offset of the floating gui based on scale.
})


Result:


Exceptions.

local FloatingGui = require(game.ReplicatedStorage:WaitForChild('FloatingGui'))

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild('PlayerGui')
local ScreenGui = PlayerGui:WaitForChild('ScreenGui')

-- When no options are provided the floating gui will be given default options.
local Gui = FloatingGui.new(ScreenGui)


Source.

Link: https://create.roblox.com/store/asset/17170508056/FloatingGui?tab=description

(Please credit it me whenever you use it in your game, thanks!)
(Also would appreciate feedback or ideas for new features, you can message me on discord @lukanker.)

21 Likes

you forgot to add the module link to the post

2 Likes

Accidentally posted it when I wasn’t finished, but it’s all good now. Let me know if you come across any problems, because I made this module pretty quickly.

2 Likes

Looks great bro, you cooked!! :fire: :fire:

3 Likes

seems like its private?
Screenshot 2024-04-16 191144

3 Likes

I kid you not, I have been searching the internet for ages looking for a tutorial for this. You are the best!

3 Likes

Sorry! The model should be public now.

2 Likes

Version 1.1 changes if anyone is interested:

  • (Bug) The adornee of the SurfaceGui doesn’t stay anymore when the SurfaceGui is destroyed.
  • The ScreenGui used for the FloatingGui will be converted to the SurfaceGui and this SurfaceGui will have the same parent as the ScreenGui, in turn the ScreenGui’s parent becomes ReplicatedFirst (as temporary parent, for whenever the user decides to :Remove the FloatingGui, which will make the ScreenGui have its original parent again)
  • Any children inside the ScreenGui will be parented to the SurfaceGui. (children won’t be cloned over anymore, to avoid scripts running twice)
  • After calling :Remove on the FloatingGui, the ScreenGui will be parented back to its original parent, and all its original children will be parented back to the ScreenGui.
1 Like

Is there a way to add this floating mechanic to existing screenguis?

What do you mean exactly? You can use the .new function on an existing ScreenGui, yes.

Very nice, however I formatted the module to look a little nicer (and also to not loop thru every single created floating gui)

--!optimize 2

local ReplicatedFirst = game:GetService('ReplicatedFirst')
local RunService = game:GetService('RunService')
local Players = game:GetService('Players')

local PLAYER = Players.LocalPlayer
local CAMERA = workspace.CurrentCamera

local DEFAULT_OPTIONS = {
	Distance = 0,
	Angle = 0,
	Speed = 0.9,
	Offset = Vector2.new(0, 0)
}

type Options = {
	Distance: number,
	Angle: number,
	Speed: number,
	Offset: Vector2
}

local FloatingGui = {}

local function createPart(): Part
	local part = Instance.new("Part")
	part.Size = Vector3.new(20, 10, 0.5)
	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false
	part.CastShadow = false
	part.Massless = true
	part.Transparency = 1
	part.Anchored = true
	part.Name = 'FloatingGuiAdornee'
	part.Parent = CAMERA

	return part
end

local function createSurfaceGui(adornee): SurfaceGui
	local surfaceGui = Instance.new("SurfaceGui")
	surfaceGui.Adornee = adornee
	surfaceGui.Face = Enum.NormalId.Front
	surfaceGui.PixelsPerStud = 512
	surfaceGui.ClipsDescendants = false
	surfaceGui.AlwaysOnTop = true
	surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
	surfaceGui.Parent = PLAYER.PlayerGui

	return surfaceGui
end

local function log(message: string, functionName, ...)
	local hasTable = false

	for _, v in {...} do
		if typeof(v) == 'table' then
			hasTable = true
		end
	end

	if hasTable then
		warn(`{script.Name}{functionName}(`, ..., `) - {message}`)
	else
		warn(`{script.Name}{functionName}({... or ''}) - {message}`)
	end
end

local function getOptions(options: Options, functionName)
	for optionName, value in options do
		local defaultValue = DEFAULT_OPTIONS[optionName]
		local defaultType = typeof(DEFAULT_OPTIONS[optionName])

		if not defaultValue then 
			log(`{optionName} is not a valid option.`, functionName, options) 
			continue 
		end

		if typeof(value) ~= defaultType then 
			options[optionName] = options[optionName] or defaultValue
			log(`{optionName} should be of type: '{defaultType}'`, functionName, options) 
			continue 
		end

		options[optionName] = value
	end

	for optionName, value in DEFAULT_OPTIONS do
		if not options[optionName] then
			options[optionName] = value
		end
	end
	
	return options
end

function FloatingGui:Change(options)
	if not options or typeof(options) ~= 'table' then 
		log(`Provide a table.`, ':Change', options) 
		return 
	end
	
	self.options = getOptions(self.options, ":Change")
end

function FloatingGui:Remove()
	self:UnbindFromRenderStep()

	for _, child in self.surfaceGui:GetChildren() do
		child.Parent = self.screenGui
	end

	self.part:Destroy()
	self.surfaceGui:Destroy()

	if self.screenGui.Parent ~= nil then
		self.screenGui.Parent = self.screenGuiParent
	end
	
	setmetatable(self, nil)
	table.clear(self)
	self = nil
end

function FloatingGui:Enable(boolean: boolean?)
	if boolean and typeof(boolean) ~= 'boolean' then 
		log(`Provide a boolean or nothing.`, ':Enable', boolean) 
		return 
	end

	local enabled = boolean == nil and true or boolean

	if self.removed then 
		log(`You can't {enabled and 'enable' or 'disable'} a FloatingGui after removing it.`, ':Enable', boolean) 
		return 
	end

	self.surfaceGui.Enabled = boolean
end

function FloatingGui:BindToRenderStep()
	if self.renderConnection then return end

	self.renderConnection = RunService.RenderStepped:Connect(function()
		if not self.surfaceGui.Enabled then return end
		
		local part = self.part
		local options = self.options

		local distance = options.Distance
		local angle = options.Angle
		local speed = options.Speed
		local offset = options.Offset
		
		local forwardOffset = 1
		
		local height = math.tan(math.rad(CAMERA.FieldOfView/2)) * 2 * (forwardOffset + part.Size.Z / 2)
		local width = (CAMERA.ViewportSize.X / CAMERA.ViewportSize.Y) * height
		part.Size = Vector3.new(width, height, part.Size.Z)

		local xOffset = width * offset.X
		local yOffset = height * offset.Y
		local lerp = math.clamp(0.9 + speed / 10, 0, 1)

		local cf = (CAMERA.CFrame + CAMERA.CFrame.LookVector * (forwardOffset + distance + part.Size.Z) 
			+ CAMERA.CFrame.RightVector * xOffset + CAMERA.CFrame.UpVector * yOffset) 
			* CFrame.Angles(0, math.rad(180), 0)

		part.CFrame = part.CFrame:Lerp(cf, lerp) * CFrame.Angles(0, math.rad(angle), 0)
	end)
end

function FloatingGui:UnbindFromRenderStep()
	if self.renderConnection then
		self.renderConnection:Disconnect()
		self.renderConnection = nil
	end
end

local function new(screenGui: ScreenGui, options: Options)
	if not screenGui or screenGui.ClassName ~= 'ScreenGui' then 
		log(`Provide a ScreenGui.`, '.new', screenGui, options) 
	end
	local part = createPart()
	local surfaceGui = createSurfaceGui(part)
	
	local screenGuiParent = screenGui.Parent
	
	screenGui.Parent = ReplicatedFirst
	for _, child in screenGui:GetChildren() do
		child.Parent = surfaceGui
	end
	
	return setmetatable({
		screenGui = screenGui,
		options = getOptions(options or {}, ".new"),
		part = part,
		surfaceGui = surfaceGui,
		screenGuiParent = screenGuiParent,
	},{
		__index = FloatingGui
	})
end

return {
	new = new
}

For those who want it!!