- Jun 10, 2017
- 10,964
- 16,207
config.periodic_callback timers:
Disclaimer: See the warning below
Ren'py come with many callbacks, among them there's the
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.
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 :
It just count the time passing, nothing really interesting I know
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 :
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
timer screen statement:
Another way to have timers with Ren'py is to use the
This timer works both as countdown and as interval trigger and it have to properties :
This timer will wait 10 second before performing the given action, here nothing at all, then stop.
The timer will repeat the given action, still nothing at all, every 10 seconds.
So, the practical example use above become this :
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 :
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
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 :
or this way :
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 :
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
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
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,
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 :
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
Disclaimer: See the warning below
Ren'py come with many callbacks, among them there's the
You must be registered to see the links
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 )
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
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 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
You must be registered to see the links
object's attributes.timer screen statement:
Another way to have timers with Ren'py is to use the
You must be registered to see the links
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()
Code:
timer 10 repeat True action NullAction()
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
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
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
Code:
label tooSlow:
"You were too slow, please, restart."
hide screen myTimer
jump start
Code:
label start:
"Alright, once you'll click me, you'll have one second to make a choice"
hide screen myTimer
show screen myTimer
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
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
You must be registered to see the links
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,
- called every 0.05 seconds with the
timer
screen statement ; - 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 ; - supposed to be called every 0.05 seconds with the
periodic_callbacks
configuration value, and acting like 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
A snake_case alias exist: interval_timer - startTimer timerName
Start the given timer, or resume it if it was on pause.
Code:startTimer myTimer
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
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
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
Python equivalent : myTimer.reset() - removeTimer
Remove the timer from the config.periodic_callbacks list and delete the timer itself.
Code:removeTimer myTimer
Python equivalent : myTimer.remove()
Code:
if myTimer.onHold() is True:
"Timer not working"
else:
"Timer working"
Also note that the intervalTimer works fine with saves, which isn't at all obvious with the demonstration script.
Thanks to @Rich
Last edited: