TweenService Plus+ [Update V1.1]

I love this module, thanks! :grinning:

One thing im messing around with right now is that my tweens seem to only work with parts that are streamed in at the time the server code executes. Im still trying a few more things, but if anyone has solved this issue - please let me know.

Hi,

I have the tweening working for moving a part forward but want to bounce the part at the same time, how is this done? can you run two tweens at the same time?

Regards

Hey so I’m trying to tween a model and when I run it it doesn’t even move and no errors om aware of either. Thanks!

TweenModule:Construct(Elevator.Doors.DoorR.DoorR.PrimaryPart, TweenInfo.new(DoorOpenTime, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut), {CFrame = Elevator.Doors.DoorR.GoTo.CFrame}):Play()

TweenService Plus is an amazing resource, however, I believe I have found a bug, and it is quite concerning.

Bug: TweenService Plus affects Proximity Prompts.

I have an object that is tweened in a constant loop. This object contains a Proximity Prompt that prints a string when triggered. The object is tweened using TweenService Plus from point A → point B → point C → point A. Each tween is approximately 35 seconds long. During the first roughly 17 seconds of each tween, the Proximity Prompt is triggered, and the string is printed in the output. However, in the second half of the tween, the Proximity Prompt does not print anything in the output.

I tested many different possibilities to determine the cause of this issue, however, I settled on the conclusion that this bug was due to the object being tweened. This is because, when I removed all the tweens, the Proximity Prompt worked like a charm without any issues. Although I am no professional, I suspect that the TweenService Plus module is affecting UserInputService or TweenService which Proximity Prompts use.

I hope you can look into this issue or provide some ideas as to what the cause may be.

Thank you in advance.

Please tell me it works with the Streaming_Enabled property set to true, I also made a api that does exactly the same, just that it supports roblox’s content streaming api, it works fine except that it doesnt have that latency and events feature, if your api works with streaming enabled then I’d be happy to use it!

Just an update: here is what I have found.

Let’s say you have a proximity prompt in a part that is tweened with this TweenService Plus module. The “Triggered” event will stop working occasionally, however, other events such as “TriggerEnded”, “HoldDownBegan”, and “HoldDownEnded” will function without any issues.

Hello!

I have run into this issue in the past and I have answers. This is indeed not a TweenService Plus issue, but rather a security feature from roblox. Let me explain:

  • The server has the part in a certain place, say Vector3.new(10,0,0)
  • The client has the part in another place, say Vector3.new(-10,0,0)
  • Roblox makes sure that the ProximityPrompt is near the player on the Server, therefore, if on the client your proximity prompt is near you, but on the server that part is not in the same spot (and too far), the prompt will not fire despite you triggering the prompt on the client.

To confirm this, and to clarify this issue to the OP, can you please check these points:

  • How does the part move on the server? Does it move at all? Does it stutter/teleport?
  • Is your tween super big? As in does it move a considerably large distance in 1 go? Is it fast? This may cause a decline from the server if the prompt if the client tweens faster than the server.
1 Like

Thank you very much for clarifying the issue I was facing.

The tween is about 40 seconds long and travel approximately 70 studs within this time frame. Since I am using TweenService Plus to tween, the object I am tweening will appear on the server as if it is teleporting.

1 Like
--[[

TweenService+ V1.2 (UNOFFICIAL)
@rek_kie on 8/9/20

Please read the devforum post here!
https://devforum.roblox.com/t/tweenservice-plus/716025

Update Log [V1.2 - UNOFFICIAL]:
- Minor Improvement

Update log [V1.1]: 

:Play() now has a mainObject parameter

 -- You are now able to set the object to calculate distance from if you are using :Play() with a range. This is useful if
    you are trying to tween something that isn't a BasePart, but you still need to calculate distance from something to tween 
    the object.

Setup instructions: 

	Put this module into REPLICATEDSTORAGE, nowhere else.
	To set up, all you have to do is require this module somewhere on a local script, and require it on a server script to be 
	ready for use on the server side.


Documentation:

=== FUNCTIONS ===

tweenServicePlus:Construct(instance Object, TweenInfo info, table Properties, number timeThreshold, bool debugMode, bool clientSync)

  Returns: tweenObject
	
- The "equivalent" to TweenService:Create(). The time threshold is the latency threshold if clientSync (latency compensation) 
  is enabled. The object is the object to tween, the TweenInfo is the info to use, the properties and are properties you 
  want to tween to.

 - debugMode defaults to false, and clientSync defaults to true.

tweenObject:Play(array/instance clients, number Range, string rootPart, BasePart mainObject)

- If clients are not specified, then the tween will play for all clients (the default setting). If you want to specify clients, either 
  pass in an ARRAY of clients, or pass in one individual client. These should be PLAYER INSTANCES.

- If a range (a NUMBER) is specified, then the tween will only play for clients (you can specify them as stated above) within the range 
  (in studs) of the object being tweened. EXTREMELY useful for optimization and reducing client load. You wouldn't need to tween 
  something for a client if they're somewhere like 1000 studs away from the object. 

- rootPart: Only works when a range is specified. Defaults to HumanoidRootPart. If you specify this (has to be a string) 
  then the distance calculations will be from the distance from the tweened object to the specified rootPart. This uses a 
  recursive :FindFirstChild(), so make sure that your rootPart has a unique name so it isn't mistaken for something else in the
  player. 

-- mainObject: The object to calculate distance from if you are using :Play() with a range. Defaults to the original object you're trying to tween.
   This is useful if you are trying to tween something that isn't a BasePart, but you still need to calculate distance from something 
   to tween the object. (Ex. You want to tween a pointlight and use a range of 50 studs, but a pointlight doesn't have a workspace 
   position. So, you can set the mainObject to a BasePart and then the range would be 50 studs within that part.)

tweenObject:Cancel(array/instance clients)

- This behaves like the normal TweenService. Documentation is on developer.roblox.com

- Cancels the tween. If clients are specified (either an ARRAY of player instances or one player instance), then the tween will only
  cancel for the specified clients. Otherwise, it cancels for all clients. 

- Always stays in sync with the server. If you cancel a tween and then call :Play() again, it will still have TweenService's normal
  behavior and take the initial tween time to finish.

Tip: Cancelling for specific clients is not reccommended, though. It only has a few use cases and could lead to things getting 
out of sync between your clients and the server.

tweenObject:Pause(array/instance clients)

- This also behaves like the normal TweenService. Documentation is on developer.roblox.com

- Pauses the tween. If clients are specified (either an ARRAY of player instances or one player instance), then the tween will only
  pause for the specified clients. Otherwise, it pauses for all clients. If a tween is paused while it wa

- Also always stays in sync with the server. If you pause a tween and then call :Play() again, it will still have TweenService's normal
  behavior and resumes where the tween had left off when it was paused.

=== EVENTS ===

- tweenObject.Cancelled -- Fires when a tween is cancelled.
- tweenObject.Resumed -- Fires when a tween is played after being paused.
- tweenObject.Paused -- Fires when a tween is paused.
- tweenObject.Completed -- Fires when a tween is completed.

]]--


local tweenService = {}

local clock = require(script.SyncedTime)

local rs = game:GetService("RunService")
local ts = game:GetService("TweenService")
local http = game:GetService("HttpService")

local wrap = coroutine.wrap

local tEvent = script:WaitForChild("TweenCommunication")

if rs:IsServer() then 	
	if not clock:IsSynced() then -- make sure our clock is synced
		repeat 
			clock:Sync()
			task.wait(.5)
		until clock:IsSynced()
	end
end

local function infoToTable(tInfo)
	local info = {}
	info["Time"] = tInfo.Time or 1 
	info["EasingStyle"] = tInfo.EasingStyle or Enum.EasingStyle.Quad
	info["EasingDirection"] = tInfo.EasingDirection or Enum.EasingDirection.Out
	info["RepeatCount"] = tInfo.RepeatCount or 0
	info["Reverses"] = tInfo.Reverses or false
	info["DelayTime"] = tInfo.DelayTime or 0
	return info
end

local function assign(object, properties, debugMode)
	if not object or not properties then return end
	
	for property, value in pairs(properties) do
		object[property] = value
		
		if debugMode then 
			print("Set "..object.Name.. "'s "..property.." to ".. tostring(value)..".")
		end
	end
end


function tweenService:Construct(obj, info, properties, timeThreshold, debugMode, clientSync)
	
	if not obj then warn("This object doesn't exist.") return end
	if not info then warn("Please provide some TweenInfo!") return end
	if not properties then warn("Please provide some properties to tween to!") return end 
	
	if timeThreshold and not type(timeThreshold) == "number" then warn("Latency threshold must be a number!") return end
	if debugMode and not type(debugMode) == "boolean" then warn("The debugMode parameter must be true or false!") return end 
	if clientSync and not type(clientSync) == "boolean" then warn("The parameter clientSync must be true or false!") return end
	
	local startProperties 
	
	if info.Reverses then 
		for property, value in pairs(properties) do
			startProperties[property] = obj[property]
		end
	end
	
	debugMode = debugMode or false
	clientSync = clientSync or true 
	
	local events = {
		["Cancelled"] = true, 
		["Completed"] = true,
		["Paused"] = true, 
		["Resumed"] = true
	}
	
	local tObject = {
		["PlaybackState"] = Enum.PlaybackState.Begin,
		["TweenId"] = http:GenerateGUID(false), -- so that we can identify each tween.
		["IsPaused"] = false, 
		["IsCancelled"] = false, 
		["LastPlay"] = clock:GetTime(), 
		["TimeElapsed"] = 0
	}
	
	local function changeState(state)
		tObject.PlaybackState = state
		
		if debugMode then 
			print("Playback state changed. New playback state:", tostring(state))
		end
	end

	for name, event in pairs(events) do  -- Setting up the events to connect to 
		events[name] = Instance.new("BindableEvent")
		tObject[name] = events[name].Event
	end
	
	local tweenWait = function(n)
		n = n or 1/60
		local now = tick()
		repeat 
			rs.Heartbeat:Wait()
			if tObject.IsPaused == true or tObject.IsCancelled == true then 
				if debugMode then 
					print("Tween cancelled/paused server-side.")
				end
				return true
			end 
		until tick() - now >= n
	end
	
	local function completionWait(t)
		local func = wrap(function()
			
			if tObject.IsPaused then 
				t = t - tObject.TimeElapsed
				
				if debugMode then 
					print("Tween is resuming from a pause. Length:", t)
				end
				
				events.Resumed:Fire()
				tObject.IsPaused = false
			end
			
			if tObject.IsCancelled then 
				tObject.IsCancelled = false
			end
			
			if info.DelayTime > 0 then
				changeState(Enum.PlaybackState.Delayed)
				task.wait(info.DelayTime)
			end
				
			tObject.LastPlay = clock:GetTime()
			
			changeState(Enum.PlaybackState.Playing)
			local cancelled = tweenWait(t)
			
			print("Cancelled:", cancelled)
			
			if not cancelled then 
				assign(obj, properties, debugMode)
				
				if not info.Reverses then 
					changeState(Enum.PlaybackState.Completed)
					events.Completed:Fire()
				elseif info.Reverses then 
					task.wait(t)

					if not tObject.IsPaused then 
						assign(obj, startProperties, debugMode)
						changeState(Enum.PlaybackState.Completed)
						events.Completed:Fire()	
					end
				end		
			end
			
		end)
		
		func()
	end
	
	 
	
	function tObject:Play(clients, range, rootPart, mainObject)
		
		rootPart = rootPart or "HumanoidRootPart" 
		
		if mainObject then 
			if not mainObject:IsA("BasePart") or not mainObject:IsDescendantOf(workspace) then warn("The mainObject must be a BasePart in the workspace.") return end
		end	
			
		mainObject = mainObject or obj
		
		if mainObject ~= obj and debugMode then 
			print("Set the main object to", mainObject.Name)
		end
		
		if clients then 
			
			clients = (type(clients) == "table") and clients or {clients} -- If you only provide a single player, it turns it into a table for you
			
			if range then 
				for _, player in ipairs(clients) do
					
					if player and player:IsA("Player") and player.Character then 
						
						local runTween = wrap(function() 
							local root = player.Character:FindFirstChild(rootPart, true)

							if root then
								
								if debugMode then 
									print("Found root part of "..player.Name..":", rootPart)
								end
								
								local dist = (mainObject.Position - root.Position).magnitude
								
								if dist <= range then  -- Tween it only for clients in the range
									tEvent:FireClient(player, obj, infoToTable(info), properties, clock:GetTime(), tObject.TweenId, timeThreshold, debugMode, nil, clientSync) -- Tell the client to tween the object and the timestamp of when it was sent
									
									if debugMode then 
										print("Sent tween data to "..player.Name..". Distance from object:", dist)
									end
								end
							end
						end)
						
						runTween()
						
					end
				end
			else	
				
				for _, player in ipairs(clients) do
					
					if player and player:IsA("Player") and player.Character then 
						local runTween = wrap(function()
							tEvent:FireClient(player, obj, infoToTable(info), properties, clock:GetTime(), tObject.TweenId, timeThreshold, debugMode, nil, clientSync)
						end)	
						
						runTween()
					end
					
				end
			
			end
		else
			if range then 
				for _, player in ipairs(game.Players:GetPlayers()) do
					
					if player and player:IsA("Player") and player.Character then 
						
						local runTween = wrap(function() 
							local root = player.Character:FindFirstChild(rootPart, true)

							if root then
								
								if debugMode then 
									print("Found root part of "..player.Name..":", rootPart)
								end
								
								local dist = (mainObject.Position - root.Position).magnitude
								
								if dist <= range then  -- Tween it only for clients in the range
									tEvent:FireClient(player, obj, infoToTable(info), properties, clock:GetTime(), tObject.TweenId, timeThreshold, debugMode, nil, clientSync) -- Tell the client to tween the object and the timestamp of when it was sent
									
									if debugMode then 
										print("Sent tween data to "..player.Name..". Distance from object:", dist)
									end
								end
							end
						end)
						
						runTween()

					end
					
				end
			else
				tEvent:FireAllClients(obj, infoToTable(info), properties, clock:GetTime(), tObject.TweenId, timeThreshold, debugMode, nil, clientSync)
			end
		end
		
		completionWait(info.Time)
		
	end
	
	function tObject:Cancel(clients)
		
		if clients then 
			clients = (type(clients) == "table") and clients or {clients}
			
			for _, client in ipairs(clients) do
				tEvent:FireClient(client, nil, nil, nil, clock:GetTime(), tObject.TweenId, nil, debugMode, "Cancel")
			end	
		else
			tEvent:FireAllClients(nil, nil, nil, clock:GetTime(), tObject.TweenId, nil, debugMode, "Cancel")
		end
		
		events.Cancelled:Fire()
		tObject.IsCancelled = true 
		changeState(Enum.PlaybackState.Cancelled)	
		
	end
	
	function tObject:Pause(clients)
		
		if clients then
			clients = (type(clients) == "table") and clients or {clients}
			
			for _, client in ipairs(clients) do
				tEvent:FireClient(client, nil, nil, nil, clock:GetTime(), tObject.TweenId, nil, debugMode, "Pause")
			end		
		else
			tEvent:FireAllClients(nil, nil, nil, clock:GetTime(), tObject.TweenId, nil, debugMode, "Pause")
		end
		
				
		tObject.TimeElapsed = clock:GetTime() - tObject.LastPlay 
		tObject.LastPlay = clock:GetTime()
		
		events.Paused:Fire()
		tObject.IsPaused = true 
		changeState(Enum.PlaybackState.Paused)
	end
	
	
	return tObject
end

if rs:IsClient() then
	local player = game.Players.LocalPlayer 
	
	local tweens = {}
	
	tEvent.OnClientEvent:Connect(function(obj, info, properties, timestamp, tweenID, threshold, debugMode, modify, sync)
		
		if modify then 
			local tweenToEdit = tweens[tweenID]
			
			if not tweenToEdit then -- if the tween doesn't exist, just return and give a warning
				warn("The tween you tried to modify does not exist.")
				return 
			end
			
			if modify == "Cancel" then
				
				tweenToEdit:Cancel()
				
				if debugMode then 
					local latency = clock:GetTime() - timestamp
					print("Cancelled a tween. Latency:", latency)
				end
				
				return 	
				
			elseif modify == "Pause" then 
				
				tweenToEdit:Pause() 
				
				if debugMode then 
					local latency = clock:GetTime() - timestamp
					print("Paused a tween. Latency:", latency)
				end
				
				return 	
			end
		end
		
		local latency = clock:GetTime() - timestamp
		
		local newtime = sync and info.Time - latency or info.Time    -- If enabled, this syncs the tween up based on latency. 
																	 -- Ex. If the server told a client to do a 10 second tween and it
																	 -- took .7 seconds to get to the client, then the time for the 
																	 -- client's tween would be 10 - .7 = 9.3 seconds. 
																     -- I'm using Quenty's module to get a global timestamp, so
																	 -- that server and client time are synced. This results in a 
																	 -- perfect sync between client and server tween completion.
																	 -- To better see this for yourself, turn on debug mode and 
																	 -- look at the output to see how the server prints exactly
																	 -- when the client tween ends. (If you are in Studio, Make sure you  
																	 -- stay watching only on the client though, because switching to 
																	 -- the server  view pauses the client's game session on Play Solo)
		
		if debugMode then 
			print("Approximate latency for ".. player.Name ..": " ..latency .. " seconds. \n New tween time: ".. newtime .." seconds")
		end
		
		threshold = threshold or 0 -- Defaults to 0. When you set this, this basically means the amount of time 
		  						   -- the new tween time (after calculating latency) needs to be greater than 
								   -- for the tween to play. If the new tween time after calculating latency is less than 
								   -- or equal to than the threshold, the tween will not play.
		
		
		if newtime > threshold and obj and properties and tweenID then -- some checks
			local newInfo = TweenInfo.new(newtime, info.EasingStyle, info.EasingDirection, info.RepeatCount, info.Reverses, info.DelayTime)
			
			local trackTween = wrap(function()
				
				if not tweens[tweenID] then 
					tweens[tweenID] = ts:Create(obj, newInfo, properties)
				end
				
				tweens[tweenID]:Play()	
				tweens[tweenID].Completed:Wait()
				tweens[tweenID] = nil
			end)
			
			trackTween()	
			
			if debugMode then 
				local printProperties = wrap(function()
					print("Currently tweening properties of "..obj.Name..":")
					
					for property, value in pairs(properties) do
						print(property .. " to "..tostring(value))
					end
				end)
				
				printProperties()
			end		
		end
	end)
end

return tweenService

u can replace the BindableEvent with Custom Signal Module (optional)

1 Like

Does anyone know a proper way to reduce the packet size for the remote event “Tween Communication?”

Edit: Just changing the TweenInfo table’s string indexes to list indexes decreased recieved packet data by more than 30%.

image
image

u can replace the remotevents with alternative networking module such like BridgeNet2, Red, Red2, FastNet2, etc. they are open-sourced

Personally recommend using BridgeNet2.

Definitely checking those out later. Looks like a very good potential modification to my game.
Side note: What does your unofficial v1.2 change? I was curious since it looks like much of the same thing.

Hey there. I was wondering if this module can be used across various scripts, such as being able to cancel a concurring tween that is happening in another script? Using variables such as _G is extremely frustrating, so if this is true then it would significantly help the workflow of cross-script tweens.

the tweening isn’t working properly for me. all the tweens are instantaneous from the start to the end goal

what did you do to fix it? im having the same issue

Had the same issue, in the TweenServicePlus module at the very top under “Setup Instructions”, it says to require the module on the client side in a localscript.

So legit just

local TweenService = require(game.ReplicatedStorage.TweenServicePlus)

on any script on the client.

**Also make sure you use his module or you’ll get some errors like I did :expressionless: **

1 Like

Hello i found a bug where module is reporting some weird errors and doesnt work:



Anyone having issues scaling SpecialMeshes?

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TweenServicePlus = require(ReplicatedStorage:WaitForChild("Modules").TweenServicePlus)

local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

local TornadoMesh = script.Parent.Mesh;

local TornadoMeshes = TornadoMesh.Parent.Parent;

local Tornado = TornadoMeshes.Parent;

local TornadoStats = Tornado:WaitForChild("TornadoStats")

local TopSize = TornadoStats:WaitForChild("TopSize")
warn(TopSize.Value)

local tween = TweenServicePlus:Construct(
	TornadoMesh,
	tweenInfo,
	{
		Scale = Vector3.new(TopSize.Value, TopSize.Value, TopSize.Value)
	},
	.4,
	true)

tween:Play()

-- This is a script in the part with the mesh.

It tweens it instantly which is not normal behavior.

In order to substitute this, I have to create a Script with RunContext set to Client put it in the part with the mesh inside of it and write this code:

local TweenService = game:GetService("TweenService")

local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

local TornadoMesh = script.Parent;

local TornadoMeshes = TornadoMesh.Parent;

local Tornado = TornadoMeshes.Parent;

local TornadoStats = Tornado:WaitForChild("TornadoStats")

local TopSize = TornadoStats:WaitForChild("TopSize")

local tween = TweenService:Create(TornadoMesh.Mesh, tweenInfo, {Scale = Vector3.new(TopSize.Value, TopSize.Value, TopSize.Value)})

tween:Play()

I also have another script in the part which destroys it after a period of time and before it destroys it, it plays a tween in which it fades out and destroys the part this piece of code works perfectly fine yet somehow, I cannot change the properties of SpecialMeshes on the other piece of code.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TweenServicePlus = require(ReplicatedStorage:WaitForChild("Modules").TweenServicePlus)

local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

local TornadoMesh = script.Parent;

local Lifetime = 5

task.wait(Lifetime)

local tween = TweenServicePlus:Construct(
	TornadoMesh,
	tweenInfo,
	{
		Transparency = 1
	},
	.4,
	false)

tween:Play()

tween.Completed:Once(function()
	TornadoMesh:Destroy()
end)

EDIT: Even tried changing the time threshold property to “0” it did nothing at all.

The hyperlink that was sent for this post is hilarious lol.