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)

10 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?