What is the right way to manage Timers?

I have some issues in my game related to timers, and at this point I have doubts if I use them right way.

Game is snake-like
Actually I’m testing two players mode
One power up can be spawned at once
Each player can take one power up at once
Power up has 3 different spawn time delay
Timers have handles initialized at the beginning
You can pause game

Here is the simplified code of aformentioned functions. Left only essential parts (code is long) so you CAN’T run it. I hope is readable tho


local composer = require( "composer" )

local scene = composer.newScene()

local gameLoopTimer 			-- Timer for game loop
local powerUpEffectTimer		-- Timer for power up effect
local powerUpClockTimer 		-- Timer for power up gui clock
local powerUpSpawnTimer			-- Timer for power up spawn delay

local gameMode = composer.getVariable( "mode" )	-- 1 - single player; 2 - multiplayer
local gameState
local snakeGodMode = {false, false}		-- Modifier if player has GOD MODE [1] - player 1 [2] - player 2

local powerUps = true 				-- Gamerule if power ups are allowed

local p1isPower = false 	-- Player 1 has powerup flag
local p2isPower = false 	-- Player 2 has powerup flag

local powerUpExist = false
local powerUpTable = {}		-- Table for spawned power ups
local powerUpTypes
local powerUpTimes = {5000,10000,15000}

if gameMode == 1 then 
	powerUpTypes = {"speedUp", "slowDown", "godMode", "revUp", "randUp"}
elseif gameMode == 2 then 
	powerUpTypes = {"speedUp", "slowDown", "godMode", "revUp", "speedUpB", "slowDownB", "revUpB", "randUp"}
end


local function pauseTimers()

	if snakeMoveTimer ~= nil then timer.pause( snakeMoveTimer ) end
	if gameLoopTimer ~= nil then timer.pause( gameLoopTimer ) end
	if powerUpEffectTimer ~= nil then timer.pause( powerUpEffectTimer ) end
	if powerUpClockTimer ~= nil then timer.pause( powerUpClockTimer ) end
	if powerUpSpawnTimer ~= nil then timer.pause( powerUpSpawnTimer ) end
	if gameMode == 2 then 
		if snakeMoveTimer2 ~= nil then timer.pause( snakeMoveTimer2 ) end 
		if respawnTimer ~= nil then timer.pause( respawnTimer ) end
	end

end

local function resumeTimers()

	if snakeMoveTimer ~= nil then timer.resume( snakeMoveTimer ) end
	if gameLoopTimer ~= nil then timer.resume( gameLoopTimer ) end
	if powerUpEffectTimer ~= nil then timer.resume( powerUpEffectTimer ) end
	if powerUpClockTimer ~= nil then timer.resume( powerUpClockTimer ) end
	if powerUpSpawnTimer ~= nil then timer.resume( powerUpSpawnTimer ) end
	if gameMode == 2 then 
		if snakeMoveTimer2 ~= nil then timer.resume( snakeMoveTimer2 ) end 
		if respawnTimer ~= nil then timer.resume( respawnTimer ) end
	end
end


-- Creates new pickup item after previous one is collected
local function pickupSpawn (itemType)
				
		if itemType == "power" then

		timer.cancel(powerUpSpawnTimer)
		powerUpSpawnTimer = nil
		local powerVar = math.random( #powerUpTypes )

		local thisPowerUp = display.etc.
		thisPowerUp.myName = powerUpTypes[powerVar]

		thisPowerUp:setSequence( powerUpTypes[powerVar] )

		thisPowerUp:play()
		powerUpTable[#powerUpTable + 1] = thisPowerUp


	end
end


-- Drawing Ui with powerup icon and clock
local function powerUpActivate(player, powerType)

	local pIconX, pIconY, pTimerX, pTimerY
	local pTime = 10

	if player == snakeHead then
		pIconX, pIconY, pTimerX, pTimerY = blah, blah, blah, blah
		p1isPower = true
	elseif player == snakeHead2 then
		pIconX, pIconY, pTimerX, pTimerY = blah, blah, blah, blah
		p2isPower = true
	end

	local pUpIcon = display.etc.
	
	local timerClock = display.etc.

		local countDown = function (event)
			pTime = pTime - 1
			timerClock.text = pTime
			if event.count == 10 then 
				display.remove(pUpIcon)
				display.remove(timerClock)
				if player == snakeHead then 
					p1isPower = false
				elseif player == snakeHead2 then 
					p2isPower = false
				end
			end
		end
	powerUpClockTimer = timer.performWithDelay( 1000, countDown, 10 )
end

-- God Mode effect power up
local function makeGodMode(player, long, target)

	local pNum

	if player == snakeHead then 
		pNum = 1
	elseif player == snakeHead2 then
		pNum = 2
	end

	snakeGodMode[pNum] = true

	powerUpActivate(player, "godMode")

	local closure = function()
		snakeGodMode[pNum] = false

		timer.cancel(powerUpEffectTimer)
		powerUpEffectTimer = nil
	end

	powerUpEffectTimer = timer.performWithDelay( long, closure )

end


-- Power up collecting check
local function powerCheck (headObject)

	for i = #powerUpTable, 1, -1 do

		local thisPower = powerUpTable[i]

		if (headObject.x == thisPower.x) and (headObject.y == thisPower.y) then
			
			if thisPower.myName == "godMode" then 

				makeGodMode(headObject, 10000)

			end

			powerUpExist = false

			display.remove(thisPower)
			thisPower = nil

		end
	end
end	

local function gameLoop()
	
	if powerUps == true and powerUpExist == false then
		local d = math.random(#powerUpTimes)
		local dd = powerUpTimes[d]
		powerUpExist = true
		local closure = function() return pickupSpawn("power") end
		powerUpSpawnTimer = timer.performWithDelay( dd, closure)
	end
	
	
	if gameMode == 1 then 
		if powerUps == true then
			if p1isPower == false then
				powerCheck(snakeHead)
			end
		end
	elseif gameMode == 2 then 

		if powerUps == true then
			if p1isPower == false then
				powerCheck(snakeHead)
			elseif p2isPower == false then
				powerCheck(snakeHead2)
			end
		end
	end
end


local function resumeGame(button)
	
	gameState = "active"

	if button == "resume" then
		resumeTimers()
		transition.resumeAll()
		--timer.resumeAll()
	elseif button == "exit" then
	
	endGame()
	end

end

local function pauseGame()
	
	-- Pausing game
	gameState = "paused"
	pauseTimers()
	transition.pauseAll()

end


function scene:show( event )

	local sceneGroup = self.view
	local phase = event.phase

	if ( phase == "will" ) then

	elseif ( phase == "did" ) then
		
		intro.create(stageNo)
		timer.performWithDelay( 2600, function()
			gameLoopTimer = timer.performWithDelay( 10, gameLoop, 0 );
		
			snakeMoveTimer = timer.performWithDelay( 300, moveLoop, 0);

			if gameMode == 2 then snakeMoveTimer2 = timer.performWithDelay( 300, moveLoop2, 0 ) end;

			gameState = "active";

		end )
	end
end

function scene:hide( event )

	local sceneGroup = self.view
	local phase = event.phase

	if ( phase == "will" ) then

		if snakeMoveTimer ~= nil then timer.cancel( snakeMoveTimer ) end
		if gameLoopTimer ~= nil then timer.cancel( gameLoopTimer ) end
		if powerUpEffectTimer ~= nil then timer.cancel( powerUpEffectTimer ) end
		if powerUpClockTimer ~= nil then timer.cancel( powerUpClockTimer ) end
		if powerUpSpawnTimer ~= nil then timer.cancel( powerUpSpawnTimer ) end
		if gameMode == 2 then 
			if snakeMoveTimer2 ~= nil then timer.cancel( snakeMoveTimer2 ) end 
			if respawnTimer ~= nil then timer.cancel( respawnTimer ) end
		end

	elseif ( phase == "did" ) then
		transition.pauseAll()
		transition.cancelAll()
		
		composer.removeScene( "game" )
	end
end
  1. After few iterations of spawning and collecting power up, when I pause/resume game I got warnings due to “expired timers”. Even I delete them when they are expired.
    (Yes, these warnings are about one of these timers 'cause I hadn’t any warnings before implementing these)

  2. Also sometimes I have problem with “powerUpSpawnTimer” that even though it gets a “dd” number to spawn (for debug I’ve put there print right before fire of timer) but nothing happened. To be precise, it spawns few time, I collect power ups, and then stops spawning. (no warnings in console)

  3. Another thing is when one player has powerup (god mode in this case) and another power up is spawned and player 2 takes it. The powerUpEffectTimer, and other power up related timers are doubled. What should I do to make them “dynamic” and not coliding with each other?

I thought about creating local handles of powerUpEffectTimer etc. and adding them to table with timers, and then removing proper one with loop after finish.
Speaking of pausing and resuming it should work to if in pauseTimers() and resumeTimers() I could add loops.

  1. Moreover double of powerUpEffectTimer is when player is respawned, you got god mode too but it has separate function due to placing in code. But still use same handle for timer like makeGodMode()

And I’m not sure if I should use iterations in powerUpSpawnTimer or in any of them. Because this one is never start two at the same time but powerUpEffectTimer does so what with it?

You’re running into three classic Solar2D timer pitfalls:

  1. sharing one handle for things that can exist more than once
  2. pausing/resuming (or canceling) timers that already expired
  3. starting new timers without clearing old references

Below is a tight “pattern” that fixes all three. You can drop it in without rewriting your whole scene.


1) Stop sharing single globals for multi-entity timers

Anything that can exist per-player (or per power-up) needs its own handle. Keep them in a table, not one global.

-- One slot per player, each slot can hold multiple handles
local PTimers = {
  [1] = { effect=nil, clock=nil },  -- player 1
  [2] = { effect=nil, clock=nil },  -- player 2
}

local function safeCancel(h)
  if h then pcall(timer.cancel, h) end
  return nil
end

local function safePause(h)
  if h then pcall(timer.pause, h) end
end

local function safeResume(h)
  if h then pcall(timer.resume, h) end
end

God mode rework (per-player)

local snakeGodMode = { false, false }

local function startGodMode(pIdx, durationMs)
  -- clear any previous timers for this player
  PTimers[pIdx].effect = safeCancel(PTimers[pIdx].effect)
  PTimers[pIdx].clock  = safeCancel(PTimers[pIdx].clock)

  snakeGodMode[pIdx] = true
  local timeLeft = math.floor(durationMs/1000)

  -- UI clock (10…1). Use a repeating timer *owned* by this player
  PTimers[pIdx].clock = timer.performWithDelay(1000, function(e)
    timeLeft = timeLeft - 1
    -- update that player's clock display here…
    if timeLeft <= 0 then
      PTimers[pIdx].clock = nil
    end
  end, timeLeft)

  -- effect timer (one-shot)
  PTimers[pIdx].effect = timer.performWithDelay(durationMs, function()
    snakeGodMode[pIdx] = false
    PTimers[pIdx].effect = nil
  end)
end

-- call like:
-- startGodMode(1, 10000) -- player 1, 10s
-- startGodMode(2, 10000) -- player 2, 10s

Why this fixes “doubling”: each player owns their own effect and clock, and you cancel the old one before starting a new one.


2) Manage the spawn timer so it never “goes stale”

Only ever keep one spawn handle. Nil it inside the timer’s listener as soon as it fires. Never resume/cancel a timer after it fired; just let it go to nil.

local powerUpSpawnTimer = nil
local powerUpExist = false
local powerUpTimes = { 5000, 10000, 15000 }

local function scheduleNextSpawn()
  if powerUpSpawnTimer or powerUpExist then return end  -- already scheduled or exists
  local dd = powerUpTimes[ math.random(#powerUpTimes) ]

  powerUpSpawnTimer = timer.performWithDelay(dd, function()
    powerUpSpawnTimer = nil  -- it just fired; clear the handle so we don't “resume” it later
    if gameState == "active" then
      pickupSpawn("power")
    end
  end)
end

-- your game loop:
local function gameLoop()
  if powerUps and not powerUpExist then
    scheduleNextSpawn()
  end
  -- … powerCheck() logic unchanged …
end

-- when a power-up gets collected or removed:
local function onPowerUpConsumed()
  powerUpExist = false
  scheduleNextSpawn()
end

Why this fixes “stops spawning”: previously you set powerUpExist = true before there’s an actual power-up on screen, and you sometimes tried to pause/resume a spawn timer that had already fired. Clearing the handle inside its listener prevents “expired timer” warnings and guarantees scheduleNextSpawn() can create a fresh one.


3) Pause/Resume without warnings (loop over live handles only)

Instead of pausing many named globals (some may have already expired), loop over what you actually own:

local function pauseTimers()
  safePause(gameLoopTimer)
  safePause(snakeMoveTimer)
  if gameMode == 2 then safePause(snakeMoveTimer2) end
  safePause(powerUpSpawnTimer)

  -- per-player timers
  for _, bag in ipairs(PTimers) do
    safePause(bag.effect)
    safePause(bag.clock)
  end
end

local function resumeTimers()
  safeResume(gameLoopTimer)
  safeResume(snakeMoveTimer)
  if gameMode == 2 then safeResume(snakeMoveTimer2) end
  safeResume(powerUpSpawnTimer)

  for _, bag in ipairs(PTimers) do
    safeResume(bag.effect)
    safeResume(bag.clock)
  end
end

Why this fixes “expired timers”: pcall swallows the “expired” warnings when a timer already fired. Also, because we nil handles on completion, you’ll naturally try to pause/resume fewer dead timers over time.


4) Spawning & existence flag (race-free)

Make powerUpExist reflect real objects on stage. Set it true when you create the display object; set it false when you remove it; never set it during scheduling.

-- Creates the actual display object and sets powerUpExist
local function pickupSpawn(itemType)
  if itemType ~= "power" or powerUpExist then return end

  local powerVar = math.random(#powerUpTypes)
  local thisPowerUp = display.newImage( ... ) -- your actual creation
  thisPowerUp.myName = powerUpTypes[powerVar]
  -- set sequence etc…

  powerUpExist = true
  powerUpTable[#powerUpTable + 1] = thisPowerUp
end

-- when collected:
local function powerCheck(headObject)
  for i = #powerUpTable, 1, -1 do
    local p = powerUpTable[i]
    if headObject.x == p.x and headObject.y == p.y then
      if p.myName == "godMode" then
        local idx = (headObject == snakeHead) and 1 or 2
        startGodMode(idx, 10000)
      end
      display.remove(p)
      table.remove(powerUpTable, i)
      powerUpExist = false
      scheduleNextSpawn()
      break
    end
  end
end

5) Respawn giving a second god mode

If respawn code also grants god mode, call the same startGodMode(pIdx, dur) you use for powerups. Since startGodMode cancels any existing effect first, you won’t get doubled timers.


6) Scene cleanup (no survivors between scenes)

On scene:hide("will"), cancel what’s still alive, then nil it, including per-player bags:

local function cancelAllTimers()
  gameLoopTimer    = safeCancel(gameLoopTimer)
  snakeMoveTimer   = safeCancel(snakeMoveTimer)
  snakeMoveTimer2  = safeCancel(snakeMoveTimer2)
  powerUpSpawnTimer= safeCancel(powerUpSpawnTimer)

  for _, bag in ipairs(PTimers) do
    bag.effect = safeCancel(bag.effect)
    bag.clock  = safeCancel(bag.clock)
  end
end

function scene:hide(event)
  if event.phase == "will" then
    cancelAllTimers()
    -- transitions, etc.
  elseif event.phase == "did" then
    composer.removeScene("game")
  end
end

TL;DR rules that will keep timers sane

  • One thing = one handle. No sharing globals for per-player/per-powerup timers.
  • Nil the handle when it fires/cancels. Do it inside the timer’s own listener.
  • Never “resume” an expired timer. If you need it again, schedule a new one.
  • Guard pause/cancel with pcall. No more “expired timer” warnings.
  • Make powerUpExist reflect reality. Only true while a display object exists.

If you want, paste your makeGodMode, powerCheck, and spawn bits, and I’ll patch them into this pattern verbatim so you can drop-in replace.