I’ve been inspired by @5uphi’s tutorial about optimizing TweenService by making the client do all the tweening. I decided to take a crack at the problem and attempt at designing a drop-in replacement for TweenService.
This is what I was able to get done so far, it looks pretty good. The green part represents the server, and the blue part represents the client and it syncs up pretty well.
My next hurdle right now is to get it to work with CFrames.
Right now I use the generic lerp formula in my code.
local function updateInstanceProperties()
for property, endValue in pairs(properties) do
if TWEENABLE_PROPERTY_TYPES[typeof(endValue)] then
local startValue = startProperties[property]
local interpolatedValue = startValue + (endValue - startValue) * TweenService:GetValue(elapsedTweenTime / tweenInfo.Time, tweenInfo.EasingStyle, tweenInfo.EasingDirection)
instance[property] = interpolatedValue
end
end
end
The problem there is that CFrame doesn’t have an operator overload for CFrame - CFrame
there is an overload for CFrame - Vector3
though.
While I could convert the CFrames to a position, apply the formula, and convert it back, that seems like a hack and wouldn’t that preserve rotation information?
My Luau is quite rusty so feel free to point out any oddities.
Replicated Tweening Module
local ReplicatedTweening = {}
local TweenService = game:GetService("TweenService")
local TweenEvents = script.TweenEvents
local PlayTween = TweenEvents.PlayTween
local PauseTween = TweenEvents.PauseTween
local CancelTween = TweenEvents.CancelTween
local DestroyTween = TweenEvents.DestroyTween
export type ReplicatedTween = {
Instance: Instance,
TweenInfo: TweenInfo,
PlaybackState: Enum.PlaybackState,
Completed: RBXScriptSignal,
Cancel: (self: ReplicatedTween) -> (),
Destroy: (self: ReplicatedTween) -> (),
Pause: (self: ReplicatedTween) -> (),
Play: (self: ReplicatedTween) -> ()
}
--[[
local function printf(formatString: string, ...: any)
print(string.format(formatString, ...))
end
-]]
local TWEENABLE_PROPERTY_TYPES = {
["number"] = true,
["boolean"] = true,
["CFrame"] = true,
["Rect"] = true,
["Color3"] = true,
["UDim"] = true,
["UDim2"] = true,
["Vector2"] = true,
["Vector2int16"] = true,
["Vector3"] = true
}
local tweenEndedThreads = {}
function ReplicatedTweening:Create(instance: Instance, tweenInfo: TweenInfo, properties: { [string]: any }): ReplicatedTween
local self = {
Instance = instance,
TweenInfo = tweenInfo,
PlaybackState = if tweenInfo.DelayTime > 0 then Enum.PlaybackState.Delayed else Enum.PlaybackState.Begin,
}
local unpackedTweenInfo = { tweenInfo.Time, tweenInfo.EasingStyle, tweenInfo.EasingDirection, tweenInfo.RepeatCount, tweenInfo.Reverses, tweenInfo.DelayTime }
local events = {
Completed = Instance.new("BindableEvent")
}
local playbackStartTime = 0
local elapsedTweenTime = 0
local startProperties: { [string]: any } = {}
for eventName, event: BindableEvent in pairs(events) do
self[eventName] = event.Event
end
for property, _ in pairs(properties) do
startProperties[property] = instance[property]
end
local function cancelTweenEndTask()
local thread = tweenEndedThreads[instance]
if thread then
task.cancel(thread)
tweenEndedThreads[instance] = nil
end
end
local function updateInstanceProperties()
for property, endValue in pairs(properties) do
if TWEENABLE_PROPERTY_TYPES[typeof(endValue)] then
local startValue = startProperties[property]
local interpolatedValue = startValue + (endValue - startValue) * TweenService:GetValue(elapsedTweenTime / tweenInfo.Time, tweenInfo.EasingStyle, tweenInfo.EasingDirection)
instance[property] = interpolatedValue
end
end
end
function self:Play()
if self.PlaybackState == Enum.PlaybackState.Playing then
self:Cancel()
end
self.PlaybackState = Enum.PlaybackState.Playing
playbackStartTime = os.clock()
PlayTween:FireAllClients(instance, unpackedTweenInfo, properties)
if tweenInfo.RepeatCount < 0 or tweenInfo.Reverses then
return
end
local totalTweenTime = (tweenInfo.Time + tweenInfo.DelayTime) * (tweenInfo.RepeatCount + 1)
local thread = task.delay(totalTweenTime, function()
updateInstanceProperties()
tweenEndedThreads[instance] = nil
self.PlaybackState = Enum.PlaybackState.Completed
events.Completed:Fire(self.PlaybackState)
end)
tweenEndedThreads[instance] = thread
end
function self:Cancel()
self.PlaybackState = Enum.PlaybackState.Cancelled
elapsedTweenTime += os.clock() - playbackStartTime
cancelTweenEndTask()
updateInstanceProperties()
end
function self:Pause()
if self.PlaybackState == Enum.PlaybackState.Paused then
return
end
self.PlaybackState = Enum.PlaybackState.Paused
elapsedTweenTime += os.clock() - playbackStartTime
cancelTweenEndTask()
PauseTween:FireAllClients(instance)
updateInstanceProperties()
end
function self:Destroy()
for _, event: BindableEvent in ipairs(events) do
event:Destroy()
end
DestroyTween:FireAllClients(instance)
end
return setmetatable(self, {
__index = self,
__newindex = nil
})
end
return ReplicatedTweening