TweenService is a nightmare

Hey guys, so I use tweens quite frequently when coding my projects and doing commissions. I love the smooth transitions it provides but I find it makes my code a mess. Tweens take up so many lines and make my code look unreadable.

Furthermore, the “reverse” option when creating a tween is extremely annoying. As far as I’m aware, you can’t delay the reverse. In my scripts, I often want to set reverse to true, but it reverses immediately after the tween finishes. I often want it to wait a set amount of time before reversing. So in order to work around this issue, I have to make a whole TweenInfo, Goal, and Tween just for reversing it.

I will provide a sample of some code below that will allow you to see what I’m talking about. Does anyone know of ways to make tweens look more readable and not jumble up the code? And is there another way to go about reversing a tween that doesn’t involve creating a reverse tween?

A snippet of my code from an ATM system (LocalScript; UI Tweening)

local function onErrorOccurance(message)
	confirmDebounce = true
	local errorInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
	local targetPosition = UDim2.new(0.316, 0,0.216, 0)
	local errorTween = TweenService:Create(errorGui.Background, errorInfo, {Position = targetPosition})

	local errorInfoReverse = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
	local targetPositionReverse = UDim2.new(-0.4, 0,0.216, 0)
	local errorTweenReverse = TweenService:Create(errorGui.Background, errorInfoReverse, {Position = targetPositionReverse})

	local confirmButtonInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
	local confirmButtonTween = TweenService:Create(confirm, confirmButtonInfo, {BackgroundColor3 = errorColor})
	
	local confirmButtonInfoReverse = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
	local confirmButtonTweenReverse = TweenService:Create(confirm, confirmButtonInfoReverse, {BackgroundColor3 = Color3.fromRGB(30, 255, 120)})

	if errorGui.Background.Position == targetPosition then
		local repeatInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear, Enum.EasingDirection.Out,0,true)
		local errorColorTween = TweenService:Create(errorMessage, repeatInfo, {TextColor3 = errorColor})
		errorColorTween:Play()
	end

	errorMessage.Text = message
	errorGui.Enabled = true
	errorTween:Play()
	confirmButtonTween:Play()
	
	local waitTime = 6
	for i = 6, 0, -0.1 do
		confirm.Text = math.round(waitTime*10)/10
		waitTime -= 0.1
		task.wait(0.1)
	end

	errorTweenReverse:Play()
	errorGui.Enabled = false
	errorMessage.Text = ""
	confirmButtonTweenReverse:Play()
	confirm.Text = "Confirm"
	confirmDebounce = false
end

Do you see what I mean? It’s a complete mess. It’s so time consuming and makes editing the code so inconvenient and inefficient. If anyone has any ideas/advice, I’d love to hear it!

Side note: I also never know how to set up the Goals = {} part of the tween. It seems as though every instance needs its own unique way of writing it.

Examples:
BackgroundColor3 = Color3.fromRGB(0,0,0)
Color3 = Color3.new(0,0,0)
Brickcolor = Brickcolor.new(“really red”)
Position = …
CFrame = …
Size = …
etc.

I’ve used tweens countless times but can’t seem to memorize each one so I often spend a ton of time sifting through various DevForum posts trying to figure out which one to use for my specific case. Like why are there 3 different methods for changing the color? It’s just confusing to me.

2 Likes

I would say the opposite because imagine not being able to use TweenService. You would need to write a loop for every single property you want to tween, making it way more cluttered than it is now. There’s also no feasible way to shorten it, because your script needs the info to know how the tween should work.

Yeah, your code needs to know what properties you want to change, it can’t just guess.

You can also reuse TweenInfos, so I’m not sure why you aren’t when you can.

Code:

local tweenInfo1 = TweenInfo.new(0.3, Enum.EasingStyle.Linear)

local function onErrorOccurance(message)
	confirmDebounce = true
		
	local targetPosition = UDim2.fromScale(0.316, 0.216)
	local targetPositionReverse = UDim2.fromScale(-0.4, 0.216)

	local errorTween = TweenService:Create(errorGui.Background, tweenInfo1, {Position = targetPosition})
	local errorTweenReverse = TweenService:Create(errorGui.Background, tweenInfo1, {Position = targetPositionReverse})

	local confirmButtonTween = TweenService:Create(confirm, tweenInfo1, {BackgroundColor3 = errorColor})
	local confirmButtonTweenReverse = TweenService:Create(confirm, tweenInfo1, {BackgroundColor3 = Color3.fromRGB(30, 255, 120)})

	if errorGui.Background.Position == targetPosition then
		local repeatInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, true)
		local errorColorTween = TweenService:Create(errorMessage, repeatInfo, {TextColor3 = errorColor})
		errorColorTween:Play()
	end

	errorMessage.Text = message
	errorGui.Enabled = true
	
	errorTween:Play()
	confirmButtonTween:Play()

	local waitTime = 6
	
	for i = 6, 0, -0.1 do
		confirm.Text = math.round(waitTime*10)/10
		waitTime -= 0.1
		task.wait(0.1)
	end

	errorTweenReverse:Play()
	errorGui.Enabled = false
	errorMessage.Text = ""
	
	confirmButtonTweenReverse:Play()
	confirm.Text = "Confirm"
	confirmDebounce = false
end

In order to know how to use TweenService effectively, you should read the documentation.

5 Likes

Couple of things:

  • TweenInfo variables can be used throughout multiple tweens, not just one.
  • Goals {} are just properties of the part you want to have in the end. Put them in the parameters instead of creating a variable and putting that in.
  • You can create repeated tweens at the beginning and play them when needed instead of making and playing them right before.
3 Likes

The simpelest and fastest way.

game:GetService('TweenService'):Create(OBJECT, TweenInfo.new(NUMBER), {PROPERTY = VALUE}):Play()

This line of code creates a tween animation for a specified object, setting certain properties to specific values over a given duration and then plays the animation.

You can also add more tween info ofc but i usually dont but that depends on your liking.

1 Like

And is there another way to go about reversing a tween that doesn’t involve creating a reverse tween?

No. If you need to add a delay before playing a reverse, you have no other choice but to create a new tween.

It’s actually not even that hard too: create your starting tween, wait for it to be completed, then play another tween that is the reverse.

The trick here though, is to play the reverse tween with the delay, which can be set in its TweenInfo, with the delay parameter.

Just remember to destroy the tweens though, or you’ll get a memory leak pretty fast.

1 Like

Is that done like this?

myTween:Destroy()

There is no need to destroy tweens (most of the time and this time is not the case.) Simply just create the tweens at the beginning of the script and play them whenever needed. There’s no “Start” of the tween, it starts at its current properties.

1 Like

Yes.

And @Laser_Gun5540 that’s not entirely true.
If you have an event connection like tween.Completed, (which is more commonly used than you think), you should be actively destroying tweens when you are done with them.

The trick here though, is to play the reverse tween with the delay, which can be set in its TweenInfo, with the delay parameter.

This is the case in point here though - you have to actively wait for the tween to complete first before you can play a DELAYED reverse version of it.

And before you ask, yes, I know you can just outright reverse the original tween by just setting TweenInfo’s reverses to true, but that’s not what OP is asking for.

As far as I’m aware, you can’t delay the reverse.

Details, amigo.

1 Like

Tweens should be destroyed after usage, but creating them in the very beginning of the script can be beneficial because you don’t have to wait for .Completed and destroy it, because in his case, onErrorOccurance() might be called more than once. If so, creating and destroying a tween every time can be easily made so much more organized by setting up the tween in the first few lines of the script, and just calling :Play() on it whenever you need to. Set up a reverse tween if you need to as well. Makes the code much more organized, which was the OP’s problem.

Code would look like:

Create tweens

(In the function)
Play one
Wait for completion
Play reverse.

This can be called multiple times and uses the same tween. recycling!!! :recycle:

1 Like

You are right, but that’s not the question I was answering, not sure if you noticed that.

You’ve also already mentioned this in another earlier reply of yours, which I totally agree with.

To be clear, I’m not saying that you shouldn’t just do tween:Create():Play() - that’s exactly how you should play a one-time tween most efficiently.

My point was that if you want to reverse a tween with a delay, you have to create and play a new tween (the :Create():Play() method should be used here) after the original tween has completed - and to do that, you’ll need to wait for .Completed to fire.
Unless you plan on reusing that original tween again, you should be destroying it.

In practice it’d look something like this:

-- pseudocode
local startTween = TS:Create()
startTween.Completed:Connect(function()
    TS:Create():Play() 
    -- this will be the reverse tween, with a defined delay
end)

startTween:Play() -- play when required

But what if startTween is only played once and never again? How do we get this to work still?

-- pseudocode
local startTween = TS:Create()
startTween:Play()
startTween.Completed:Wait() -- use Connect instead if you need it asynchronously
TS:Create():Play() -- this will be the reverse tween, with a defined delay

startTween:Destroy()
startTween = nil

-- notice how i'm destroying the startTween here.
-- i'm done with startTween here, hence i destroy it.
-- if i didn't do it, i'd get a memory leak.
1 Like

You don’t have to destroy tweens. They’re Instances and therefor will be garbage collected when in a closure aslong as no reference is held to it outside of the closure, I believe.

1 Like

You could define your info at the top of the script or in a module script for a cleaner structure, ig.

local tweenInfos = {
	
	tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear),
	repeatInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear, Enum.EasingDirection.Out,0,true)
	
}

local colors = {
	
	confirmColor = Color3.fromRGB(30, 255, 120),
	errorColor = --fill error color
	
}

local positions = {
	
	targetPosition = UDim2.new(0.316, 0,0.216, 0),
	targetPositionReverse = UDim2.new(-0.4, 0,0.216, 0)
	
}

Then tween in your function:

local errorTween = TweenService:Create(errorGui.Background, tweenInfos["tweenInfo"], {Position = positions["targetPosition"]})
errorTween:Play()
1 Like

I don’t recommend this at all. That’s questionable practice - and it doesn’t just apply to tweens.

Any instance will not get GC’ed if it has event connections (as is pretty common with tweens especially), even if it’s wrapped in a closure.

You’re basically just depending on blind luck to clear up your instances - tolerable for small projects, unacceptable when your codebase gets big.

The replies and examples provided are good enough suggestions - I don’t think any further elaboration on the thread is required.

2 Likes

“If you do bad practice in general, this is a bad practice” is what I got from that.

If you have event connections (the OP does not), you obviously disconnect them when needed, and it GC’s the instance since no reference is held anymore. Calling it blind luck is crazy. Holding a connection to a tween is uncommon since popular events such as .Completed only fires once and therefor more suited for a :Once() or :Wait() connection.

I would appreciate your insight if I have misunderstood anything :slight_smile:

1 Like

If you have event connections (the OP does not)

He will have to create one if he intends on playing a reverse delayed tween - how else is he going to catch when the original tween has completed playing?

you obviously disconnect them when needed, and it GC’s the instance since no reference is held anymore.
Calling it blind luck is crazy.

I meant what I said in the broad sense of things.

Abusing closures in your code is a recipe for a debugging disaster.
Humans are forgetful creatures (programmers especially), and you will forget to “tell your GC to clean up this instance when you’re done with it”.

Your first instinct to clean up an instance shouldn’t be to “tell your GC to clean up this instance” by wrapping it up in a closure or to just disconnect its events - it should be to outright “destroy it”.
This is literally what Roblox themselves recommend, and it’s for good reason.
Yes, you will also forget to call :Destroy() on some of your instances, but at least the fault isn’t drowned out by a thousand closures.

We should stop here though, this is getting offtopic.

1 Like

Define a function that takes relevant parameters and returns a configured tween. This way, you can reuse the function throughout your code.

local function createTween(target, property, goal, duration, easingStyle)
    local tweenInfo = TweenInfo.new(duration, easingStyle or Enum.EasingStyle.Linear)
    local tween = TweenService:Create(target, tweenInfo, {[property] = goal})
    return tween
end

Then, you can use this function to create tweens for different properties:

local errorTween = createTween(errorGui.Background, "Position", targetPosition, 0.3)
local confirmButtonTween = createTween(confirm, "BackgroundColor3", errorColor, 0.3)

Group related tweens into tables to make your code cleaner:

local errorTweens = {
    tween = createTween(errorGui.Background, "Position", targetPosition, 0.3),
    reverseTween = createTween(errorGui.Background, "Position", targetPositionReverse, 0.3),
}

local confirmButtonTweens = {
    tween = createTween(confirm, "BackgroundColor3", errorColor, 0.3),
    reverseTween = createTween(confirm, "BackgroundColor3", Color3.fromRGB(30, 255, 120), 0.3),
}

errorTweens.tween:Play()
confirmButtonTweens.tween:Play()

Instead of specifying the property as a string, use an Enum for better clarity.

local EnumProperty = Enum.TextXAlignment -- Choose the correct Enum based on your use case

local errorTweens = {
    tween = createTween(errorGui.Background, EnumProperty, targetPosition, 0.3),
    reverseTween = createTween(errorGui.Background, EnumProperty, targetPositionReverse, 0.3),
}

local confirmButtonTweens = {
    tween = createTween(confirm, EnumProperty, errorColor, 0.3),
    reverseTween = createTween(confirm, EnumProperty, Color3.fromRGB(30, 255, 120), 0.3),
}

I re-did the whole script anyways to save you time but the above were tips.

Script:

local TweenService = game:GetService("TweenService")

local function createTween(target, property, goal, duration, easingStyle)
    local tweenInfo = TweenInfo.new(duration, easingStyle or Enum.EasingStyle.Linear)
    local tween = TweenService:Create(target, tweenInfo, {[property] = goal})
    return tween
end

local function onErrorOccurance(message)
    local confirmDebounce = true

    local targetPosition = UDim2.new(0.316, 0, 0.216, 0)
    local targetPositionReverse = UDim2.new(-0.4, 0, 0.216, 0)
    local errorColor = Color3.fromRGB(255, 0, 0)

    local errorTweens = {
        tween = createTween(errorGui.Background, "Position", targetPosition, 0.3),
        reverseTween = createTween(errorGui.Background, "Position", targetPositionReverse, 0.3),
    }

    local confirmButtonTweens = {
        tween = createTween(confirm, "BackgroundColor3", errorColor, 0.3),
        reverseTween = createTween(confirm, "BackgroundColor3", Color3.fromRGB(30, 255, 120), 0.3),
    }

    local repeatInfo = TweenInfo.new(0.3, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, true)
    local errorColorTween = createTween(errorMessage, "TextColor3", errorColor, repeatInfo)

    errorMessage.Text = message
    errorGui.Enabled = true

    errorTweens.tween:Play()
    confirmButtonTweens.tween:Play()
    errorColorTween:Play()

    local waitTime = 6
    for i = 6, 0, -0.1 do
        confirm.Text = string.format("%.1f", waitTime)
        waitTime = waitTime - 0.1
        wait(0.1)
    end

    wait(2) -- Add a delay of 2 seconds before reversing
    errorTweens.reverseTween:Play()

    errorGui.Enabled = false
    errorMessage.Text = ""
    confirmButtonTweens.reverseTween:Play()
    confirm.Text = "Confirm"
    confirmDebounce = false
end

onErrorOccurance("An error occurred!")
1 Like

There is many way to reduce lines of code and make it more readable.


1) Reduce number of tween infos

TweenInfos is just a settings preset that any tween can use, so if multiple tweens need to have the same settings, you can use only one tweenInfo for all of them.

local TweenService = game:GetService("TweenService")
local TweenInfos = TweenInfo.new(0.3, Enum.EasingStyle.Linear)

local Tween1 = TweenService:Create(Object, TweenInfos, {Position = UDim2.new()})
local Tween2 = TweenService:Create(Object, TweenInfos, {Position = UDim2.new()})

2) Tween Goal table

You can use a Table to store all your differents tween goals.
There is 3 uses of goal table, it allow you to play multiple goals simultaneously, store all differents goals into this same table, editing current goals and adding new goal at any time.

Play multiple goals simultaneously

local TweenService = game:GetService("TweenService")
local TweenInfos = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
local TweenGoals = {
	Position = UDim2.new(),
	Color = Color3.new(),
}

local Tween = TweenService:Create(Object, TweenInfos, TweenGoals)

Store all differents goals into this same table

local TweenService = game:GetService("TweenService")
local TweenInfos = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
local TweenGoals = {
	MovePosition = {Position = UDim2.new()},
	MovePositionReverse = {Position = UDim2.new()},
}

local Tween1 = TweenService:Create(Object, TweenInfos, TweenGoals.MovePosition)
local Tween2 = TweenService:Create(Object, TweenInfos, TweenGoals.MovePositionReverse)
-- OR
local Tween1 = TweenService:Create(Object, TweenInfos, TweenGoals[1])
local Tween2 = TweenService:Create(Object, TweenInfos, TweenGoals[2])

Editing current goals and adding new goal at any time

local TweenService = game:GetService("TweenService")
local TweenInfos = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
local TweenGoals = {}

TweenGoals.Position = UDim2.new() --First Position
local Tween = TweenService:Create(Object, TweenInfos, TweenGoals.Position)
Tween:Play()

TweenGoals.Position = UDim2.new() --New Position
local Tween = TweenService:Create(Object, TweenInfos, TweenGoals.Position)
Tween:Play()

3) One Line Tween

In case you only have one tween to do in your script, then you can do a one line tween

local TweenService = game:GetService("TweenService")

TweenService:Create(Object, TweenInfo.new(0.3, Enum.EasingStyle.Linear), {Position = UDim2.new()}):Play()

So lets apply all these tips to your code:

local TweenService = game:GetService("TweenService")
local TweenInfos = TweenInfo.new(0.3, Enum.EasingStyle.Linear)
local Goals = {
	targetPosition = {Position = UDim2.new(0.316, 0,0.216, 0)},
	targetPositionReverse = {Position = UDim2.new(-0.4, 0,0.216, 0)},
	errorColor = {BackgroundColor3 = Color3.fromRGB(255, 70, 70)},
	sucessColor = {BackgroundColor3 = Color3.fromRGB(30, 255, 120)}
}

local errorTween = TweenService:Create(errorGui.Background, TweenInfos, Goals.targetPosition)
local errorTweenReverse = TweenService:Create(errorGui.Background, TweenInfos, Goals.targetPositionReverse)
local confirmButtonTween = TweenService:Create(confirm, TweenInfos, Goals.errorColor)
local confirmButtonTweenReverse = TweenService:Create(confirm, TweenInfos, Goals.sucessColor)
2 Likes

I’m sure this has already all been said but this is my opinion:

  • Don’t make variables for every TweenInfo and goal, you can just write it on one line
  • If you do make a variable, reuse them when you can
  • The reason every property “needs its own unique way of writing it” is because they accept different types
1 Like

Thank you everyone for your responses, it has provided me a ton of insight. Several of your replies are really good solutions to my issue, I wish I could make more than one reply the solution :frowning:

I did learn a lot from these replies, so thank you all so much! :slight_smile:

3 Likes

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