Simple Animation Controller Module

So I was bored and basically wrote a wrapper for handling animations I guess.

--! strict
export type Construct = {
	new : (Character : Model) -> (),
	Play : (Construct, Animation : number | Animation, Priority : Enum.AnimationPriority, Looped : boolean?, FadeIn : number?, Weight : number?, Speed : number?, FadeOut : number?) -> (),
	Stop : (Construct, FadeOut : number?) -> (),
	StopAtKeyFrame : (Construct, KeyFrame : string, FadeOut : number?) -> (),
	GetAnimation : (Construct) -> Animation | nil,
	SetPriority : (Construct, Priority : Enum.AnimationPriority) -> (),
	SetSpeed : (Construct, Speed : number) -> (),
	SetWeight : (Construct, Weight : number, FadeTime : number?) -> (),
	GetKeyFrameTime : (Construct, KeyFrame : string) -> number?,
	MarkerReachedSignal : (Construct, Marker : string, callBack : () -> ()) -> () -> (),
	ClearMarkers : (Construct, Marker : string?) -> (),
	OnLoop : (Construct, callBack : () -> ()) -> () -> (),
	OnEnded : (Construct, callBack : () -> ()) -> (),
}

export type AnimationController = {
	_animation : Animation,
	
	_markers : {
		[string] : {
			[number] : () -> ()
		}
	},
	
	_markerConnections : {
		[string] : RBXScriptConnection
	},
	
	Character : Model,
	Animator : Animator,
	
	_loopConnection : RBXScriptConnection,
	_loopCallbacks : {
		[number] : () -> (),
	},
	
	animationTrack : AnimationTrack,
}

--> Services
local Players = game:GetService('Players')

--> Player
local Player = Players.LocalPlayer

--> Class
local AnimationController = {}
AnimationController.__index = AnimationController

--> Constructor
function AnimationController.new(Character : Model) : Construct & AnimationController
	local self = setmetatable({}, AnimationController)
	
	self.Character = Character
	
	self.Animator = nil
	
	if Character:FindFirstChildWhichIsA('Humanoid') ~= nil then
		local Humanoid = Character:FindFirstChildWhichIsA('Humanoid')
		self.Animator = Humanoid:FindFirstChildWhichIsA('Animator')
	else
		if Character:FindFirstChildWhichIsA('AnimationController') then
			local _AnimationController = Character:FindFirstChildWhichIsA('AnimationController')
			self.Animator = _AnimationController:FindFirstChildWhichIsA('Animator')
		end
	end
	
	self._animation = nil
	self.animationTrack = nil
	
	self._markers = {}
	self._markerConnections = {}
	
	self._loopConnection = nil
	self._loopCallbacks = {}
	
	return self :: Construct & AnimationController
end

--> Methods
function AnimationController:Play(Animation : number | Animation, Priority : Enum.AnimationPriority, Looped : boolean?, FadeIn : number?, Weight : number?, Speed : number?, FadeOut : number?)
	self = self :: Construct & AnimationController
	if self.Animator == nil then return warn(`No animator object was found in {self.Character.Name}`) end
	
	if Looped == nil then Looped = false end
	if FadeIn == nil then FadeIn = 0 end
	if Weight == nil then Weight = 1 end
	if Speed == nil then Speed = 1 end
	if FadeOut == nil then FadeOut = 0 end
	
	
	if self._animation ~= nil then
		self._animation:Destroy()
	end
	
	self._animation = if typeof(Animation) ~= 'number' and Animation:IsA('Animation') then Animation else Instance.new('Animation')
	
	if self.animationTrack ~= nil then
		self:Stop(FadeOut)
	end
	
	if self._animation.AnimationId == '' then
		self._animation.AnimationId = `rbxassetid://{Animation}`
	end
	
	self.animationTrack = self.Animator:LoadAnimation(self._animation)
	self.animationTrack.Priority = Priority
	self.animationTrack:Play(FadeIn, Weight, Speed)
	self.animationTrack.Looped = Looped
	
	if not Looped then
		self.animationTrack.Ended:Wait()
		self.animationTrack:Destroy()
		self.animationTrack = nil
	end
end

function AnimationController:Stop(FadeOut : number?)
	self = self :: Construct & AnimationController
	if FadeOut == nil then FadeOut = 0 end
	
	if self.animationTrack == nil then return end
	
	self.animationTrack:Stop(FadeOut)
	self.animationTrack.Ended:Wait()
	self.animationTrack:Destroy()
	self.animationTrack = nil
end

function AnimationController:OnLoop(callBack : () -> ()) : nil | () -> ()
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	if self.animationTrack.Looped == false then return end
	
	table.insert(self._loopCallbacks, callBack)
	
	if self._loopConnection == nil then
		self._loopConnection = self.animationTrack.DidLoop:Connect(function()
			for _, v in pairs(self._loopCallbacks) do
				v()
			end
		end)
	end
	
	return function()
		local index = table.find(self._loopCallbacks, callBack)
		if index ~= nil then
			table.remove(self._loopCallbacks, index)
		end
	end
end

function AnimationController:OnEnded(callBack : () -> ())
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	
	if self.animationTrack.IsPlaying == true then
		self.animationTrack.Ended:Once(callBack)
	else
		callBack()
	end
end

function AnimationController:StopAtKeyFrame(KeyFrame : string, FadeOut : number?)
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	if KeyFrame == nil then return end
	
	self.animationTrack.KeyframeReached:Once(function(name : string)
		if name == KeyFrame then
			self:Stop(FadeOut or 0)
		end
	end)
end

function AnimationController:SetPriority(Priority : Enum.AnimationPriority)
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	
	self.animationTrack.Priority = Priority
end

function AnimationController:SetSpeed(Speed : number)
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	
	self.animationTrack:AdjustSpeed(Speed)
end

function AnimationController:SetWeight(Weight : number, FadeTime : number?)
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	if FadeTime == nil then FadeTime = 0 end
	
	self.animationTrack:AdjustWeight(Weight, FadeTime)
end

function AnimationController:MarkerReachedSignal(Marker : string, callBack : () -> ()) : nil | () -> ()
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end
	
	if self._markers[Marker] == nil then
		self._markers[Marker] = {}
	end
	
	table.insert(self._markers[Marker], callBack)
	
	if self._markerConnections[Marker] == nil then
		self._markerConnections[Marker] = self.animationTrack:GetMarkerReachedSignal(Marker):Connect(function()
			for _, v in pairs(self._markers[Marker]) do
				v()
			end
		end)
	end
	
	return function()
		local index = table.find(self._markers[Marker], callBack)
		
		if index ~= nil then
			table.remove(self._markers[Marker], index)
		end
	end
end

function AnimationController:ClearMarkers(Marker : string?) --> Don't pass a argument to clear all markers
	self = self :: Construct & AnimationController
	
	if Marker == nil then
		self._markers = {}
		
		for _, v in pairs(self._markerConnections) do
			v:Disconnect()
		end
	elseif Marker ~= nil then
		local MarkerTable = self._markers[Marker]
		if MarkerTable ~= nil then
			table.clear(MarkerTable)
			self._markers[Marker] = nil
		end
		
		if self._markerConnections[Marker] ~= nil then
			self._markerConnections[Marker]:Disconnect()
		end
	end
end

function AnimationController:GetKeyFrameTime(KeyFrame : string) : number?
	self = self :: Construct & AnimationController
	if self.animationTrack == nil then return end

	return self.animationTrack:GetTimeOfKeyframe(KeyFrame)
end

function AnimationController:GetAnimation() : Animation | nil
	self = self :: Construct & AnimationController
	
	if self.animationTrack == nil then return nil end
	
	return self.animationTrack.Animation
end

--> return
return AnimationController

Documentation:


Creating a new animation controller

Start by setting up a new class for the animation controller

local Character = game.Players.LocalPlayer.Character --> Can be a model of a NPC or anything like that
local Controller = require(path-to-module)
local AnimationController = Controller.new(Character)

Playing an animation

Play a simple animation either from an animation id or a animation instance

AnimationController:Play(1234567, Enum.AnimationPriority.Idle, true, 0, 1, 1, 0)

-- OR
local Animation = Instance.new('Animation')
Animation.AnimationId = 'rbxassetid://1234567'
AnimationController:Play(Animation, Enum.AnimationPriority.Idle, true, 0, 1, 1, 0)

Stopping an animation

Stop the current animation (this will fire all callbacks used on OnEnded)

AnimationController:Stop(0)

Stopping an animation at a key frame

Stop the current animation once it hits a certain key frame

AnimationController:StopAtKeyFrame('TestKeyFrame', 0)

Getting the animation instance

Get the current animation tracks animation instance

local Animation = AnimationController:GetAnimation()

Setting the priority

Set or change the priority of the current animation

AnimationController:SetPriority(Enum.AnimationPriority.Action)

Setting the speed

Set or change the speed of the current animation

AnimationController:SetSpeed(5)

Setting the weight

Set or change the weight of the current animation

AnimationController:SetWeight(0.5, 0)

Get a Key Frames time

Get the time a key frame is located

local TimeOfKeyFrame = AnimationController:GetKeyFrameTime('TestKeyFrame')

Detecting when a marker is reached

Set callbacks for when certain markers are reached on the animation. (returns a function to remove the callback)

local callBack_1 = AnimationController:MarkerReachedSignal('MarkerName', function()
    print('Marker reached')
end)

-- Want to remove callBack_1 from the MarkerReached signal? call callBack_1 as a function!
callBack_1()

Clearing markers

Clear all markers or certain markers from the marker reached signal

-- Clearing ALL markers
AnimationController:ClearMarkers()

-- Clearing ONE marker
AnimationController:ClearMarkers('MarkerName')

Detecting when the animation did a loop

Add callbacks to fire when the animation has done a loop. (returns a function to remove the callback)

local onLoop_1 = AnimationController:OnLoop(function()
    print('Did loop 1')
end)

-- Want to remove onLoop_1 from the DidLoop signal? call it as a function!
onLoop_1()

Detect when the animation has fully ended

Add a callback to when the animation fully ends

AnimationController:OnEnded(function()
    print('Animation Ended')
end)

Any feedback / criticism or suggestions is appreciated!

Market place link: AnimationController - Creator Store (roblox.com)

9 Likes

Could this module be used on none humanoids?

If there is an animator, probably.

I’m not the OP though, don’t take this as an answer.

Rather a educated guess.

I would assume so you’d just have to change a few things in it, but I could possibly make it so you pass a character in the .new and it would look for either a humanoid for a animator or a AnimationController for non-humanoid animations.

1 Like

I have updated it so you pass a character in the .new and it will look for the animator in either a humanoid if it exists, or a animationcontroller

1 Like

It was just a theoretical question, I will need to (annoyingly) make a custom animater script for my game, sorry I think for wasting your time?