How to use TweenService:GetValue with CFrame? (Forumula for lerping CFrames, aren't CFrame matrices?)

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

As a side note, how are values such as booleans or EnumItems tweened?

what i do is use the Lerp function like this

local LerpNumber = function(value1, value2, alpha)
	return value1 + (value2 - value1) * alpha
end

local LerpProperties = function(self)
	local alpha = self.Time / self.TweenInfo[1]
	if self.TweenInfo[5] == false then alpha = 1 - alpha elseif alpha > 1 then alpha = 2 - alpha end
	alpha = tweenService:GetValue(alpha, self.TweenInfo[2], self.TweenInfo[3])
	for index, property in self.Properties do
		if type(property) == "number" then
			-- if its a number then we use the lerp function above
			self.Instance[index] = LerpNumber(self.StartProperties[index], property, alpha)
		else
			-- if its not a number we use the built in Lerp function
			-- this works for color3 vector2 vector3 and cframes (any datatype that has the lerp function)
			self.Instance[index] = self.StartProperties[index]:Lerp(property, alpha)
		end
	end
end
2 Likes

That would be CFrame1 * CFrame2:Inverse()

I found the solution, it’s really simple since CFrames have a Lerp function…

local lerps = {
	["CFrame"] = function(a: CFrame, b: CFrame, alpha: number)
		return a:Lerp(b, alpha)
	end,
}

local function updateInstanceProperties()
	for property, endValue in pairs(properties) do
		if TWEENABLE_PROPERTY_TYPES[typeof(endValue)] then
			local startValue = startProperties[property]
			local alpha = TweenService:GetValue(elapsedTweenTime / tweenInfo.Time, tweenInfo.EasingStyle, tweenInfo.EasingDirection)
			local lerp = lerps[typeof(endValue)]
			local interpolatedValue: any = nil
			if lerp then
				interpolatedValue = lerp(startValue, endValue, alpha)
			else
				interpolatedValue = startValue + (endValue - startValue) * alpha
			end
			
			instance[property] = interpolatedValue
		end
	end
end

Your solution is much better, I’ll use that instead. I assumed only CFrame has a lerp function.

many datatypes have the Lerp function
Vector2, Vector3, CFrame and Color3 are some datatypes that have the Lerp function

1 Like

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