How many wait timers can we use?

I recently whipped up an example for someone, showing how to make stars animate in the sky. it was a quick and dirty hack on my part, and initially i liked the idea that i would spin off a dozen or so wait timers...

the idea was pretty straight forward:

  • spawn "passive stars" for every tile of the night sky; they are just a single black frame
  • once every few seconds emit an "animateStars" event that "passive stars" handle
  • use random to setup a stochastic process to transition something like 1% of the "passive stars" to "active stars" (active == they will become visible using timers/etc)
  • "active stars" would run a script at this transition, and here is where things went south for me
  • "active stars" did this on a transition from "passive":
    • swap to one of several animated tiles
    • issue a "wait" command for some random duration (0-2 seconds) of "animation on" period
    • swap to an intermediary "dim star" tile
    • issue a "wait" command for some random duration (0-10 seconds) of "always dark time"
    • swap back to "passive star"
    • (now this star is among the set which will react to "animateStars", resulting in the stochastic proess through which random stars show up in the sky

but it seems we can't do that at all - the code ran fine, but the "wait" duration for every star ended up being exactly the same. I did a quick search here for "wait" and found that @Mattgames suggested a while back that calling "wait" more than once would overwrite the previous "wait" command:
Wait function with Label function:

So are we only able to spawn a single "wait" timer? Or can we use a few, so long as they use different variables for duration?

Assuming we can only spawn a single timer i'll likely craft up something a bit more robust (using tiles as "class" like containers, with a root time-step of 1 loop/frame duration). Still, if there is a way to have more than one wait timer running... or plans to implement that in the runtime eventually... i'd love to use those options instead.

[EDIT: for what it's worth, i did share a different approach for the animated stars - one that depends on each star animation tile having different numbers of frames in order to create the randomize start/end timing. here's the code, including some place hodlers for the wait-timer based method:
Test.zip (5.0 KB) ]

test

What you might want to do instead is make a Sprite with multiple frames (and a fps of 0), and then make a tick function that happens every 1 or 2 times per second. Then , in the sprite code on tick have a random chance of advancing to the next frame.

in the Game

on loop do
  nextTick -= 1
  if nextTick < 0 then
    emit "tick"
    nextTick = 10
  end
end

On the sprite code

on tick do
	starFrame = frame
	if starFrame == 0 then
	  goNext = random 2
	elseif starFrame == 1 then
	  goNext = random 10
	end
	if goNext == 0 then
	  starFrame += 1
	  if starFrame > 2 then
	    starFrame = 0
	  end
	  frame starFrame
	end
end
1 Like

thanks @BoldBigflank - that's definitely a viable solution to animating the stars... but it doesn't help me understand what's happening with timers.

I've written some test code; first I tried creating 10 timers within a while loop:

on timerLoop do
  if it_loop>0 then
    log "timerLoop already running, please wait"
  else
    // it_loop=1
    itL_local=0
    while itL_local <10 do
      itL_local++
      it_loop++
      log "kicking off timerLoop @ {itL_local}"
      wait it_loop then
        log "waited {it_loop} seconds"
        it_loop--
      end
    end
    // it_loop=0
  end
end

Ignore the fact that my log messages can't work as intended due to all variables always being global... nonetheless you might expect 10 unique timers to run, each for a different duration, when this code executes. That doesn't happen though. Instead all 10 "kick off timerLoop..." log messages are emitted, then ~10 seconds elapses before all 10 "waited {it_loop} seconds" log messages are emitted, all at about the same time.

From this I assumed there might be only 1 timer and that multiple callbacks might be registered to that timer - so i end up with a single timer being reconfigured 10 times, eventually running for 10 seconds... but 10 unique callbacks executed when the timer finally expires.

To test this I created another function:

on timerDiscrete do
  if it_discrete > 0 then
    log "timerDiscrete already running, pleae wait"
  else
    log "kicking off timerDiscrete"
    it_discrete=10
    
    disc_0=1
    disc_1=3
    disc_2=5
    disc_3=7
    disc_4=9
    disc_5=2
    disc_6=4
    disc_7=6
    disc_8=8
    disc_9=10
    
    wait disc_0 then
      it_discrete--
      log "0: waited {disc_0}"
    end
    wait disc_1 then
      it_discrete--
      log "1: waited {disc_1}"
    end
    wait disc_2 then
      it_discrete--
      log "2: waited {disc_2}"
    end
    wait disc_3 then
      it_discrete--
      log "3: waited {disc_3}"
    end
    wait disc_4 then
      it_discrete--
      log "4: waited {disc_4}"
    end
    wait disc_5 then
      it_discrete--
      log "5: waited {disc_5}"
    end
    wait disc_6 then
      it_discrete--
      log "6: waited {disc_6}"
    end
    wait disc_7 then
      it_discrete--
      log "7: waited {disc_7}"
    end
    wait disc_8 then
      it_discrete--
      log "8: waited {disc_8}"
    end
    wait disc_9 then
      it_discrete--
      log "9: waited {disc_9}"
    end
    
    log "submitted 10 timers within timerDiscrete"
  end
end

This script creates 10 unique timers of 10 different durations. When I run it I see "kicking off timerDiscrete" followed immediately by "submitted 10 timers...". Then, once per second thereafter, i see each of the 10 timers expire and emit their unique log messages. Here I can confirm that we CAN run at least 10 unique timers! I assume, then, that this is intentional - that Pulp Script was designed to allow for some plurality of timers.

That leaves me wondering why it fails in the timerLoop function? I wondered if perhaps the code scope with the while loop caused an issue in timer instantiation within the runtime, so I created another function:

on timerInline do
  if it_inline>0 then
    log "timerInline already running, please wait"
  else
    log "kicking off timerInline"
    it_inline=10
    it_local=1
    wait it_local then // 1
      it_inline-- // 9
      log "waited 1 seconds"
    end
    it_local++
    
    wait it_local then // 2
      it_inline-- // 8
      log "waited 2 seconds"
    end
    it_local++
    
    wait it_local then // 3
      it_inline-- // 7
      log "waited 3 seconds"
    end
    it_local++
    
    wait it_local then // 4
      it_inline-- // 6
      log "waited 4 seconds"
    end
    it_local++
    
    wait it_local then // 5
      it_inline-- // 5
      log "waited 5 seconds"
    end
    it_local++
    
    wait it_local then // 6
      it_inline-- // 4
      log "waited 6 seconds"
    end
    it_local++
    
    wait it_local then // 7
      it_inline-- // 3
      log "waited 7 seconds"
    end
    it_local++
    
    wait it_local then // 8
      it_inline-- // 2
      log "waited 8 seconds"
    end
    it_local++
    
    wait it_local then // 9
      it_inline-- // 1
      log "waited 9 seconds"
    end
    it_local++
    
    wait it_local then // 10
      it_loop-- // 0
      log "waited 10 seconds"
    end
    it_local++
    
    log "submitted 10 timers with timerInline"
  end
end

This function behaves effectively the same as timerLoop - I end up with just a single 10 second timer and 10 unique callbacks which execute when that single timer expires... so the issue isn't related to the while loop itself.

I next wondered if the issue might be related to how the runtime triggers timers - perhaps timer creation is queued up on each iteration of the super loop with a REFERENCE to the duration value, not a copy of it. therefore on a single iteration of the loop if we call for N timers we might get N timers to start at the end of that loop iteration... but their duration wouldn't be evaluated until that moment between frames, after one loop iteration and before the next one, therefore they would all have the same duration.

Ignore the naming changes below, I was starting to think of the timer as a watch dog timer (wdt):

on load do
  num_wdt=0
  wdt_dur=1
end

on feedTheDog do
  log "fed the dog"
  wdt_dur+=3
end

on watchDog do
  wdt_dur+=2
  num_wdt++
  log "setting watch dog [{num_wdt}] @ {wdt_dur}"
  wait wdt_dur then
    log "the dog is hungry! [{num_wdt}]"
    num_wdt--
  end
end

on confirm do
// 	call "starEvent"
// 	call "timerLoop"
  // call "timerDiscrete"
  call "feedTheDog"
end

on cancel do
  // call "timerInline"
  call "watchDog"
  call "watchDog"
  
end

Ok so this test is a little more complicated - i'm testing a couple things.

  • i attached these watchDog and feedTheDog to my cancel and confirm events, respectively. I did this in the past too, as you can see in the commented-out code
  • on cancel: i call "watchDog" twice. this means a single button tap will try to create 2 timers. note that the timer duration changes each time i call "watchDog"
  • on confirm: i try increasing the value of my wdt_dur variable, to see if i can "feed the dog" (cause a timer that is already running to extend its duration before timing out)

Observations and conclusion

  • calling "watchDog" twice in a single super loop iteration (twice in a single function call) recreated the issues originally seen; this effectively behaves like a single timer with the duration given by the last call to "wait", but all the callbacks are registered and execute.
  • calling "watchDog" multiple times, even while other watchDog timers are running, but on DIFFERENT iterations of the super loop... results in every timer executing exactly as the code suggests (they all run properly)
  • the most recent hypothesis, that the timers are all created with duration values by reference to the variable in the code appears CORRECT.
  • if you want to run multiple timers concurrently and with the same variable holding the timer duration, then you'll need to start each timer on a unique iteration of the super loop

Final test

It felt wrong to hijack the player's DRAW handler, so i forward the game's LOOP handler to a loop handler within player:

// game script
on loop do
  tell event.player to
    call "loop"
  end
end
// player script
on loop do
	if num_wdt>10 then
		wdt_go = 0
	end
	
	if wdt_go==1 then
		call "watchDog"
	end
end

on confirm do
	wdt_go = 1
end

on watchDog do
	wdt_dur += 2
	num_wdt++
	log "setting watch dog [{num_wdt}] @ {wdt_dur}"
	wait wdt_dur then
		log "the dog is hungry! [{num_wdt}]"
		num_wdt--
	end
end

This code creates my 10 unique timers without issues! each one starts about 1/20th of a second after the prior timer, but that could be accounted for in the script if you really want to.

With the above we can create arbitrary numbers of timers :slight_smile:
Linking this into something like the original star animation code might be ugly... you can't just kick off a timer from within each star ... but i'll leave that as an exercise for another day.

I wonder, though, whether this is a bug or a feature?
I might try to summarize this post before pulling in a moderator - it's a long one and i'm sure they are pretty busy already.

-bit

For what it's worth, using the above described process to start just one timer per loop iteration (i.e. per frame), i was able to make a pretty nice star field simulation (project JSON attached below):

Test_stars_new.zip (3.3 KB)
stars_2

This works well for creating just one new star per loop iteration: shifting just one star from 'passive' to 'active' on a given iteration, and starting a wait timer which will eventually tell this star to leave the 'active' state.

But it's a bit trickier after that: shifting from 'active' to 'dim' (in the GIF that's a shift from the large pointed star to a single pixel) happens when the earlier timer expires. I can't be certain that there aren't two or more stars making this change during the same loop iteration.

I suppose I could randomize only the first part of this, keeping the 'dim' duration constant for any/all stars... but that feels like a compromise to me.

I think it would be better to alter the runtime so that calls on "wait" work even if you change the variable later in the same loop iteration. I think i'll post a new thread asking for this as a feature request and pointing here for details.