Open ivan-mogilko opened 1 year ago
Hey, about timer as objects, I think the API you made in your module is the best, and I always use it instead of AGS own timers. One thing I noticed, is AGS developers take some time to understand that if something should exist for more than a slice of a tick, a reference has to be kept either in the room scope or the global scope, otherwise it ceases to exist.
I think this "has to exist during the game runtime" equivalent in something like Winforms is masked by being able to create it in the visual editor, so perhaps having somewhere where they could be created in the Editor too?
Written to replace #610 and #612.
Overview
The existing Timer API in AGS works like this:
See:
SetTimer
documentationProblem 1: identification
The major problem here is not much that timers are identified with a number (you may use named constants), but that these IDs are all sharing same "namespace" (global scope) and are very limited in number. You are not creating your own timers, you are reusing same ones from the small pool of timer "slots". And there is little way to guarantee that you do not start timer N in another place while the previously created timer N is still running. This is very bug prone and makes it hard to work with timers in large projects.
Additionally, this complicates using timers in the modules and templates: as module writer would have to let people customize timer ID's to make sure they don't conflict with anything in game.
It would be desired to get a unique timer "handle" each time a new timer is started instead, something that you may save in a variable and use to reference this exact timer instance.
Problem 2: expiration trigger
This is a less significant issue, in comparison, but still may cause annoyance sometime.
As the timer reports expiration strictly the first instance when it's checked (see
IsTimerExpired
), this means that:That's fine in most common cases, but may be inconvenient if you need to handle timers in a modular way: if two scripts are testing for same timer(s) in "repeatedly" callbacks, then only first one will know it's expired.
Also, for example, if you have started the timer in one room, but check for expiration in another, the expiration may be detected some time after it actually expired. Again, this is what happens if you are not using timer API as intended, but this illustrates how bug prone it is.
Suggested solution
Timer*
pointer. That would give a fully secure instance, as you cannot "occasionally" refer to the same object using another handle (unless you assign a wrong pointer into a variable, but that's separate issue).This was mentioned couple of times in tickets #610, #612; I wrote a script module for Timers a while ago, and may propose it for the reference for a new Timer api. Not necessarily to be copied 1:1, as it may contain excessive properties not usually meant for simple timers. https://github.com/ivan-mogilko/ags-script-modules/blob/master/scripts/util/Timer.ash Forum thread: https://www.adventuregamestudio.co.uk/forums/modules-plugins-tools/module-timer-0-9-0-alternate-variant/
Disadvantages
The primary disadvantage of having a Timer as an object is the same as when using any pointers to a dynamically created objects: user must be aware of how to use pointers and make sure they don't try to use a null pointer variable. In my module I even made couple of static "helper" functions that test whether passed Timer pointer is valid.
Having a sort of a "handle" instead will make this safer, as the validity checking will become purely an engine's responsibility.
Alternative to expiration test: a callback
There are two alternatives to expiration checking in "repeatedly" function.
First is to use a function pointer, passed as an argument to "CreateTimer" function. This is a "fantasy" solution, as function pointers are not supported by AGS Script at the time of writing this. There's a feature ticket opened for them though: #1409. If that is implemented, then we might add delegate support to a Timer object.
Second is to use
on_event
, or a similar new expanded callback. As the timer expires, the event is being scheduled, and executed among other events during a game frame update. By the way, this is doable even now,on_event
have 1 integer parameter, which may be a timer ID. If the Timer becomes an object, a different type of callback would likely be necessary. The possible disadvantage of using this event system is that currently these scheduled events are not called during blocking actions, during which onlyrepeatedly_*_always
callbacks are still called. So unless event behavior is modified, the timers will not trigger callbacks during blocking actions.