Is my module really necessary?

I’ve made a module that handles tweening several objects. For example, I want to tween a text label to disappear, but it has a stroke. I would need to make a tween for two objects with basically the same thing, and it would look bad:

local TS = game:GetService("TweenService")

local textLabel = script.Parent
local stroke = textLabel.UIStroke

local tween = TS:Create(textLabel, TweenInfo.New(2, Enum.EasingStyle.Sine, Enum.EasingDirection.In), {TextTransparency = 1})
local tween2 = TS:Create(stroke, TweenInfo.New(2, Enum.EasingStyle.Sine, Enum.EasingDirection.In), {Transparency = 1})

tween:Play()
tween2:Play()

My module would turn all that into this:

local myModule = require(script.Module)

local textLabel = script.Parent
local stroke = textLabel.UIStroke

local tweens = myModule.Create({
   {TextTransparency = 1}, -- Goal for textLabel
   {Transparency = 1}, -- Goal for UIStroke
}, "2 sine in", textLabel, stroke) -- (time = 2 easingStyle = sine easingDirection = in), and textLabel and stroke, respectively.

tweens:Play() -- Will play both tweens

The actual module that does that:

--!nocheck
local TS = game:GetService("TweenService")

type goal = {{[string] : any}} | {[string] : any}
type info = string | TweenInfo | {string | TweenInfo}

type tweenObject = {
	Tweens : {Tween?},
	Objects : {Objects},
	Goals : goal?,
	Infos : {TweenInfo?} | TweenInfo,
	SetGoals : (self : tweenObject, Goals : goal, ...number?) -> (),
	SetInfos : (self : tweenObject, Infos : info, ...number?) -> (),
	AppendTweens : (self : tweenObject, ...number?) -> (),
	Play : (self : tweenObject, ...number?) -> (),
}

type tween = {
	Create : (Goals : goal?, Infos : info?, ...Object) -> tweenObject,
}

local tween : tween = {}
tween.__index = tween

local easingStyle = {"sine", "quad", "cubic", "linear", "quint", "exponential", "circular", "back", "quart", "bounce", "elastic"}
local easingDirection = {"out", "in", "inout"}
local booleans = {["true"] = true, ["false"] = false}

local syntax = {
	"number",
	easingStyle,
	easingDirection,
	"number",
	booleans,
	"number",
}

-- Returns a table with removed duplicated values from the specified `table`.
local function filtrate(indexes : {number}) : {number}
	local filtrated = {}

	for _, index in indexes do
		if table.find(filtrated, index) then continue end

		table.insert(filtrated, index)
	end

	return filtrated
end

-- Converts the specified `string` to a `TweenInfo` object.
local function convertToTweenInfo(info : string) : TweenInfo?
	local options = string.split(info, " ")

	local rawOptions = {}

	for pos, option in options do
		local currentSyntax = syntax[pos]

		if not currentSyntax then return end

		local convertion
		local value

		local syntaxType = typeof(currentSyntax)
		local lowered = string.lower(option)

		if syntaxType == "string" then
			convertion = "number"

			value = tonumber(option)
		else
			local enumType = pos == 2 and Enum.EasingStyle or (pos == 3 and Enum.EasingDirection)

			if enumType then
				convertion = "enum"

				if table.find(currentSyntax, lowered) then
					value = enumType[option:gsub("^%l", string.upper):gsub("out", "Out")]
				end
			else
				convertion = "boolean"

				for booleanName, booleanValue in currentSyntax do
					if lowered == booleanName then
						value = booleanValue

						break
					end
				end
			end
		end

		if value == nil then
			return warn("[{script.Name}]: Couldn't convert \"{option}\" to `{convertion}`; Correct syntax: \"number easingStyle easingDirection number boolean number\"")
		end

		table.insert(rawOptions, value)
	end
	
	return TweenInfo.new(table.unpack(rawOptions))
end

-- Creates a `tween` for the specified `object` with `goal` and `info`, storing it in a `table` for later use.
local function appendTween(object : Object, goal : {[string] : any}, info : TweenInfo, pos : number, store : {Tween?}) : ()	
	if object and goal then
		store[pos] = TS:Create(object, info, goal)
	end
end

-- Iterate through indexes and calls the `func` <strong>function</strong> for each index.
local function iterateThroughIndexes(indexes : {number}, func : (index : number) -> ()) : ()
	indexes = filtrate(indexes)

	for _, index in indexes do
		func(index)
	end
end

-- Resets the specified `table` or `string` with the `objects`.
local function reset(t : {any} | string, objects : {Object}) : {any}
	local nextGoal = {}

	for pos in objects do
		nextGoal[pos] = t
	end
	
	return nextGoal
end

function tween.Create(Goals, Infos, ...)
	local objects = {...}

	if #objects == 0 then
		return warn("[{script.Name}]: No objects to tween")
	end

	local self = {}

	self.Objects = objects
	self.Tweens = {}
	self.Goals = {}
	self.Infos = TweenInfo.new()

	setmetatable(self, tween)

	self:SetInfos(Infos)
	self:SetGoals(Goals)
	self:AppendTweens()

	return self
end

function tween:SetGoals(Goals, ...)
	if debug.info(2, "s") ~= "Create" then
		if not Goals then
			return warn("[{script.Name}]: Argument 'Goals' is missing")
		end
	end

	local objects = self.Objects

	if ... then
		if not self.Goals[1] then
			self.Goals = reset(self.Goals, objects)
		end

		iterateThroughIndexes({...}, function(index)
			if index < 1 or index > #objects then return end

			self.Goals[index] = Goals
		end)

		return
	end

	self.Goals = Goals
end

function tween:SetInfos(Infos, ...)
	if debug.info(2, "s") ~= "Create" then
		if not Infos then
			return warn("[{script.Name}]: Argument 'info' is missing")
		end
	end

	local initializeConversion = function(object : (string | TweenInfo)?) : TweenInfo?
		object = object or Infos

		if typeof(object) == "string" and string.len(object) > 0 then
			return convertToTweenInfo(object)
		end
	end

	local objects = self.Objects

	if ... then
		if typeof(self.Infos) ~= "table" then
			self.Infos = reset(self.Infos, objects)
		end

		iterateThroughIndexes({...}, function(index)
			if index < 1 or index > #objects then return end

			local info = self.Infos[index]

			if info then
				self.Infos[index] = initializeConversion(Infos)
			end
		end)
	else
		if typeof(Infos) == "table" then
			for pos, info in Infos do
				Infos[pos] = initializeConversion(info)
			end
		else
			Infos = initializeConversion()
		end
		
		self.Infos = Infos
	end
end

function tween:AppendTweens(...)
	local objects = self.Objects
	local info = self.Infos
	local goals = self.Goals
	local tweens = self.Tweens
	local completed = self.CompletedTweens
	local appendConnection = self.AppendConnection

	if ... then
		iterateThroughIndexes({...}, function(index)
			if index < 1 or index > #objects then return end

			local object = objects[index]
			local goal = goals[index] or goals

			appendTween(object, goal, typeof(info) == "table" and info[index] or info, index, tweens)
		end)
	else
		for pos, object in objects do
			local goal = goals[pos] or goals

			appendTween(object, goal, typeof(info) == "table" and info[pos] or info, pos, tweens)
		end
	end
end

function tween:Play(...)
	local objects = self.Objects
	local tweens = self.Tweens

	if #tweens == 0 then
		return warn("[{script.Name}]: There are no tweens to play")
	end

	if ... then
		iterateThroughIndexes({...}, function(index)
			if index < 1 or index > #objects then return end

			local tween = tweens[index]

			if tween then
				tween:Play()
			end
		end)
	else
		for pos, object in objects do
			local tween = tweens[pos]

			if tween then
				tween:Play()
			end
		end
	end
end

return tween

Though, is that really necessary? Perhaps I’m just overthinking, and the first case where I use TweenService is more suitable.

2 Likes

I meaaan, it’s not “necessary” but – do whatever you want champ. If you think it’s going to help you with a lot of simultaneous tweens + make your code cleaner in your eyes.

…then sure.

2 Likes

For the first use case, it may not be that necessary but a helper can help a lot.
Like a Trove library/package, your module can be a useful to organise and create cinematic effects together which may be your outlook to making this module into a Helper.

But, it should use a bit more commonly known words instead of your own.
For example myModule.Create() may be just better as TweensHelpers.new(…) like Trove.new()
Also two methods is hard to understand, :SetGoals() and :SetInfo() for me perosnally. And :AppendTweens() seems like something it should do by itself whenever a new Tween object is being added into the Tween(self) here.

Just my personal opinion, upon being created it should not uses its own methods though. If it should be used inside .new() it should be considered naming as local function instead of Tween:()

Overall, it can be useful and I find making your own package can make your own game development faster too. And this would remind me of “Motor” package that uses Spring/Linear etc, it also have something like MotorsGroups which MotorsGroup:SetGoal() would make all the motors inside the group to the set goal, rather than doing it induvial.

2 Likes

Thanks, I’ll implement your ideas!

Bro is Sherlock’s Father

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