Model Tween V2 - Tween models with ease

Awhile ago I made a “Model Tween Module” that you could use to tween the models properties (Scale, Position and Orientation). It was just a side project of mine and I didn’t think it would be that useful, that is why I thought of rewriting this module because the version I made back then is nowhere performant.

Anyway enough talk:

Here are video concepts:



The Code:

export type ModelTweenProperties = {
	Scale: number?,
	Position: Vector3?,
	Orientation: Vector3?,
	CFrame: CFrame?
}

local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ModelTween = {}

local function createSignal()
	local callbacks = {}

	local signal = {}

	function signal:Fire(...)
		for _, callback in callbacks do
			task.spawn(callback, ...)
		end
	end

	function signal:Connect(callback: () -> ()) : RBXScriptConnection
		table.insert(callbacks, callback)

		return {
			Connected = true,
			Disconnect = function(self)
				self.Connected = false
				table.remove(callbacks, table.find(callbacks, callback))
			end,
		}
	end

	function signal:Once(callback: () -> ()) : RBXScriptConnection
		local connection
		connection = signal.Connect(function(...)
			callback(...)
			connection:Disconnect()
		end)

		table.insert(callbacks, connection)

		return {
			Connected = true,
			Disconnect = function(self)
				self.Connected = false
				table.remove(callbacks, table.find(callbacks, connection))
			end,
		}
	end

	function signal:Wait()
		local thread = coroutine.running()
		local argList

		local connection
		connection = signal.Connect(function(...)
			argList = {...}
			connection.disconnect()
			task.defer(function()
				coroutine.resume(thread, table.unpack(argList))
			end)
		end)

		return coroutine.yield()
	end

	return signal
end

local ModelGetMethods = {
	Scale = function(model: Model)
		return model:GetScale()
	end,

	Position = function(model: Model)
		return model:GetPivot().Position
	end,

	Orientation = function(model: Model)
		local sx, sy, sz, m00, m01, m02, m10, m11, m12, m20, m21, m22 = model:GetPivot():GetComponents()

		local x = math.atan2(-m12, m22)
		local y = math.asin(m02)
		local z = math.atan2(-m01, m00)

		return Vector3.new(x, y, z)
	end,

	CFrame = function(model: Model)
		return model:GetPivot()
	end,
}

local ModelSetMethods = {
	Scale = function(model: Model, scale: number)
		model:ScaleTo(scale)
	end,

	Position = function(model: Model, position: Vector3)
		local modelPivot = model:GetPivot()

		model:PivotTo(CFrame.lookAt(position, position + modelPivot.LookVector))
	end,

	Orientation = function(model: Model, orientation: Vector3)
		local modelPivot = model:GetPivot()

		model:PivotTo(CFrame.lookAt(modelPivot.Position) * CFrame.Angles(math.rad(orientation.X), math.rad(orientation.Y), math.rad(orientation.Z)))
	end,

	CFrame = function(model: Model, cframe: CFrame)
		model:PivotTo(cframe)
	end,
}

local lerpMethods = {
	CFrame = function(cframe: CFrame, targetCframe: CFrame, alpha: number)
		return cframe:Lerp(targetCframe, alpha)
	end,
} 

function ModelTween:Create(model: Model, tweenInfo: TweenInfo, properties: ModelTweenProperties) : Tween
	local preTweenProperties = {}
	
	local tweenThread: thread?
	local tweenHandler: RBXScriptConnection?
	local signal = createSignal()

	return {
		TweenInfo = tweenInfo,
		PlaybackState = Enum.PlaybackState.Begin,
		Completed = signal,

		Cancel = function(tween)
			if tweenHandler then
				tweenHandler:Disconnect()
			end
			
			if tweenThread then
				task.cancel(tweenThread)
			end
			
			tween.PlaybackState = Enum.PlaybackState.Cancelled
			signal:Fire(tween.PlaybackState)

			for propertyName, value in preTweenProperties do
				ModelSetMethods[propertyName](model, value)
			end 
		end,

		Pause = function(tween)
			if tweenHandler then
				tweenHandler:Disconnect()
			end

			if tweenThread then
				task.cancel(tweenThread)
			end

			tween.PlaybackState = Enum.PlaybackState.Paused
		end,

		Play = function(tween)
			tween.PlaybackState = Enum.PlaybackState.Delayed
			
			tweenThread = task.delay(tweenInfo.DelayTime, function()
				tween.PlaybackState = Enum.PlaybackState.Playing

				for propertyName, value in properties do
					preTweenProperties[propertyName] = ModelGetMethods[propertyName](model, value)
				end 
				
				local goingReverse = false
				local repeatedTimes = 0
				local startTime = os.clock()

				tweenHandler = RunService.PostSimulation:Connect(function(_)
					local currentTime = os.clock()
					local deltaTime = currentTime - startTime

					if deltaTime >= tweenInfo.Time then
						if tweenInfo.Reverses and not goingReverse then
							goingReverse = true
							startTime = os.clock()
							return
						end

						if tweenInfo.RepeatCount == -1 or repeatedTimes < tweenInfo.RepeatCount then
							repeatedTimes += 1
							goingReverse = false
							startTime = os.clock()
							return
						end

						if tweenInfo.Reverses then
							for propertyName, value in preTweenProperties do
								ModelSetMethods[propertyName](model, value)
							end
						else
							for propertyName, value in properties do
								ModelSetMethods[propertyName](model, value)
							end
						end

						tween.PlaybackState = Enum.PlaybackState.Completed
						signal:Fire(tween.PlaybackState)

						tweenHandler:Disconnect()
						return
					end

					local alpha = TweenService:GetValue(deltaTime / tweenInfo.Time, tweenInfo.EasingStyle, tweenInfo.EasingDirection)

					for propertyName, value in properties do
						local preValue = preTweenProperties[propertyName]
						
						if goingReverse then
							ModelSetMethods[propertyName](model, lerpMethods[propertyName] and lerpMethods[propertyName](value, preValue, alpha) or value + (preValue - value) * alpha)
						else
							ModelSetMethods[propertyName](model, lerpMethods[propertyName] and lerpMethods[propertyName](preValue, value, alpha) or preValue + (value - preValue) * alpha)
						end
					end
				end)
			end)
		end,
	}
end

return ModelTween

Code sample:

local modelTween = require(PATH_TO_MODULE)
local model = SOME_MODEL
local tweenInfo = TweenInfo.new(2, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut, -1, true)

local modelTween = modelTween:Create(model, tweenInfo, {
	Position = model:GetPivot().Position + Vector3.yAxis * 10,
	Scale = 2
})

modelTween.Completed:Connect(function(playbackState: Enum.PlaybackState)
	print(playbackState)
end)

modelTween:Play()
modelTween:Cancel()
modelTween:Play()

Link to the model

P.S

  • While the returned object is of a type “Tween”, it does not have all the properties (such as Parent, Instance, ect.) it has the one needed for the tween to work.
  • Consider the fact that while this is a optimized version its still not that performant and may cause lag when used on large models, aka with a lot of parts. Consider switching to meshes or other approaches if you experience issues.
10 Likes

Update 1.1 - Quick fix

  • Bug fixes

I can’t click the link to your model

1 Like

Fixed it, didn’t copy the link correctly my bad

1 Like