Tutorial How-To: Interval timer

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
config.periodic_callback timers:

Disclaimer: See the warning below

Ren'py come with many callbacks, among them there's the and the (apparently not documented) config.periodic_callbacks. These two are called around twenty times each second and so let you have a timer inside your game.

The basic way to use this callback is to count the number of time your code is called, and only works every X times ; with X representing the expected interval.
Code:
init python:
    intervalCounter = 0

    def myTimer():
        store.intervalCounter += 1
        if intervalCounter < 20: return
        store.intervalCounter = 0
        [do what you want]

    config.periodic_callbacks.append( myTimer )
Here, the interval is 1 seconds (1/20 of seconds * 20). But as you can see, it need an external variable to works.
On a side note, you can see that I used both "store.intervalCounter" and "intervalCounter" in my code. The variables in Ren'py store are defined as global, which mean that they can be used inside a Python code without being imported by putting "global theVariableName" in your code. Still there's a limit to this, and it's the assignation. Due to the way Ren'py works, Python have no way to make the difference between the assignation to a global variable, and the creation of a local variable. That why you MUST prefixed the assignations. And don't forget that "-=" is also an assignation.

A practical example of use can be this :
Code:
init python:
    intervalCounter = 0
    value = 0

    def myTimer():
        store.intervalCounter += 1
        if intervalCounter < 20: return
        store.intervalCounter = 0
        store.value += 1
        renpy.restart_interaction()

screen value:
    text "[value] seconds since the start"

label start:
    show screen value
    $ config.periodic_callbacks.append( myTimer )
    "Just watch the time passing."
    $ config.periodic_callbacks.remove( myTimer )
    "Thanks"
    return
It just count the time passing, nothing really interesting I know :D

This works as expected, but there's a better way to do this. One which don't need that you have to wrote all the counting part each time you add a timer, and also don't need an external counter. This better way is to use an object instead of a function :
Code:
init python:

    class MyTimer( renpy.store.object ):
        def __init__( self, interval, fnct ):
            self._interval = interval
            self._fnct = fnct
            self.__ticks = int( interval * 20 )

        def __call__( self ):
            self.__ticks -= 1
            if self.__ticks > 0: return
            self.__ticks = int( self._interval * 20 )
            if callable( self._fnct ): self._fnct()

    def myTimer():
        [do what you want]

    config.periodic_callbacks.append( MyTimer( 1.0, myTimer ) )
The effect will be exactly the same, but now you can reuse MyTimer how many time you want. This mean that you can concentrate your attention on the code which will be executed every X seconds.

The principle is simple.
If an object have a __call__ method, then it will be assimilated to a function ; which mean that it can be... called. This let you use an object, and in the same time make Ren'py and Python think that they have to deal with a function. This way, no more external variable, the counter is integrated to the object.
The other part of the principle regard the functions. A function is, basically speaking, just a variable which have code as value. So you can reference it (functionName) as well as calling it (functionName()). And I used this to pass the code which must be executed at the end of each interval.
Finally, I also declared the counter as private attribute, because it's the most sensible part and it SHOULD NOT be changed outside of the object itself. As for the interval and the called code, they are only protected. They are less sensible, but still shouldn't be seen as public. More regarding object's attributes.


timer screen statement:

Another way to have timers with Ren'py is to use the screen statement.
This timer works both as countdown and as interval trigger and it have to properties :
  • repeat
    This property define if the timer must stop once the asked time is reached, or it it should repeat itself.
  • action
    This property describe what action will be performed by the timer.
Code:
timer 10 repeat False action NullAction()
timer 10 action NullAction()
This timer will wait 10 second before performing the given action, here nothing at all, then stop.
Code:
timer 10 repeat True action NullAction()
The timer will repeat the given action, still nothing at all, every 10 seconds.

So, the practical example use above become this :
Code:
init python:
    value = 0

screen value:
    text "[value] seconds since the start"

screen myTimer:
    timer 1 repeat True action SetVariable( "value", value + 1 )

label start:
    show screen value
    show screen myTimer
    "Just watch the time passing."
    hide screen myTimer
    "Thanks"
    return
Now, there's a pitfall to know about the timer screen statement, it's in a screen. It can seem to be nothing, but look at this :
Code:
screen myTimer:
    timer 1 action Jump( "tooSlow" )

label start:
    "Alright, once you'll click me, you'll have one second to make a choice"
    show screen myTimer
    menu:
        "This is perhaps the good choice, who know. Is it really the good choice ? No":
            "Too bad, it wasn't the good choice"
            jump start
        "Is this the good choice ? No, seriously, is it it ? Yes, it is":
            "Congratulation. As reward, this madness will end."
            return

label tooSlow:
    "You were too slow, please, restart."
    jump start
Just wait instead of making a choice, and look what happen. You'll see that the second time the menu is displayed, nothing happen. Still the code worked the first time and seem right... but it isn't.
Ren'py show a screen only if it isn't already displayed. Which mean that the second time the start label is played, the line
Code:
    show screen myTimer
is silently ignored by Ren'py, so the timer isn't restarted. For this code to works, you need to hide the screen at one time in your code. Either this way :
Code:
label tooSlow:
    "You were too slow, please, restart."
    hide screen myTimer
    jump start
or this way :
Code:
label start:
    "Alright, once you'll click me, you'll have one second to make a choice"
    hide screen myTimer
    show screen myTimer
Note that you aren't stuck with a hard coded (directly wrote on the code) value for the timer. You can also have a soft coded (which can evolve with the code execution) one. For this, you just need to use the possibility to pass parameters to a screen. So, the practical example used above can become this insane thing :
Code:
init python:
    value = 0

screen value:
    text "[value] seconds since the start"

screen myTimer( itv ):
    timer itv repeat True action SetVariable( "value", value + 1 )

label start:
    show screen value
    show screen myTimer( 0.1 )
    "Just watch the time passing."
    hide screen myTimer
    "Thanks"
    return
You are kind of slow isn't it ? Time is suddenly passing way faster than it seem.

Now, the timer screen statement have an advantage, it's native to Ren'py. This make it easier to use since you don't need additional code to make your timed events works.
It also have an almost disadvantage, you are limited on the action you can perform. But it's not this a disadvantage since you can still use the screen action, and so have a function performing all the operations you can need.
But, alas, come on top of this a real disadvantage, you have no control over this timer. Either it's working, or it's not working, but you can't pause it, by example, which is possible with a Python timer put in the config.periodic_callbacks list.

So, which one of the two methods to use depend mostly on you and on what you intend to do. The callback way is better if you need to have control over the timer, while the screen statement is better if your needs stay basics.

Warning:

Unlike said in the documentation, periodic_callbacks are not called with a frequency of 20Hz (every 0.05 seconds), but more around every 0.06 seconds. And this value seem to vary (+/- 0.01) depending of the charge of the computer when the game is played. This while the timer screen statement rely on another mechanism and is more accurate.
As demonstration, I attached a script to this how-to. You can use it as it, you don't need intervalTimer for this. It display three counters,
  1. called every 0.05 seconds with the timer screen statement ;
  2. supposed to be called every 0.05 seconds with the periodic_callbacks configuration value, but acting like if it was called every 0.06 seconds ;
  3. supposed to be called every 0.05 seconds with the periodic_callbacks configuration value, and acting like it.
The first time is accurate, it will increase the value every 1 second. The second should start being a little late, then around 30 seconds should become a little faster. As for the third one, it should be less and less accurate as the time pass. If you found another behavior, don't hesitate to say it.


To know before using timers:

This said, there's few limitations to take in count when working with timer in Ren'py...

Firstly, it's that everything in Ren'py resolve around the notion of "interaction". Every change happening in the game will be validated only when Ren'py will encounter what can be called "the next interaction point".
Basically speaking, each time the player interact with the game, it mark the end of an interaction. So, when the player click to see the next dialog line, chose an option on the menu presented, or click on a button on the screen displayed, boom, the previous changes are take in count.
This isn't a problem most of the time, but can become one when working with timers. By using them, you lose your control over the moment when the change will happen. So, you can not ensure that they will be followed by an immediate interaction to validate them. By example, a player can be AFK for some times, then when coming back decide that he don't have the time to continue, and so save the game. When he will restart it, the situation of the game will be the one before he goes away, not the one when he decided to save.
So, never use timers to perform decisive change. Or if you do so, ensure that they will be validated by an interaction. To do this, just force the player to click on something. Obviously, don't make decisive change every single seconds, it will quickly become unplayable.

An other thing to take in count is that a timer is designed to count the time passing...
While they can deal with rollback, they do not love them ; some weird things can happen if the player rollback. It also apply to the screen statement, even if it's more protected against this.
Once again the consequences shouldn't be important. Especially since the changes made by the timed event will be, them, reverted by the rollback. But it isn't guaranteed that the timer itself will go back in time. Therefor, when it's important that an event do NOT happen before this or that, try to include a reset of the timer or a pause, which will be triggered by the rollback, to protect your code.

Finally, the last thing to take in count is that a timer do not know what is happening outside of it. Let's say that you planed to have a timed event every two minutes. The timer will not know that the player isn't behind is computer and that the said event is waiting for him to click on something. It mean that the same event can be called over itself, again and again...
The best way to avoid this is to pause, or to stop, the timer until the said event is finished.


Going further:

Now, that you know more about timers on Ren'py, time to use one... And why not the one attached to this page ?
It's a variation around the object MyTimer presented above, with more control over it. And as always, I made it part of Ren'py language.
By including to your game the "00intervalTimer.rpy" file included in the archive below, you add (two times) six statements to Ren'py :

  • intervalTimer timerName interval function
    Let you create and install a new timer.
    Code:
    intervalTimer myTimer 2.5 myFunction
    Will create a timer named myTimer, which will call myFunction every 2 and half seconds.
    A snake_case alias exist: interval_timer
  • startTimer timerName
    Start the given timer, or resume it if it was on pause.
    Code:
    startTimer myTimer
    A snake_case alias exist: start_timer
    Python equivalent : myTimer.start()
  • pauseTimer timerName
    Pause the given timer. It will not count the time passing, and not reset the actual counter value. When you'll use startTimer to resume the timer, the counter will continue the current countdown.
    Code:
    pauseTimer myTimer
    A snake_case alias exist: pause_timer
    Python equivalent : myTimer.pause()
  • stopTimer timerName
    Stop the timer. Not only it will not count the time passing, but it will also reset the actual counter value. When you'll restart the counter with startTimer, it will start a fresh new countdown.
    Code:
    stopTimer myTimer
    A snake_case alias exist: stop_timer
    Python equivalent : myTimer.stop()
  • resetTimer
    Force the timer to start a fresh new countdown. If the timer is stopper or on pause, it just reset the counter.
    Code:
    resetTimer myTimer
    A snake_case alias exist: reset_timer
    Python equivalent : myTimer.reset()
  • removeTimer
    Remove the timer from the config.periodic_callbacks list and delete the timer itself.
    Code:
    removeTimer myTimer
    A snake_case alias exist: remove_timer
    Python equivalent : myTimer.remove()
The timer object also have an onHold method letting you test if it's currently counting the time (False), or on pause or stopped (True).
Code:
    if myTimer.onHold() is True:
        "Timer not working"
    else:
        "Timer working"
The archive also contain a "script.rpy" file which will give you a quick demonstration of how to use the statements.
Also note that the intervalTimer works fine with saves, which isn't at all obvious with the demonstration script.


Thanks to @Rich
 
Last edited:

Rich

Old Fart
Modder
Respected User
Donor
Game Developer
Jun 25, 2017
2,452
6,908
Great tutorial. Just for the record, Renpy itself has some timer capabilities, if you're looking for timing within screens. (Such as if you want to force the player to make a choice within N seconds.)


 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
Just for the record, Renpy itself has some timer capabilities, if you're looking for timing within screens. (Such as if you want to force the player to make a choice within N seconds.)
I focused too much on the interval ; I should have included the timer feature.
This said, the cookbook have finally be erased, so I'll have to write at least one example before including it :(
 

Rich

Old Fart
Modder
Respected User
Donor
Game Developer
Jun 25, 2017
2,452
6,908
@anne O'nymous : The cookbook link I included works for me. I know that site is rather old and out of date, though, so an up-to-date tutorial would always been a good thing.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
The cookbook link I included works for me.
Oh ? I'm so used to find, "this page have been removed", when clicking on the cooklbook, sorry.


[...]so an up-to-date tutorial would always been a good thing.
It's more about my lazy side. Finding example isn't always easy, so I'll take what I found in the cookbook and just update it if needed.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
Alright, just updated OP to add the timer screen statement, and I jumped on the occasion to add a practical example for the callback method.
 

CobraPL

NTR PALADIN
Donor
Sep 3, 2016
1,916
3,764
Code:
screen heartbeat():
    fixed:
        style_prefix "hud"
        text "Heartbeat: [counter]" xalign 0.1 yalign 0.003
        intervalTimer shuffle1 10 shuffle
        startTimer shuffle1
Code:
    def shuffle():
        global counter
        counter+=1
counter is defaulted

So, instead of adding 1 to counter every 10 seconds, it adds 2 every ~6 seconds...
anne O'nymous
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
Code:
screen heartbeat():
[...]
        intervalTimer shuffle1 10 shuffle
So, instead of adding 1 to counter every 10 seconds, it adds 2 every ~6 seconds...
anne O'nymous
It's probably because you use it in a screen and not in a label.
Explanation: Everything in a screen is proceed each time the screen is display, which goes from once in a while when the screen is predicted (Ren'py know that he'll have to display it later), to sometimes once every second when Ren'py refresh a displayed screen.
Consequence: Each time the screen is displayed, you add a new timer to the list of actually working timers.
Lets say that the screen is refreshed every 5 seconds, you quickly have this :
  • timer 1 is created at h:m and 5 seconds
  • timer 2 is created at h:m and 10 seconds
With just this, you already have the counter that will be increased every 5 seconds, but it continue :
  • timer 3 is created at h:m and 15 seconds
  • timer 4 is created at h:m and 20 seconds
Now, every five seconds, you have two timers that are triggered, and each one increase the counter. Which correspond more or less to what you say.



Note: I'm answering while the diner is cooking, so it's more to take as a guess than an affirmation. I'm pretty sure that it's the cause of your problem, but going in and out of the kitchen every now and then while answering could have made me miss something.
Try to put the timer in a label instead of a screen, to see if it correct the problem or not. If not, don't hesitate to come back.
 

CobraPL

NTR PALADIN
Donor
Sep 3, 2016
1,916
3,764
Same problem :( Hmm, maybe i'll force cache purge.
edit: ok, still weird things, it still adds to the counter two times. This time not +2, just +1 and half sec later another +1. At least it is every ~10 seconds, not 6 now.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
Same problem :( Hmm, maybe i'll force cache purge.
Do you test it by loading a saved game, or by starting a new one ? If it's by loading a saved game, then you'll also load the timer already defined.
If it's not possible to test by starting a new game, you can do this on the console :
Code:
config.after_load_callbacks = []
intervalTimerNames = []
It will virtually remove the timers. But warning, you need to have an interaction before it become really effective, so you need to pass a dialog line or something like this. It's only then that you can save and have the saved file freed or the timers.

Note: Diner is ready, I let you at your test, I'll come back later.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,103
14,755
Same problem :( Hmm, maybe i'll force cache purge.
edit: ok, still weird things, it still adds to the counter two times. This time not +2, just +1 and half sec later another +1. At least it is every ~10 seconds, not 6 now.
Alright, I've performed many tests and looked deep into the code of Ren'py. I still don't understand why in your case the counter advance so fast, because what I found is in fact the opposite (which make me update the first message).
It happen that unlike said in the documentation, and expected by pyTom (who use a 50ms delay), the periodic callbacks are in fact called around every 60ms (0.06 seconds), which make them slower than the timer screen statement.

Reported to your particular case, it mean that instead of the increase of 2 every 6 seconds that you see, it should be more something like an increase of 1 every 1.2 seconds. So, for now I stay on what I said, it's probably because you used it inside a screen, then did your test by loading a save file where the timer where present.
Can you try this script and tell me if the second and third timer (the two named 'callback') are slower or faster that the first one ?