adventuregamestudio / ags

AGS editor and engine source code
Other
705 stars 159 forks source link

Script API: Timers as objects #1958

Open ivan-mogilko opened 1 year ago

ivan-mogilko commented 1 year ago

Written to replace #610 and #612.

Overview

The existing Timer API in AGS works like this:

See: SetTimer documentation

Problem 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

  1. Instead of requiring user to pass their own ID into the "CreateTimer" function, the CreateTimer should assign a UID and return it, so that a user may save one in a variable, and use further to refer to this exact timer instance.
  2. Then, if we follow the OO-style, the Timer should be a struct, and "CreateTimer" would return a 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).
  3. If we have a Timer object, we might as well supply its interface with all necessary attributes for retrieving and changing its state.
  4. In regards to expiration test, the Timer might have a state that tells whether it's idle, running, or expired (undefined time ago); and separately a way to check if it expired right this game frame, which may be used multiple times within same frame until the game updates further. This would allow to both know exact frame when timer expires, and be able to know that it is in expired state later, if that is necessary for any reason.

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 only repeatedly_*_always callbacks are still called. So unless event behavior is modified, the timers will not trigger callbacks during blocking actions.

ericoporto commented 11 months 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?