Help with animated textures script

Hello, today I’ve been trying to finish work on my animated textures script, which is supposed to play a tagged texture atlas as an animation. But I’m currently having trouble trying to get it to sync properly. At the moment individual parts, despite containing the same tags and textureid, are a few frames off from one another creating an effect that’s too distracting to ignore. I thought that its possible to change multiple parts properties at the same time in a script, but I’ve been told otherwise and I am now a little bit lost on how to handle this feature. Below will be the current script, any and all help figuring this out will be greatly appreciated <3

local AnimatedTextureService = {}
-- Services
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")

-- Variables
local frames = 1 -- Number of frames in a spritesheet
local rows = 1 -- Number of rows in a spritesheet
local columns = 1 -- Number of columns in a spritesheet

local targetRate = 0.1 -- The smaller this number, the faster the animations will play
local accumulatedTime = 0

local connections: { [Texture]: RBXScriptConnection } = {}

local shouldUpdate = false
local needsUpdates: { () -> () } = {}
local amountOfTexturesNeedingUpdates = 0
local amountOfTextures = 0

-- Cleanup function
local function cleanup(texture: Texture)
	connections[texture]:Disconnect()
	connections[texture] = nil
end

-- Connection function
local function connect(texture: Texture)
	local currentFrame = 1
	local currentRow = 1
	local currentColumn = 1
	connections[texture] = RunService.Heartbeat:Connect(function(deltaTime)
		if not shouldUpdate then
			return
		end

		if texture.Parent == nil then
			cleanup(texture)
			return
		end

		if amountOfTexturesNeedingUpdates >= amountOfTextures then
			for _, update in needsUpdates do
				update()
			end
			table.clear(needsUpdates)
		end

		local size = texture.Parent.Size -- The spritesheet should be on the FRONT of the part

		accumulatedTime = accumulatedTime + deltaTime

		if accumulatedTime >= targetRate then
			if texture:HasTag("Water") then -- Water
				frames = 32

				rows = 6
				columns = 6

				texture.StudsPerTileU = columns * size.X
				texture.StudsPerTileV = rows * size.Y

				currentColumn = currentColumn + 1
				if currentColumn > columns then
					currentColumn = 1
					currentRow = currentRow + 1
				end

				if currentFrame > frames then
					currentRow = 1
					currentColumn = 1
					currentFrame = 1
				end

				texture.OffsetStudsU = size.X * (currentColumn - 1)
				texture.OffsetStudsV = size.Y * (currentRow - 1)
				currentFrame = currentFrame + 1
			end

			if texture:HasTag("Seafoam") then -- Seafoam
				frames = 27

				rows = 9
				columns = 3

				texture.StudsPerTileU = columns * size.X
				texture.StudsPerTileV = rows * size.Y

				currentColumn = currentColumn + 1
				if currentColumn > columns then
					currentColumn = 1
					currentRow = currentRow + 1
				end

				if currentFrame > frames then
					currentRow = 1
					currentColumn = 1
					currentFrame = 1
				end

				texture.OffsetStudsU = size.X * (currentColumn - 1)
				texture.OffsetStudsV = size.Y * (currentRow - 1)
				currentFrame = currentFrame + 1
			end

			accumulatedTime -= targetRate
		end
	end)
end

-- CollectionService instances
CollectionService:GetInstanceAddedSignal("AnimatedTexture"):Connect(function(texture)
	if texture:IsA("Texture") then
		amountOfTextures += 1
		connect(texture)
	else
		texture:RemoveTag("AnimatedTexture")
	end
end)

CollectionService:GetInstanceRemovedSignal("AnimatedTexture"):Connect(cleanup)

-- For texture loop
for _, texture in CollectionService:GetTagged("AnimatedTexture") do
	connect(texture)
end

-- Initialization and start functions
function AnimatedTextureService.Init()
	return
end

function AnimatedTextureService.Start()
	shouldUpdate = true
	return
end

return AnimatedTextureService
2 Likes

Would os.clock() be possible for this case?

For a better idea:

local FPS = 30
local Frames = 32

local CurrentFrame = math.floor(os.clock() * FPS) % Frames

You’re off a bit and it’s adding up. These need to be right down to the pixel. Always takes me a lot of testing to get it right. The picture has to be perfect also..

One of my templates set to a texture for this..

Summary
root = workspace:WaitForChild("Omega"):WaitForChild("Parts")
	:WaitForChild("PodC"):WaitForChild("Faces"):WaitForChild("Com")
local frame01 = root.Bridge01L.Display.AniTexture

function AnimA(frame, xF, yF)
	local oU, oV = 0, 0
	task.wait(0.7) 
	task.spawn(function()
		while true do
			for stepY = 1, 4  do
				local oV = ((yF * stepY) - yF) frame.OffsetStudsV = oV
				for stepX = 1, 2  do
					local oU = ((xF * stepX) - xF) frame.OffsetStudsU = oU - 0.075
					task.wait(0.3)
				end 
			end 
		end
	end)
end

AnimA(frame01, 8.875, 6)

Hey! I see your template here but don’t fully understand it yet. Do you mind elaborating on it a bit further? Also, what how are your animated texture images setup? Is it the same as mine, one big image with different frames, or are you somehow using separate texture images?

Well, the picture is many shots lined up in a few rows. This one is 4 by 2
OOOO
OOOO
Then I’m using two for loops to run through them all and loop back.
This is more about that picture being perfect than anything else.
As you see it’s down to 8.875 moving horizontally for this shot,
It can’t be 0.001 off, or it will show.

For these, the trim offsets are local oU, oV = 0, 0. Looking at yours, it looks like your oU would need to be more than 0. These were re edited like 30 times to hit that perfect 0, 0. That is not really needed as long as the offset is right for the pictures. The move offset is 8.875 horizontally and 6 vertically.

I hadn’t considered moving the frames along by pixels, I’ll try that out next. Using for loops to run through the frames over run service should be worth looking into as well. If I successfully model my code off of yours as you’ve described it, I will need to separate each texture into their own module scripts, which will actually be better for organization’s sake! Below is the water texture I’m using, which has a resolution of 1000 x 1000.

I’m not even sure how I can get it perfect, since I used programs to create this spritesheet from a gif and resize it to the proper resolution. Do you have any tips on how I should go about this? Sorry if this is an odd request, I’m just unfamiliar with this process xD

Looking at it, it’s perfectly off. That may not be off at all. Maybe that is six rows. It’s different than mine. I actually make the clips (frames) one by one to match each other, with the moment of course. This is a very old school way of doing a cycle animation.. frames, loops, and math.

I think what we are looking at here is 6 rows cut into 36 frames.
The one I’m showing is 2 rows cut into 8 frames.

Considering you used a sprite sheet here. I’ll assume it is perfect..
If it is, this maybe right.

function AnimA(frame, xF, yF)
	task.wait(0.7)
	task.spawn(function()
		while true do
			for stepY = 1, 6 do
				frame.OffsetStudsV = (yF * stepY) - yF
				for stepX = 1, 6 do
					frame.OffsetStudsU = (xF * stepX) - xF
					task.wait(0.3)
				end
			end
		end
	end)
end

AnimA(frame, 1000/6, 1000/6)

These things take a lot of tinkering..

Its getting somewhat late where I am, I’ll return to this tomorrow. Thanks for helping me so far!!

No problem, this is good stuff. Good luck, it’s worth learning. Hard to go through all the steps here on the forums. I’m sure there is some tutorial showing how to do this. It’s been around forever.

Also I’ll take a shot at your script ..

Summary
local AnimatedTextureService = {}
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")

local animations = {
	Water = {
		frames = 32,
		rows = 6,
		columns = 6,
		fps = 10,
	},
	Seafoam = {
		frames = 27,
		rows = 9,
		columns = 3,
		fps = 10,
	}
}

local currentTime = 0
local shouldUpdate = false
local textures = {}
local connections = {}
local animationConnection

local function updateTexture(texture, config, size, frameIndex)
	local frameZeroIndex = frameIndex - 1
	local column = (frameZeroIndex % config.columns) + 1
	local row = math.floor(frameZeroIndex / config.columns) + 1
	
	texture.StudsPerTileU = config.columns * size.X
	texture.StudsPerTileV = config.rows * size.Y
	texture.OffsetStudsU = size.X * (column - 1)
	texture.OffsetStudsV = size.Y * (row - 1)
end

animationConnection = RunService.Heartbeat:Connect(function(deltaTime)
	if not shouldUpdate then return end
	
	currentTime = currentTime + deltaTime
	
	for texture, data in textures do
		if not texture.Parent then
			textures[texture] = nil
			if connections[texture] then
				connections[texture]:Disconnect()
				connections[texture] = nil
			end
			continue
		end
		
		local config = animations[data.type]
		if not config then continue end
		
		local fps = config.fps
		local totalFrames = config.frames
		local frameTime = 1 / fps
		
		local animationTime = currentTime % (totalFrames * frameTime)
		local frameIndex = math.floor(animationTime / frameTime) + 1
		
		frameIndex = math.clamp(frameIndex, 1, totalFrames)
		
		updateTexture(texture, config, data.size, frameIndex)
	end
end)

local function cleanup(texture)
	if connections[texture] then
		connections[texture]:Disconnect()
		connections[texture] = nil
	end
	textures[texture] = nil
end

local function setupTexture(texture)
	if not texture:IsA("Texture") then return end
	
	local animType = nil
	if texture:HasTag("Water") then
		animType = "Water"
	elseif texture:HasTag("Seafoam") then
		animType = "Seafoam"
	end
	
	if not animType then return end
	
	textures[texture] = {
		type = animType,
		size = texture.Parent and texture.Parent.Size or Vector3.new(1, 1, 1)
	}
	
	local parent = texture.Parent
	if parent then
		connections[texture] = parent:GetPropertyChangedSignal("Size"):Connect(function()
			if textures[texture] then
				textures[texture].size = parent.Size
			end
		end)
	end
end

CollectionService:GetInstanceAddedSignal("AnimatedTexture"):Connect(function(texture)
	setupTexture(texture)
end)

CollectionService:GetInstanceRemovedSignal("AnimatedTexture"):Connect(cleanup)

for _, texture in CollectionService:GetTagged("AnimatedTexture") do
	setupTexture(texture)
end

function AnimatedTextureService.Init()
	return
end

function AnimatedTextureService.Start()
	shouldUpdate = true
	return
end

function AnimatedTextureService.Stop()
	shouldUpdate = false
end

function AnimatedTextureService.Cleanup()
	animationConnection:Disconnect()
	for texture in pairs(textures) do
		cleanup(texture)
	end
	table.clear(textures)
end

return AnimatedTextureService

Tried for a synchronized animation state: Instead of each texture having its own frame counter.

Your take on my script ended up working better than my original version, thank you! I’m sure I’ll be able to learn a new practice or to by studying it for my future scripts. Although in regards to a tutorial, I’ve searched a bit online but couldn’t find anything concrete on how to create this feature. In matter of fact it was a resource here on the forum that gave me the code to work into the script I originally posted. Maybe I’m just looking in the wrong places lol.


Not too sure about these seams between the textures, but I have a few ideas on how I can hide them so I’m not too worried about them atm.

1 Like

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