Author Topic: MODULE: Timer 0.9.0 (Alternate variant)  (Read 471 times)

Crimson Wizard

  • AGS Project Tracker Admins
    • Best Innovation Award Winner 2013, for spearheading the AGS 3.3.0 project
    •  
    • Lifetime Achievement Award Winner
    •  
    • Crimson Wizard worked on a game that was nominated for an AGS Award!
      Crimson Wizard worked on a game that won an AGS Award!
MODULE: Timer 0.9.0 (Alternate variant)
« on: 03 Dec 2017, 23:46 »
DOWNLOAD Timer 0.9.0 MODULE

Latest source code may be found here
Git cloning address: https://ivan-mogilko@bitbucket.org/ivan-mogilko/ags-script-modules.git

Module supports AGS 3.4.0 and higher


Other script modules by me:
Add spoiler tag for Hidden:



Quick pre-word

Few months back I urgently needed a Timer module with certain functionality, and was hoping to download monkey0506's Timer module, but official links were broken (later I found the only working link, but it lead to an old version). So I wrote my own alternate variant, and posted an initial code in the same thread.

I made certain decisions, such as creating a managed class which you have to operate through pointers, that may cause controversy (this clearly has both advantages and disadvantages).  Even now I keep having doubts about that. In theory it could be possible to remake it into non-managed class if people will find this not convenient (although there may be complications related to AGS scripting limits).
I also do not know whether monkey0506 had any plans on his own module; I would not mind even merging our modules at some point.


Introduction

The Timer module addresses an issue of built-in timers in AGS: that may be difficult to ensure that you do not reuse same timer for different purpose.
Add spoiler tag for Hidden:
In AGS timers are identified with numbers; this is not a problem on its own, because you can always declare a named constant with #define (like "#define TIMER_BOMB 1") and use that instead. What is the problem, that these IDs are all sharing same "namespace". Basically, you are not creating your own timers, you are reusing same ones from the single list 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 requires coder's discipline and planning, but in the big project may become tedious. Additionally, that complicates using timers in the modules and templates (module writer would have to let people customize timer ID's to make sure they don't conflict with anything in game).

Timer class solves this issue, because you are no longer forced to use same available X timers from the list, but can create any number of Timer variables.

For AGS-related technical reasons there is however a limitation: only finite number of timers may be run at the same time. This number is 20 by default, but may be changed at will.
Add spoiler tag for Hidden:
The real problem in AGS is that it's impossible to call repeatedly_execute function explicitly for the managed object, we have to use global repeatedly_execute and call object updates from there. Which in turn means that the module must keep references to active timers in an array.
Of course I could implement self-resizing array, but thought that timers are not kind of things that might need such effort. This still may be done in a future update though.

Timer is implemented as a managed class, which means that you need Timer* pointers to work with it (similar to Character*, DynamicSprite* and so on). This requires certain attention from coder, because pointers may be "null"; on the other hand this lets you to pass Timer object as a function parameter, or return it from a function as return value (something you cannot do with basic structs in AGS).

Timer supports timeout defined in either game ticks or real time. When defining it in real-time you need to keep in mind that the timer's precision depends on FPS (game speed). If your game is running at 40 FPS then the timers are updated only once per 0.025 second (25 ms). In such case setting timeout to, say, 10 ms, won't do much sense (it will still expire after 25 ms).

Timer can be run once (default behavior) or be repeating. Repeating timers will reset themselves after every timeout and never end running. You must remember to stop them yourself when you no longer need them (this is especially important considering that number of running timers is finite).

Timer can be paused and resumed at will. Additionally, you can require all or certain timers to pause and resume with the game (when PauseGame/UnPauseGame is called).

Timers can also be created as local. Local timers remember which room the player was in when they started, and work only while player remains there. If player leaves that room, local timer will stop (default action) or pause. Timers paused this way will resume automatically when the player returns back to their "home" room.

NOTE: From the technical side, it does not matter whether variable is declared in room script or global script, either can be used to make both global and local timers. But remember that if a Timer* variable is in the room script then you won't be able to use it (e.g. stop the timer) and check it for expiration when the player is in another room. Unless that's exactly what you want, better have Timer* variable in the global script.

Since there are three causes for the timer's pause, - script command, autopause with PauseGame and autopause for local timer, - following rule is applied for unpausing the timer: timer only resumes when all three conditions are allowing to. For example, local timer which is configured to automatically pause with the game, will only unpause when in correct room AND game is not paused. If you additionally pause that timer yourself, it will require you resuming it, but resuming the timer in wrong room still won't unpause it until correct room is loaded.


Using Timer

Alright, now to the actual scripting.

Timer pointers are declared simply as:
Code: Adventure Game Studio
  1. Timer *MyTimer;
  2. Timer *tBomb;
  3. Timer *ALotOfTimers[100]; // array of 100 timer pointers
  4.  

These are pointers, but timer objects are not created yet.

Here is how you create and start the new timer:
Code: Adventure Game Studio
  1. MyTimer = Timer.Start(50); // create a timer that runs 50 game ticks
  2. tBomb = Timer.StartRT(10.0); // create a timer that runs 10 seconds (regardless of game speed).
  3. // RT - means "realtime" ;)
  4. // In case you wondered, you can pass value less than second -
  5. MyTimer = Timer.StartRT(0.3); // this timer will run for 0.3 seconds, or 300 milliseconds
  6.  
  7. // Creating repeating timers -
  8. SignalTimer = Timer.Start(40, eRepeat); // this timer will "expire" every 40 ticks and run again
  9.  
  10. // Creating local timers -
  11. RoomTimer = Timer.StartLocal(2000); // this timer will run for 2000 game ticks OR until player leaves the room
  12. ResumingRoomTimer = Timer.StartLocalRT(60.0, eTimerPause); // this timer will run for one minute, but pause if player leaves the room
  13. AnotherLocalTimer = Timer.StartLocal(40, eTimerPause, eRepeat); // repeating local timer
  14.  
  15. // Making the particular timer automatically pause with the game
  16. SignalTimer.PauseWithGame = true; // do this only after you created one, duh
  17.  
  18. // Or maybe you want ALL of the timers in the game to pause when game is paused?
  19. Timer.AllPauseWithGame = true; // better do this once in the game_start()
  20.  


Checking for timer's expiration is done using either of two ways.
First of all you may check timer's property EvtExpired. But because you are using timer pointers, normally you would need to first test if the pointer is not null (unless you are absolutely 100% sure it will always be created at that point):
Code: Adventure Game Studio
  1. if (tBomb != null && tBomb.EvtExpired)
  2. {
  3.     Display("BOOOOM!!!");
  4. }
  5.  

Another method takes into account the pointer fact, and works as a static function which safely manages even null pointers:
Code: Adventure Game Studio
  1. if (Timer.IsExpired(tBomb)) // you may pass even null-pointer there
  2. {
  3.     Display("BOOOOM!!!");
  4. }
  5.  


IMPORTANT: the way EvtExpired works is slightly different from built-in AGS timers.
* In AGS this flag is valid until you check it.
* In Timer module this flag is valid until the next game update. This means that you may check same timer several times, but cannot wait to check it later.


At any point in time you may test if the timer is still existing/active like this:
Code: Adventure Game Studio
  1. if (tBomb != null && tBomb.IsActive)
  2. {
  3.     // bomb is still ticking
  4. }
  5.  
  6. if (tBomb == null || !tBomb.IsActive)
  7. {
  8.     // bomb timer is either did not start yet, or already run out
  9. }
  10.  


Pausing and stopping a timer is done with the other static functions.
(I was considering to add non-static counterparts but did not make them for now)
Code: Adventure Game Studio
  1. Timer.Pause(MyTimer); // pause it
  2. ...
  3. Timer.Resume(MyTimer); // resume it
  4. ...
  5. Timer.Stop(MyTimer); // stop it completely
  6. // After timer was stopped it is completely useless
  7. MyTimer = null; // you may even nullify pointer now
  8.  


Speaking of deleting a timer, as was said - only running timers number is finite. As with other managed objects, the timer object will be only deleted after all pointers referencing it are nullified You are not obliged to do this, because timer object is small. Also, if you will assign new timer to same pointer, the old object will be safely removed from memory. But if you know that you won't be using same pointer for a while (or no longer in this game), I'd suggest to do that.


Finally, there is a way to get some information about running timer:
Code: Adventure Game Studio
  1. function repeatedly_execute()
  2. {
  3.     if (tBomb != null && tBomb.IsActive)
  4.     {
  5.         Label1.Text = String.Format("Ticks left: %d, seconds left: %.3f", tBomb.RemainingTicks, tBomb.RemainingSeconds);
  6.         if (tBomb.IsPaused)
  7.             Label2.Text = "Bomb is paused";
  8.         else
  9.             Label2.Text = "Bomb is running";
  10.     }
  11. }
  12.  


Possible problems

1. Prematurely clearing or replacing pointer value.

The timer object you have in the pointer variable is the only way to control it. For the technical reasons the module itself also keeps a reference to every running timer. This means that if you nullify your pointer variable (or assign another timer to it) before the previous one has stopped, not only you won't be able to control the previous timer anymore, but the timer is not going to be deleted immediately and keep running.

For one-time timers that may not be a big issue, because they will stop on their own eventually and get deleted by the module's update function. But leaving repeatable timer like that will make it run forever, occupying a slot.

So - always Stop a timer when you no longer need it (unless it has already expired and stopped, that is).

2. Timers in non-state-saving rooms.

AGS has two types of rooms, and non-state saving rooms are special in that they do not keep their data when player leaves. What this means is even the variables are lost.
But as explained above, the Timer is a managed object, and will be kept in memory as long as there is at least one pointer referencing it. And the module itself keeps references to all running timers.

So here comes the possible issue: if you have a Timer* variable in non-state-saving room, and created a timer there, as soon as player leaves the room you will loose control over that timer. If it were repeatable timer, it will also keep running forever, as noted in problem 1).

If you need a timer in non-state-saving room:
* If you want a global timer, make a variable in global script, not room script;
* If you want a local timer that will RESUME when you return to the room, STILL declare a variable in global script. This way you will keep pointer to that timer when return to that room again.
* If instead you want a local timer that stops when player leaves the room, then just create it with eTimerStop parameter as usual, and it will get deleted properly.


Timer API reference

Code: Adventure Game Studio
  1.   // Maximal number of simultaneously running timers (not related to built-in AGS limit).
  2.   #define MAX_RUNNING_TIMERS 20
  3.  
  4.   // Local timer behavior when room has changed
  5.   enum LocalTimerBehavior
  6.   {
  7.     eTimerPause,
  8.     eTimerStop
  9.   };
  10.  
  11.   // Flags determining the reason for timer's pause (can be combined using bitwise OR)
  12.   #define TIMER_PAUSED_BY_USER 1
  13.   #define TIMER_PAUSED_BY_GAME 2
  14.   #define TIMER_PAUSED_BY_ROOM 4
  15.  
  16.  
  17.   //
  18.   // General operations.
  19.   //
  20.  
  21.   /// Start the timer, giving timeout in game ticks.
  22.   static Timer *Timer.Start(int timeout, RepeatStyle repeat = eOnce);
  23.   /// Start the timer, giving timeout in real time (seconds).
  24.   /// Remember that timer can be only as precise as your GameSpeed (40 checks per
  25.   /// second, or 0.025s by default).
  26.   static Timer *Timer.StartRT(float timeout_s, RepeatStyle repeat = eOnce);
  27.   /// Starts local timer working in game ticks, that may be paused when player leaves the room
  28.   static Timer *Timer.StartLocal(int timeout, LocalTimerBehavior on_leave = eTimerStop, RepeatStyle repeat = eOnce);
  29.   /// Starts local timer working in real time (seconds), that may be paused when player leaves the room
  30.   static Timer *Timer.StartLocalRT(float timeout_s, LocalTimerBehavior on_leave = eTimerStop, RepeatStyle repeat = eOnce);
  31.  
  32.   /// Tells whether timer has JUST expired. Safe to pass null-pointer.
  33.   static bool   Timer.IsExpired(Timer *t);
  34.   /// Stops the running timer. Safe to pass null-pointer.
  35.   static void   Timer.Stop(Timer *t);
  36.   /// Pause the running timer. Safe to pass null-pointer.
  37.   static void   Timer.Pause(Timer *t);
  38.   /// Resume the running timer. Safe to pass null-pointer.
  39.   static void   Timer.Resume(Timer *t);
  40.  
  41.   //
  42.   // Additional setup.
  43.   //
  44.  
  45.   /// Gets/sets whether all timers should pause when game is paused
  46.   static bool  Timer.AllPauseWithGame;
  47.   /// Gets/sets whether this particular timer should pause when game is paused
  48.   bool Timer.PauseWithGame;
  49.  
  50.   //
  51.   // Current state inspection.
  52.   //
  53.  
  54.   /// Tells whether timer is currently active (counting down).
  55.   readonly bool  Timer.IsActive;
  56.   /// Signal property telling that the timer has expired. This flag will remain set
  57.   /// for one game tick only and self-reset afterwards.
  58.   readonly bool  Timer.EvtExpired;
  59.  
  60.   /// Gets the home room of the local timer (returns -1 if timer is global)
  61.   readonly int  Timer.HomeRoom;
  62.   /// Gets what this timer should do when home room gets unloaded
  63.   readonly LocalTimerBehavior Timer.WhenLeavingRoom;
  64.  
  65.   /// Gets whether this timer is working in real-time
  66.   readonly bool  Timer.IsRealtime;
  67.   /// Gets the timer's timeout in game ticks
  68.   readonly int   Timer.TimeoutTicks;
  69.   /// Gets the timer's timeout in real-time (considering current game speed)
  70.   readonly float Timer.TimeoutSeconds;
  71.   /// Gets the remaining time in current game ticks
  72.   readonly int   Timer.RemainingTicks;
  73.   /// Gets the remaining time in real-time (considering current game speed)
  74.   readonly float Timer.RemainingSeconds;
  75.   /// Gets current timer's paused state (0 - working, >= 1 - suspended)
  76.   readonly int   Timer.IsPaused;
  77.  
« Last Edit: 04 Dec 2017, 06:09 by Crimson Wizard »

Snarky

  • Global Moderator
  • Mittens Earl
  • Private Insultant
    • I can help with proof reading
    •  
    • I can help with translating
    •  
Re: MODULE: Timer 0.9.0 (Alternate variant)
« Reply #1 on: 04 Dec 2017, 12:51 »
Timers done right! Great work, CW!

Crimson Wizard

  • AGS Project Tracker Admins
    • Best Innovation Award Winner 2013, for spearheading the AGS 3.3.0 project
    •  
    • Lifetime Achievement Award Winner
    •  
    • Crimson Wizard worked on a game that was nominated for an AGS Award!
      Crimson Wizard worked on a game that won an AGS Award!
Re: MODULE: Timer 0.9.0 (Alternate variant)
« Reply #2 on: 05 Dec 2017, 17:53 »
There is one thing that keeps bothering me though, it's expiration checking for local (one room only) timers.

Consider this scenario:
* when the player enters the room, we start the timer.
* we want something to happen when the timer has expired and player is in the room at that very moment, OR, if the timer had expired while player was in another room, this something happens the moment player returns back again.

This might be pretty common task, and even with built-in AGS timers could be solved somehow like this:
- if player is in the room, then check for timer in room script;
- if player is not in the room check for same timer in global script, and if it expires - set variable.
- check that variable next time player visits the room.

But I was thinking, would that make sense to let you check only timer in the room script somehow. I see two alternatives here:
1) Add another property, along with EvtExpired, that tells you that timer had expired at least once (not necessarily just now). It's just that Timer.IsExpired static function may be confusing then: should it return true only if timer has just expired, or also in case timer had expired some time ago (for one-time running timers)?
2) Don't reset EvtExpired if it's a one-time running timer. In such case users will have to nullify their Timer* pointer after catching IsExpired, otherwise IsExpired will keep returning true all the time.
OTOH I could make this work like with built-in AGS timers - reset flag right after user checked it once - but I do not like this very much, because, since Timer* is a pointer, potentially there may be other places where its reference is kept and checked for expiration (if that even makes sense).



PS. Also, the question just came to me, why did I make realtime parameter float in seconds, and not just integer in milliseconds...
« Last Edit: 05 Dec 2017, 17:56 by Crimson Wizard »