Issues with Image Preloading and Performance Optimization in Pad Animation Script

I’m currently working on multiple pads that indicate energy levels by flashing the activated images to deactivated images based on how much energy is left. The script is designed to change each pad’s image when the energy reaches a certain threshold. However, I’ve run into a few issues that I could use help with.

What I’m Trying to Do:

I’m creating a system where each pad shows different images to indicate the remaining energy of a player. The idea is that as the player’s energy gets lower, the pads change their images to deactivated image to show the current status. For example, when the energy is at 95%, the first pad will show the activated image, and as the energy decreases, deactivated images are displayed for each threshold.

Issue:

  1. Image Preloading Bug:
  • When the pads are activated for the first time, there’s a slight delay or even a blank screen where the first image (deactivated image) should be. This happens only for the first image of each pad, and after that, the images load and display correctly.
  • I suspect that the issue is that the deactivated image is not preloaded properly, while the activated images work fine.
  1. Pads Flashing Simultaneously:
  • When TimeAvailable is added, all the pads start flashing at once according to their threshold. I would like them to flash in sequence, but backward—starting from the last pad to the first.
  • Could anyone suggest how I can approach this behavior? If you have examples or ideas on how to make the pads flash in a specific order, I’d love to hear them.
  1. Performance Concerns:
  • I’m also wondering if the script will continue running unnecessarily in the background, especially considering that it’s checking thresholds frequently. It checks the energy level and triggers animations every time the energy value changes. Would this be inefficient, or is it an acceptable way to handle this kind of logic?
  • I’m looking for any performance optimization suggestions. Is there a better way to write this to improve performance and reduce unnecessary resource usage?

Potential Bugs:

  • Is there any potential issue I might be missing with the way I’m handling the image changes or how the debounce system is implemented? Are there edge cases that I should be aware of that might cause unexpected behavior?

local script

local ContentProvider = game:GetService("ContentProvider")

local player = game:GetService("Players")
local localPlr = player.LocalPlayer

local plrGUI = localPlr:WaitForChild("PlayerGui")
local energyGUI = plrGUI:WaitForChild("EnergyIndicator")
local padsContainer = energyGUI:WaitForChild("Pad")

local moduleScript = require(script.ModuleScript)

local timeAvailable

local thresholds = 
	{
		{value = 95, name = "FirstPad", repeatNum = 10, interval = 0.3, debounce = false},
		{value = 80, name = "SecondPad", repeatNum = 10, interval = 0.3, debounce = false},
		{value = 70, name = "ThirdPad", repeatNum = 10, interval = 0.3, debounce = false},
		{value = 20, name = "FourthPad", repeatNum = 10, interval = 0.3, debounce = false},
		{value = 10, name = "FifthPad", repeatNum = 17, interval = 0.2, debounce = false}
	}

local animationDebounce = {}

local imageUrls = {}

for _, pad in ipairs(moduleScript.padsInfo) do
	table.insert(imageUrls, pad.ActivatedImage)
	table.insert(imageUrls, pad.DeactivatedImage)
end

local function checkFailed(contentId, status)
	if status == Enum.AssetFetchStatus.Failure then
		warn("Failed to load:", contentId)
	else
		print("Successfully preloaded:", contentId)
	end
end

ContentProvider:PreloadAsync(imageUrls, checkFailed)

function checkThreshold()
	for _, threshold in ipairs(thresholds) do
		local padName = threshold.name

		if not animationDebounce[padName] then
			animationDebounce[padName] = false
		end


		if timeAvailable <= threshold.value and threshold.debounce == false and not animationDebounce[padName] then
			animationDebounce[padName] = true 
			moduleScript.AnimationOff(padsContainer[padName], threshold.repeatNum, threshold.interval)
			threshold.debounce = true 
			animationDebounce[padName] = false 
		elseif timeAvailable >= threshold.value and threshold.debounce == true and not animationDebounce[padName] then
			animationDebounce[padName] = true
			moduleScript.AnimationOn(padsContainer[padName], threshold.repeatNum, threshold.interval)
			threshold.debounce = false 
			animationDebounce[padName] = false 
		end
	end
end

localPlr:GetAttributeChangedSignal("TimeAvailable"):Connect(function()
	timeAvailable = localPlr:GetAttribute("TimeAvailable")
	checkThreshold()
end)

module script :

local EnergyPad = {}

EnergyPad.padsInfo = {
	FirstPad = { ActivatedImage = "rbxassetid://129041405703479", DeactivatedImage = "rbxassetid://101979786307533" },
	SecondPad = { ActivatedImage = "rbxassetid://81607568451504", DeactivatedImage = "rbxassetid://106275973037057" },
	ThirdPad = { ActivatedImage = "rbxassetid://78419360549368", DeactivatedImage = "rbxassetid://102809037614547" },
	FourthPad = { ActivatedImage = "rbxassetid://99544566244937", DeactivatedImage = "rbxassetid://98627352130997" },
	FifthPad = { ActivatedImage = "rbxassetid://78610061179352", DeactivatedImage = "rbxassetid://89043824267220" }
}

function EnergyPad.AnimationOff(pad, returnNum, interval)
	print("Pad Name (Off):", pad.Name)

	local padInfo = EnergyPad.padsInfo[pad.Name]
	if not padInfo then
		warn("No entry found for pad name:", pad.Name)
		return
	end

	for i = 1, returnNum do
		task.wait(interval)
		pad.Image = padInfo.DeactivatedImage
		task.wait(interval)
		pad.Image = padInfo.ActivatedImage
	end
	pad.Image = padInfo.DeactivatedImage
end

function EnergyPad.AnimationOn(pad, returnNum, interval)
	print("Pad Name (On):", pad.Name)

	local padInfo = EnergyPad.padsInfo[pad.Name]
	if not padInfo then
		warn("No entry found for pad name:", pad.Name)
		return
	end

	for i = 1, returnNum do
		task.wait(interval)
		pad.Image = padInfo.ActivatedImage
		task.wait(interval)
		pad.Image = padInfo.DeactivatedImage
	end
	pad.Image = padInfo.ActivatedImage
end

return EnergyPad

Things I’ve Tried So Far:

  • I’ve used ContentProvider:PreloadAsync to preload the images, but it seems like the first image still doesn’t load immediately when the animation starts.
  • I’ve checked the debounce mechanism, and it seems to be working fine, but the images still don’t appear as expected initially.

Any help, suggestions for performance improvements, or insights on potential bugs would be greatly appreciated!

Images:

Capture
Capture2

Reference:

Im trying to replicate what the jewels do in miraculous ladybug. Where the parts in their jewels will sequentially disappear and flash to indicate how much energy left .

I’ve run into this with menu backgrounds. I tried a preload command, but it didn’t work out. So, when the game starts, I have a black UI that covers the screen. Under that, I display the menu pictures, then close everything out and use a fade-out effect on the black screen. After that, all the pictures load without the stall you’re talking about. So that works for sure… Later I found this…

local ContentProvider = game:GetService("ContentProvider")
local imagesToPreload = {
    "rbxassetid://Image1ID",
    "rbxassetid://Image2ID",
    "rbxassetid://Image3ID"
}

ContentProvider:PreloadAsync(imagesToPreload)

I use this on some objects but I still use the first way too. Also use the “rbxassetid://picture”
on the decal or whatever you have them on and not the “http:” way.

1 Like

I tried your suggestion, and it worked perfectly! I’ll work on adding the loading screen later. For now, I want to figure out how to make the pads appear in sequence but in reverse order for the AnimationOn.

Also, does the animation look good to you? Would it cause any lag in the game?

0.033 covers a fps so 0.3 or 0.2 in your interval should keep things in check.
You’re all client here after the loads are done… so lag shouldn’t be a problem.

1 Like

i managed to make the pad reverse when the timeAvailability is increase

local ContentProvider = game:GetService("ContentProvider")

local player = game:GetService("Players")
local localPlr = player.LocalPlayer

local plrGUI = localPlr:WaitForChild("PlayerGui")
local energyGUI = plrGUI:WaitForChild("EnergyIndicator")
local padsContainer = energyGUI:WaitForChild("Pad")

local moduleScript = require(script.ModuleScript)

local timeAvailable

local thresholds = {
	{value = 95, name = "FirstPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 80, name = "SecondPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 70, name = "ThirdPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 20, name = "FourthPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 10, name = "FifthPad", repeatNum = 17, interval = 0.2, debounce = false}
}

local animationDebounce = {}

local imageUrls = {}
for _, pad in ipairs(moduleScript.padsInfo) do
	table.insert(imageUrls, pad.ActivatedImage)
	table.insert(imageUrls, pad.DeactivatedImage)
end

ContentProvider:PreloadAsync(imageUrls)

function checkThreshold()
	for _, threshold in ipairs(thresholds) do
		local padName = threshold.name
		if not animationDebounce[padName] then
			animationDebounce[padName] = false
		end

		if timeAvailable <= threshold.value and not threshold.debounce and not animationDebounce[padName] then
			animationDebounce[padName] = true
			moduleScript.AnimationOff(padsContainer[padName], threshold.repeatNum, threshold.interval)
			threshold.debounce = true
			animationDebounce[padName] = false
		end
	end

	for i = #thresholds, 1, -1 do
		local threshold = thresholds[i]
		local padName = threshold.name

		if timeAvailable >= threshold.value and threshold.debounce and not animationDebounce[padName] then
			animationDebounce[padName] = true
			moduleScript.AnimationOn(padsContainer[padName], threshold.repeatNum, threshold.interval)
			threshold.debounce = false
			animationDebounce[padName] = false
		end
	end
end

localPlr:GetAttributeChangedSignal("TimeAvailable"):Connect(function()
	timeAvailable = localPlr:GetAttribute("TimeAvailable")
	checkThreshold()
end)
local EnergyPad = {}

EnergyPad.padsInfo = {
	FirstPad = { ActivatedImage = "rbxassetid://129041405703479", DeactivatedImage = "rbxassetid://101979786307533" },
	SecondPad = { ActivatedImage = "rbxassetid://81607568451504", DeactivatedImage = "rbxassetid://106275973037057" },
	ThirdPad = { ActivatedImage = "rbxassetid://78419360549368", DeactivatedImage = "rbxassetid://102809037614547" },
	FourthPad = { ActivatedImage = "rbxassetid://99544566244937", DeactivatedImage = "rbxassetid://98627352130997" },
	FifthPad = { ActivatedImage = "rbxassetid://78610061179352", DeactivatedImage = "rbxassetid://89043824267220" }
}

function EnergyPad.AnimationOff(pad, repeatNum, interval)
	print("Pad Name (Off):", pad.Name)

	local padInfo = EnergyPad.padsInfo[pad.Name]
	if not padInfo then
		warn("No entry found for pad name:", pad.Name)
		return
	end

	for i = 1, repeatNum do
		task.wait(interval)
		pad.Image = padInfo.DeactivatedImage
		task.wait(interval)
		pad.Image = padInfo.ActivatedImage
	end
	pad.Image = padInfo.DeactivatedImage
end

function EnergyPad.AnimationOn(pad, repeatNum, interval)
	print("Pad Name (On):", pad.Name)

	local padInfo = EnergyPad.padsInfo[pad.Name]
	if not padInfo then
		warn("No entry found for pad name:", pad.Name)
		return
	end

	for i = repeatNum, 1, -1 do
		task.wait(interval)
		pad.Image = padInfo.ActivatedImage
		task.wait(interval)
		pad.Image = padInfo.DeactivatedImage
	end
	pad.Image = padInfo.ActivatedImage
end

return EnergyPad

is this good?

Another Issue :

I’m having an issue with the pad animation syncing with timeAvailable. The animations start correctly when the threshold is met (e.g., at 95), but the final animation, where the loop ends and the image is set to either the activated or deactivated image, is not matching the intended threshold. For example, the animation starts when timeAvailable hits 95, but it ends at 84, which isn’t what I want. How can I ensure the animation ends exactly when the threshold is reached?"

A possible fix…

function EnergyPad.AnimationOff(pad, repeatNum, interval)
    print("Pad Name (Off):", pad.Name)

    local padInfo = EnergyPad.padsInfo[pad.Name]
    if not padInfo then
        warn("No entry found for pad name:", pad.Name)
        return
    end

    for i = 1, repeatNum do
        task.wait(interval)
        pad.Image = padInfo.DeactivatedImage
        task.wait(interval)
        pad.Image = padInfo.ActivatedImage
    end

    --final image reflects the threshold state
    if timeAvailable <= thresholds[1].value then
        pad.Image = padInfo.DeactivatedImage
    else
        pad.Image = padInfo.ActivatedImage
    end
end

function EnergyPad.AnimationOn(pad, repeatNum, interval)
    print("Pad Name (On):", pad.Name)

    local padInfo = EnergyPad.padsInfo[pad.Name]
    if not padInfo then
        warn("No entry found for pad name:", pad.Name)
        return
    end

    for i = repeatNum, 1, -1 do
        task.wait(interval)
        pad.Image = padInfo.ActivatedImage
        task.wait(interval)
        pad.Image = padInfo.DeactivatedImage
    end

    --again, final image reflects the threshold state
    if timeAvailable >= thresholds[#thresholds].value then
        pad.Image = padInfo.ActivatedImage
    else
        pad.Image = padInfo.DeactivatedImage
    end
end

Not tested (and you know how that goes) it is showing the possible fix however.

2 Likes

Before i tried it, i knew an error would coming up because the module script doesn’t have timeAvailable and threshold sooo…

well, i kinda solve it?

local ContentProvider = game:GetService("ContentProvider")

local player = game:GetService("Players")
local localPlr = player.LocalPlayer

local plrGUI = localPlr:WaitForChild("PlayerGui")
local energyGUI = plrGUI:WaitForChild("EnergyIndicator")
local padsContainer = energyGUI:WaitForChild("Pad")

local moduleScript = require(script.ModuleScript)

local timeAvailable

local thresholds = {
	{value = 95, name = "FirstPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 80, name = "SecondPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 70, name = "ThirdPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 20, name = "FourthPad", repeatNum = 10, interval = 0.3, debounce = false},
	{value = 10, name = "FifthPad", repeatNum = 17, interval = 0.2, debounce = false}
}

local animationDebounce = {}

local imageUrls = {}
for _, pad in ipairs(moduleScript.padsInfo) do
	table.insert(imageUrls, pad.ActivatedImage)
	table.insert(imageUrls, pad.DeactivatedImage)
end

ContentProvider:PreloadAsync(imageUrls)

function checkThreshold()
    for _, threshold in ipairs(thresholds) do
        local padName = threshold.name

        local totalAnimationTime = threshold.repeatNum * threshold.interval + 3

        if not animationDebounce[padName] then
            animationDebounce[padName] = false
        end

        if timeAvailable <= (threshold.value + totalAnimationTime) and not threshold.debounce and not animationDebounce[padName] then
            animationDebounce[padName] = true
            moduleScript.AnimationOff(padsContainer[padName], threshold.repeatNum, threshold.interval)
            threshold.debounce = true
            animationDebounce[padName] = false
        end
    end

    for i = #thresholds, 1, -1 do
        local threshold = thresholds[i]
        local padName = threshold.name

        -- Calculate total animation duration
        local totalAnimationTime = threshold.repeatNum * threshold.interval

        -- Adjust timing to start animation earlier by totalAnimationTime
        if timeAvailable >= (threshold.value + totalAnimationTime) and threshold.debounce and not animationDebounce[padName] then
            animationDebounce[padName] = true
            moduleScript.AnimationOn(padsContainer[padName], threshold.repeatNum, threshold.interval)
            threshold.debounce = false
            animationDebounce[padName] = false
        end
    end
end


localPlr:GetAttributeChangedSignal("TimeAvailable"):Connect(function()
	timeAvailable = localPlr:GetAttribute("TimeAvailable")
	checkThreshold()
end)

but i felt like its not practical, it still stop at the value i wanted but yeah… i felt that there’s other more efficient way to do it