TweenService Plus+ [Update V1.1]

Introducing TweenService+, a module that is focused solely on server and client tween replication! It lets you play tweens on the server, without them actually being on the server. It tells the clients to play that tween, and then sets the properties of the object instantly on the server once the tween time is finished.

“Can’t I just use TweenService V2? What’s the point of this?”

This module has the same purpose as TweenService V2, but is vastly improved and comes with many more features to fully help you control your tweens on the server.

Some features include:

  • Cancelled, Completed, Paused, and Resume events
  • Tween ranges (if a client is within the range, the tween will play.)
  • Client latency compensation + client latency threshold (deeper explanation later)
  • Specific clients you can specify to tween for.
  • PlaybackStates are supported and behave like the regular TweenService.
  • :Pause and :Cancel still behave like the normal TweenService.
  • A debug mode, so that you fully know what is going on with your tweens. This prints out any critical info about the tween to the output (ex. a PlaybackState being changed, client latency, the distances of players found inside of a tween range, the server assignment of properties on an object)

Client Latency Compensation and Threshold

If enabled, this syncs the tween up based on latency.

For example (without a specified threshold):

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 TimeSyncManager 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, at least for me.)

Another visual example:
image

Now, for example, let’s say we set a threshold for latency compensation (defaults to 0). What this means is that if the new tween time after calculating latency isn’t greater than the threshold, then the tween won’t play for that client. If I set a threshold of 2 seconds with latency compensation enabled, let’s think of this:

The server tells the client to do a 5 second tween. The client has very high ping, so it takes a while for the client to receive the server request. The request finally gets to the client, and it took 3.7 seconds. With latency compensation, the client’s new tween time would be 5 - 3.7 = 1.3 seconds. This is less than the 2 second threshold we set, so the tween will not play for that client.

Another visual example:
image

Tween Ranges

This concept behind this is a common practice for people that do some sort of optimizations for their games, including me. Dumbed down, the concept can be worded like this:

If a player is within __ studs of ___, then ____. Otherwise, don’t do anything.

If you specify a tween range, then it will only tell the clients (which you can specify) within the range of the object to play the tween. For example, if you specified a range of 50 studs to a part, then only players within its range would see the tween.

You can also specify the rootPart of the player to calculate the distance from the object from. This defaults to the HumanoidRootPart. For example, if you set the rootPart to the player’s head, then it would do (ObjectToTween.Position - PlayerHead.Position).magnitude to calculate the distance instead of using the player’s HumanoidRootPart.

You are now able to set the object to calculate distance from by using the new mainObject parameter 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.

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.)

Visual:

How do I use it?

API, instructions, and source code are here (Also inside of the module):

Example usage:

Somewhere on a local script:

require(game.ReplicatedStorage.Modules:WaitForChild("TweenServicePlus"))

Server script:

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

wait(15)

local tween = ts:Construct(
	workspace.Part, 
	TweenInfo.new(10, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), 
	{
		Size = workspace.Part.Size * 3, 
		Position = workspace.Part.Position + Vector3.new(0, 10, 0)
	}, 
	.4, 
	true)

tween:Play()

wait(5)

tween:Pause()

wait(5)

tween:Play()

Where do I get it?

You can get this module right here!

Enjoy! If there are any bugs, please tell me! Have a good day.

Happy tweening!

Updates:

Update 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.

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.)

Minor Update: Small bug fix with a conditional involving MainObject. It’s been fixed and updated for a couple of days now, but make sure you have the latest source. Check here:

or get the ROBLOX model again, everything should be updated.

137 Likes

For anyone who wants to view the source code:

--[[

TweenService+ V.10
@rek_kie on 8/9/20

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

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)

- 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. 

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 wait = function(n)
	n = n or 1/30
	local now = tick()
	repeat rs.Heartbeat:Wait() until tick() - now >= n
end

local tEvent 

if rs:IsServer() then 
	tEvent = Instance.new("RemoteEvent")
	tEvent.Name = "TweenCommunication"
	tEvent.Parent = game.ReplicatedStorage
	
	if not clock:IsSynced() then -- make sure our clock is synced
		repeat 
			clock:Sync()
			wait(.5)
		until clock:IsSynced()
	end
	
elseif rs:IsClient() then
	tEvent = game.ReplicatedStorage:WaitForChild("TweenCommunication")
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/30
		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)
				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 
					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)
		
		rootPart = rootPart or "HumanoidRootPart" 
		
		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 = (obj.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 = (obj.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

My only concern is that if you play any EasingSyle that is not linear it will not sync up. For example, if I were to have an exponential animation then this will happen:


As you can see, the 0% and 50% doesn’t line up.

9 Likes

??

There is no way to get the tweens to look exactly the same if they’re starting at different times. This is the whole reason for latency compensation. What you’re asking for is unnecessary/negligible for most use cases. The concept of sync here is for all of the tweens to end at the same time, which for most use cases is really what you need. This method makes sure that when the server sets the properties, the client tweens will all be finished and ending at around the same time. In normal scenarios, latency is usually in fractions of a second, so tweens will not look very different on player screens. If this is really a concern for you, you can also turn client latency compensation off, or set a threshold for the minimum tween time.

The source is also in a pastebin in the post, so I don’t see why you needed to post it here again in a reply.

8 Likes

It’s much easier for mobile users if it’s straight on the topic.


Well, it was just a concern, but there are times where latency can go upwards of 0.5 seconds, enough for you to determine if it’s too far back or not. You can do the math, it’s hard, but it wouldn’t affect performance too hard.

The faster the tween finishes, the worse it would look with latency.

9 Likes

I still don’t even understand your diagram, please explain it.

Edit: I understand now. But that is a different concept of sync. And aren’t the 0% lines supposed to start at the same height? Unless it is something that NEEDS to be at the exact same place at the exact same time on all clients, it doesn’t matter. What matters is that the clients are all ending their tweens at the same time the server sets the properties of the object you’re tweening. This module is for more aesthetic things that just need to be tweened and seen by all clients without putting stress on the server.

Also, I’m pretty sure you can’t. This is virtually impossible if the tweens are starting at different times. This wouldn’t be possible with linear either. If there were math to do this, you would have to edit the actual easing curve of the tween, which ROBLOX can’t do.

So, this was your point, thank you. I wasn’t fully understanding your point.

To answer this, of course. Which is why you can set a latency threshold or turn latency compensation off. Relatively fast tweens wouldn’t need latency compensation.

This doesn’t make any sense and is void. You can literally just tap the link and you can see the source. The difficulty difference is microscopic.

1 Like

The reason they don’t both start at the same point is because they start at different times. Obviously the graph that has a tween of 0.5 seconds would have to be shorter than the graph with 1 second.


It is possible. For easing styles, the percentage until finished is calculated by the term number. For the exponential animation, the formula is simply n ^ 2. So, if you had a total tween time of 10 and a 1.5 second latency, you would use this formula:
((TotalTime - Latency - TimeElapsed)/10) ^ 2
Tat formula would account for delays and have a precise point for all devices.


What I meant here was if you were to play a tween that is 1 second over a distance of 10 studs with a latency of 0.1, there will be a gap of 0.1 studs from the rendered position and the actual position. On the other hand, having the tween that is 10 second over a distance of 10 studs with a latency of 0.1 would have a gap of 0.01 studs from the rendered position and the actual position.

But, sometimes the latency could be quite bigger, like when a player joins the game mid tween.


Anyway, these are just concerns, the module is well made overall.

Thank you!

Okay, so if this is the case, then shouldn’t the graph look like this? Remember that the tweens with latency compensation are all getting to the same end position at the same time. This doesn’t really do anything to the argument though because they will inevitably not be in the same place at the exact same time.

And like I said before, normal latency is pretty short (down to the milliseconds) so the differences will not be so noticeable. If you have a really short tween (1 second and below), you really don’t need any latency compensation. If the latency was so high and the tween was so short, the object’s properties would have already been set on the server so the tween wouldn’t even be visible in the first place since by the time the client had picked up the tween the server had already changed the properties of the object. Meaning, the tween just wouldn’t play, which is normally what you’d want. You can’t control a player’s ping.

Okay. This still really isn’t needed for my use cases though. The difference doing this would make is pretty negligible unless you had a really short tween, and I already explained my thoughts on that above. If you want to add it go ahead. Doing math like this means you’d have to make your own entirely new TweenService.

Edit: Rethought about this over again. It’s impossible.

This formula doesn’t do anything, lol

This doesn’t sync anything. What I said was right. One has .3 latency, another has .8 latency. This formula doesn’t do anything to “sync” them.

If client 1’s tween starts at t, and client 2’s tween started at t + .5, and they both end at x, no matter how you try to edit the easing curve it will be impossible to get the client 2’s object to tween over a position on client 1’s easing curve without overshooting or undershooting the end properties, unless you the start the tween of the object on client 2 with the properties of the object on client 1 at client 1’s current percentage to completion, or just instantly finished client 2’s tween when client 1’s tween finishes, or another hacky method which would just ruin the easing curve. This would result in a visible “chop” in the tween because the tween on client 2 wouldn’t be from the object’s real beginning properties to the server’s end properties, it’d be from client 1’s object properties at the time to the server’s end properties. This would look really ugly. Which is why this module just uses latency to shorten the time of the tween itself, so that the tween is still tweening from its real beginning properties to its real end properties and looks smooth. My point still remains. Your formula doesn’t do anything at all. And what do devices have to even do with this?

What? You can’t compare what you’re seeing on the client to anything else. I’m not sure you’re understanding what this module does. Nothing is moving/happening on the server. There is no “actual position,” which I’m assuming is server side position, during the tween. On the server there is only one instant change, as stated:

6 Likes

This graph also doesn’t depict what this module does correctly. It’s misleading. This is a better representation of what the module does looks like in a graph.

Here, I’m using an In Quad curve. The formula to construct a graph that describes what the module does here would be:

image
(for an InQuad curve)

Where c is the change of value in the tween, t is the current time, d is the duration of the tween, and l is latency. This equation just allows all of the graphs at the equation to always intersect (end the tween) at the same x value, which is time. This is what this module does with client latency compensation.

In any easing equation (for different easing styles) it’s the same thing. Easing functions require 4 things: a start point, change in value, duration, and current time. So, using this concept with any easing equation would mean that

  • start point = server start point + latency
  • duration = server tween info duration - latency
  • change in value = whatever the server is tweening to
  • current time = always a moving time-based value like tick()

Y axis is change, X axis is time

The server tells client 1 and client 2 to run a 3 second tween, and move a part 2 studs up.

The blue tween is client 1, who will have no latency for the sake of this. The green tween is client 2, who has .5 seconds of latency which is why the green graph is translated a bit to the right (it means that it starts after client 1).

As you can see, they finish at the same point at the same time. This is what the module is supposed to do.

Here, I’ll apply 1.5 seconds of latency to client 2.

As you can see, they still end up at the same place at the same time.

No matter how much latency you apply, they will end up at the same point at the same time. This is the “client latency compensation” being done here.

5 Likes

Yes, that was my point. I know the whole purpose of using this module is to account for latency, but, some people might use this for obbies and if theres a noticeable gap between the actual position and the position on their screen, they might see it as unfair:
image
Noticeable gap

3 Likes

I just explained why there’s no “actual position.” Nothing is happening on the server. Every client has their own local position, which is what you’re pointing out, and is what I’ve said multiple times already. This module is for things that do not need to be at the same place at the same time for the whole tween, and only need to end at the same place at the same time, like a door for example, or for tweening atmosphere colors. (Which is some of the things I’m using it for right now). And you shouldn’t use this for obbies where a moving platform would need to be in the same place for everyone at the same time on everyone’s screen.

And I’m going to repeat this again, for 85% of the time the graphs will be almost exactly the same because normal ping is 1-200 ms. Which is .2 seconds and under. I’m just explaining what happens with bigger latency

2 Likes

This does not work for me. I’ve converted a server tween to use your module, and upon playing it to all clients within 500 studs, the tween doesn’t play for the time allotted in the info, but it just suddenly puts the Part into the specified goal position.

2 Likes

Any errors or warnings? Also can I see some code/ an explorer setup? Weird, cause it still works for me. Also make sure you have the latest version cause I fixed a small bug a few days ago.

Edit: Also make sure you’ve set it up right. You have to require it somewhere in a LocalScript, and my assumption right now is that you haven’t done that part so you’re only seeing what the server is doing (one instant change) since the client side isn’t set up. Also make sure this module is in ReplicatedStorage

2 Likes

Thank you for your reply, I had saved this post from another day and forgot that you had to require the module in a client-side script, which explains my issue. My apologies.

2 Likes

@rek_kie Very nice module here! I have a small question though:

While I understand that the module essentially waits for client to finish the tween animation and then makes the server part the final size, is there a way to make it so that the part on the server is instantly the maximum size so that the client is just doing the tween for aesthetics?

My use case uses the touch event on the parts (via server script) and it detects other players a bit too late as the parts initial size is (1, 1, 1) and then instantly jumps to (100, 100, 100) 5 seconds later.

This is one of my first posts on the dev forum so sorry if I’m posting in the wrong format or something!

1 Like

Thank you! There isn’t actually isn’t a function for this in the module, though, because I wasn’t really thinking that people would have a use case for it. But, if you want it, I can add it, no problem.

Expect an update soon :upside_down_face:

1 Like

I look forward to it! :smiley:

Extra: I don’t know if this is too complicated to create but functionality where instead of having the part on the server start from the start size and then jump straight to the end size, maybe a way to have the part on the server change size as frequently as the developer needs?

For example if you want a part’s size to tween from (1,1,1) to (100, 100, 100) in 10 seconds, the developer would input how often the server should update the server part’s size. So if a step of 0.5 is used then this means that at the 5 second mark of the tween (because 10*0.5 = 5) the server part has it’s size updated to be half the size of the final size (50, 50, 50). If the step was 0.2 then every 1/5th of the 10 seconds (the tween time given) the server part size is updated to what it should be.

I know with this plugin you’re trying to mitigate lag by having as little changes as possible to the part on the server, but having this option might give more reliability to the module if touch events are involved so that things can collide more accurately! This is just an idea and idk how hard this would be to implement!

1 Like

I’ll probably get around to updating this pretty soon!

I’m a lot less busy and have some free time so stay tuned.

1 Like

Hey there, great module so far, going to be extremely useful for optimization if I can get it working right. Thus far I’ve been able to implement it as a replacement for most of my tweens, however I keep consistently running into an error I’m having problems debugging. It seems to occur only when the Reversed value is true and has been happening whether I tween the CFrame or Position, though I havent tested if it happens when reverse is true and tweening any property.

Error: Error

Module Code In Question: ModuleCode

My Code In Question: Code

Any ideas? So far it definitely seems to root from having reversed enabled and from the module itself as tweening the cframe with the module using my code works fine so long as it’s not reversed, however I don’t have the time tonight to get to the root of the cause and it wouldnt surprise me if its something on my end.

1 Like

Yeah there’s probably a bug with reverses. I personally don’t use reverses so I didn’t really test that part fully. I’ll look into it and get back to you

1 Like

@TimesIllusion fix the reverse issue, right after if info.Reverses then add startProperties = {}. The issue is that you are trying to add members to a variable that is not a table, so we make it a table before we add to it.

3 Likes