Beams for 2d objects?

For my game, I’m trying to indicate interactions between tiles on a grid. I thought the best way to do this would be to have an arrow from a tile pointing towards another tile.

Kinda like this:
image

The thing is, I have literally no idea how to do this. All the tiles are parented to a big parent frame with a UIGridLayout, which kind of throws out a lot of my ideas. I’ve also tried using AbsolutePositions and math, but it never really worked correctly. All I have access to is the Frames that represent the tiles and where they are located on a grid.

Oh, and I also researched if there was a way to have Beams for 2d objects. It would obviously help here, but I found nothing.

1 Like

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
2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.