I found out how to do this! First, I tried finding the midpoint and angle between two GuiObjects based on their AbsolutePositions. Finding distance along with this would work, but Roblox is weird about resizing rotated GuiObjects (it failed.)
So, I moved towards the 2d beam approach. This would mean that I would not have to find a Midpoint, but rather interpolate images between two points. An important function needed was the conversion from AbsolutePosition to Position (relative).
local function ConvertAbsolutePosition(GuiObject)
local x = (GuiObject.AbsolutePosition.X) + (GuiObject.AbsoluteSize.X/2)
local y = (GuiObject.AbsolutePosition.Y) + (GuiObject.AbsoluteSize.Y/2)
return UDim2.fromOffset(x,y)
end
--* This function only works when using the returned position in a ScreenGui
--* The returned value will be centered, so use AnchorPoints of (0.5,0.5)!!
--* If the ScreenGui has IgnoreGuiInset disabled, y += 36
With this, I was able to create an ObjectClass for a 2D Beam (UIBeam)! It’s pretty lackluster and clunky right now and I’m not sure if it’s as efficient as it can be, but it works for my game! (The full module is at the bottom of this reply!)
To use UIBeam, first check the module and connect the missing variables Maid (MaidClass)
and Dot (ImageLabel)
to correctly suit your place.
You can find the Maid class here! (By Quenty)
After that, you can use these functions to create UIBeams!
UIBeam UIBeam.new(beamData)
Creates a new UIBeam object.
beamData: dictionary, with these values {
Part0: GuiBase2d, start of the beam
Part1: GuiBase2d, end of the beam
TweenInfo: TweenInfo? (optional)
}
void UIBeam:Destroy()
Destroys the UIBeam, removes from memory.
void UIBeam:Init(parent)
Starts rendering the Beam.
parent: ScreenGui, where the beam will be rendered.
void UIBeam:SetNewPart(part, object)
Sets a new Start or End point for the beam.
part: int, either 0 or 1, indicates which part is being changed.
object: GuiBase2d, indicates what will be connected.
In addition to those functions, UIBeam also supports newindex
changes! Suppose you wrote in your code:
UIBeam.BackgroundColor3 = Color3.fromRGB(123, 23, 54)
This would work on UIBeam, as would any other ImageLabel property like ImageTransparency
or Image
itself! Of course, this can change on what you change your Dot
variable to within the script. Unfortunately, you cannot directly change values of the UIBeam, once :Init()
is called, you cannot change values within the beam.
UIBeam Module:
-- Services
local TweenService = game:GetService("TweenService")
-- SRO
local Maid = -- Maid class. (By Quenty!)
local DEFAULT_INFO = TweenInfo.new(
3,
Enum.EasingStyle.Linear,
Enum.EasingDirection.Out,
-1,
false,
0
)
-- V, T, C
local Dot = -- ImageLabel of a white circle. (Size: UDim2.fromOffset(10,10))
local function FirstTweenInfo(tweenInfo: TweenInfo, duration: number)
return TweenInfo.new(
duration,
tweenInfo.EasingStyle,
tweenInfo.EasingDirection,
0,
false,
tweenInfo.DelayTime
)
end
local UIBeam = {}
UIBeam.__index = UIBeam
UIBeam.__type = "UIBeam"
-- Creates a new UIBeam object.
function UIBeam.new(beamData)
local self = setmetatable({
_part0 = beamData.Part0,
_part1 = beamData.Part1,
_points = {},
_settings = {},
_maid = Maid.new(),
_active = false,
_tweens = Maid.new(),
_tweenInfo = (beamData.TweenInfo or DEFAULT_INFO),
}, UIBeam)
return self
end
-- Removes the UIBeam from memory.
function UIBeam:Destroy()
self._maid:DoCleaning()
self._tweens:DoCleaning()
rawset(self, "_maid", nil)
rawset(self, "_tweens", nil)
rawset(self, "_part0", nil)
rawset(self, "_part1", nil)
rawset(self, "_ignoreInset", nil)
rawset(self, "_parent", nil)
for _, p in pairs(self._points) do
p:Destroy()
end
table.clear(self._points)
table.clear(self._settings)
rawset(self, "_points", nil)
rawset(self, "_settings", nil)
self = nil
end
-- Returns the distance (in pixels) between the ends of the beam
function UIBeam:CalcDistance()
local xDif = (self._part0.AbsolutePosition.X - self._part1.AbsolutePosition.X)
local yDif = (self._part0.AbsolutePosition.Y - self._part1.AbsolutePosition.Y)
return math.sqrt((xDif^2) + (yDif^2))
end
-- Returns the RelativePosition of the start of the beam.
function UIBeam:CalcStartPosition()
local x = (self._part0.AbsolutePosition.X) + (self._part0.AbsoluteSize.X/2)
local y = (self._part0.AbsolutePosition.Y) + (self._part0.AbsoluteSize.Y/2)
if self._ignoreInset then y += 36 end
return Vector2.new(x,y)
end
-- Returns the RelativePosition of the end of the beam.
function UIBeam:CalcEndPosition()
local x = (self._part1.AbsolutePosition.X) + (self._part1.AbsoluteSize.X/2)
local y = (self._part1.AbsolutePosition.Y) + (self._part1.AbsoluteSize.Y/2)
if self._ignoreInset then y += 36 end
return Vector2.new(x,y)
end
-- Returns the angle between the two ends of the beam (in radians)
function UIBeam:CalcAngle()
local x = math.abs(self._part0.AbsolutePosition.X - self._part1.AbsolutePosition.X)
local y = math.abs(self._part0.AbsolutePosition.Y - self._part1.AbsolutePosition.Y)
return math.atan(y/x)
end
-- Starts rendering the beam.
function UIBeam:Init(parent)
if not parent:IsA("ScreenGui") then return end
rawset(self, "_ignoreInset", parent.IgnoreGuiInset)
rawset(self, "_parent", parent)
rawset(self, "_active", true)
self:Recalibrate()
self._maid:GiveTask(self._part0:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
self:Recalibrate()
end))
self._maid:GiveTask(self._part1:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
self:Recalibrate()
end))
end
-- Sets one of the beam ends to another object.
function UIBeam:SetNewPart(part, object)
if part == 0 then
rawset(self, "_part0", object)
elseif part == 1 then
rawset(self, "_part1", object)
end
self._maid:DoCleaning()
self:Recalibrate()
self._maid:GiveTask(self._part0:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
self:Recalibrate()
end))
self._maid:GiveTask(self._part1:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
self:Recalibrate()
end))
end
-- Updates the beam.
function UIBeam:Recalibrate()
self._tweens:DoCleaning()
for _, p in pairs(self._points) do
p:Destroy()
end
table.clear(self._points)
local inc = (1/self:CalcDistance() * 20)
local points = math.floor(1/inc)+1
local p0 = self:CalcStartPosition()
local p1 = self:CalcEndPosition()
local angle = math.deg(self:CalcAngle())
for i = 1, points do
local nAlpha = i/points
local rAlpha = (-nAlpha) + 1
local position = p0:Lerp(p1, nAlpha)
local newDot = Dot:Clone()
newDot.Visible = true
newDot.Position = UDim2.fromOffset(position.X, position.Y)
newDot.Parent = self._parent
newDot.Rotation = angle
for k, v in pairs(self._settings) do
newDot[k] = v
end
self._points[#self._points + 1] = newDot
local info = FirstTweenInfo(self._tweenInfo, self._tweenInfo.Time * rAlpha)
local t = TweenService:Create(newDot, info, {
Position = UDim2.fromOffset(p1.X, p1.Y)
})
local connect = t.Completed:Connect(function()
newDot.Position = UDim2.fromOffset(p0.X, p0.Y)
t = TweenService:Create(newDot, self._tweenInfo, {
Position = UDim2.fromOffset(p1.X, p1.Y)
})
t:Play()
end)
self._tweens:GiveTask(connect)
t:Play()
end
local newDot = Dot:Clone()
local defaultSize = Dot.Size
for k, v in pairs(self._settings) do
if k == "Size" then defaultSize = v continue end
if k == "ImageTransparency" then continue end
newDot[k] = v
end
newDot.Visible = true
newDot.Position = UDim2.fromOffset(p1.X, p1.Y)
newDot.Size = defaultSize + UDim2.fromOffset(7,7)
newDot.Parent = self._parent
self._points[#self._points+1] = newDot
end
-- Checks for when one wants to change beam properties.
function UIBeam:__newindex(index, value)
self._settings[index] = value
if self._active then
for _, p in pairs(self._points) do
p[index] = value
end
end
end
return UIBeam