Tween PlayAsync function

As a developer it is annoying to set up code that waits for a tween to finish.

Very common pattern: Tween value, then do something. For example, fade out image and then destroy it.

Right now you have to do a bunch of stuff for this.

tween = TS:Create(
   obj,
   TweenInfo.new(2),
   prop
)

tween.Completed:Connect(function()
   obj:Destroy()
end)

tween:Play()

A tween is a chronological operation. If you want to read this code chronologically then your eyes have to jump back and forth. This is bad code design.

To avoid bad code design, an antipattern emerges:

tween = TS:Create(
   obj,
   TweenInfo.new(2),
   prop
)
tween:Play()
task.wait(2)
obj:Destroy()

Observe the magic number, repeated in multiple places. The code reads in chronological order now but at what cost? We can assign the tweeninfo elsewhere and index its length, but then we’re back to jumping around the document as we read it.

The elegant solution to this is PlayAsync.

TS:Create(
   obj,
   TweenInfo.new(2),
   prop
):PlayAsync()

obj:Destroy()

The code reads in chronological order, there are no duplicate constants, everything is right in the world.

6 Likes

I think you can just do

tween.Completed:Wait()

then anything you put afterwards will happen after the tween is finished.

16 Likes

In full that would be

tween = TS:Create(
   obj,
   TweenInfo.new(2),
   prop
)


tween:Play()

tween.Completed:Wait()

obj:Destroy()

5 Likes

Yeah this is not needed. Doing 3 lines of code and modularizing it is still a better option

7 Likes

This isn’t documented behavior

what do you mean? what i said was right, ive used it before.

6 Likes

Tween.Completed:Wait()


It is documented.

         -- Tween (inherit from TweenBase) | Completed (RBXScriptSignal) | Wait (Variant)
local Result: Enum.PlaybackState = Tween.Completed:Wait()
11 Likes

Here’s a quick module I made just for you

local TweenService = game:GetService("TweenService")

export type WrappedTween = Tween & {
	PlayAsync: (self: Tween) -> ()
}

export type TweenService2 = typeof(TweenService) & {
	Create: (self: TweenService2, Instance: Instance, TweenInfo: TweenInfo, Properties: {[string]: any}) -> WrappedTween
}

local TweenService2 = setmetatable({}, {
	__index = function(_, Key: string)
		local Value = TweenService[Key]
		if typeof(Value) == "function" then
			return function(_, ...) return Value(TweenService, ...) end
		end
		return Value
	end,
}) :: TweenService2

function TweenService2:Create(Instance: Instance, TweenInfo: TweenInfo, Properties: {[string]: any}): WrappedTween
	local Tween = TweenService:Create(Instance, TweenInfo, Properties)
	
	function Tween:PlayAsync()
		self:Play()
		self.Completed:Wait()
		return self
	end
	
	return Tween :: WrappedTween
end

return TweenService2
2 Likes

It would be beneficial if you do your research on all Roblox features before asking for one yourself. There’s a 99% chance that it already exists and you don’t know it.

5 Likes

There is nothing here about the proposed behavior. You are proposing that:

t:Play()

t.Completed:Wait()

Will always resume. What if the tween has a time of zero? Does it resume? The behavior is not documented in this case. That’s what I’m talking about.

1 Like

It’s also really easy to test yourself and decide how you want it to behave
I suggested a simple wrapper module; it already gives you :PlayAsync(), and if you need to handle special cases (like duration = 0) you can add that logic yourself

That’s the point of making a wrapper: you can edit the behavior yourself

Tween.Completed will always fire after a Tween finishes, even with a Time of 0.

2 Likes

It will always fire, and it is documented, i would encourage you to try it out.

Tween.Completed:Wait()
1 Like

Where is this behavior documented? It’s not on the wiki page.

Completed is an event, meaning its documented in RBXScriptSignal.

2 Likes

What they’re saying is that there may be some conflict about timing between

tween:Play()

with a tween time of 0 seconds, and

tween.Completed:Wait()

which would result in an infinite yield.
Indeed the documentation of the Completed event does not specify how it behaves.

So far I have never had any concern about that, and the precised workflow always worked for me. I guess tweeners use a scheduler that will fire Completed, even after an hypothetical delay. The reason why it works may also be completely unrelated of course.

Ooooh I see, my bad. I wasn’t following the full conversation. That is frustrating that it isn’t documented though.

But considering Lua runs on a single thread, and :Play() isn’t asynchronous, and :Wait() seems to consistently fire. I would say it’s safe to bet that Roblox only fires the completed event once the elapsed time is larger than the duration, not equal too it (since its impossible in any duration that’s not 0, so checking is a waste of resources.)

However if I’m wrong and it isn’t consistent then I support this feature request.

1 Like

The thing is. Why would you need a 0 second tween?

local tweenService = game:GetService("TweenService")
local part = script.Parent
-- Why do this
local uselessTween = tweenService:Create(part, TweenInfo.new(0), {Color = Color3.new(1, 0, 0)})
uselessTween:Play()
-- When you could just do this as it would look exactly the same
part.Color = Color3.new(1, 0, 0)
4 Likes

And here I couldn’t agree more. I guess it would be more of a safety measure in case some formula was used, outputting 0.
I would not create a tween instead of changing a property at all !

2 Likes

If you’re in a situation where you know the tween can have a time of zero, why are you not already creating logic that never creates & plays the tween to begin with? Your example makes no sense when it’s a case that should be solved by the developer, not Roblox.

4 Likes