HeroEngine Forums
Welcome, Guest. Please login or Register for HeroCloud Account.

Author Topic: Smart Timers (such as in JavaScript)  (Read 2179 times)

DanMorEx

  • General Accounts
  • *
  • Posts: 3
    • View Profile
Smart Timers (such as in JavaScript)
« on: Nov 19, 15, 11:22:55 AM »

Hi, all!
May be you got lack of easy to use timers in Hero Engine?
This tutorial will show you how to define timer-system, that allows you to use timers so easy as you can do it in JavaScript.

Upgrade your timer system

1. Define new client fields. Fastest way to do it is not using DOM editor, but just executing command by command in console:
Code: [Select]
|cfd "continuous", boolean; description="x" -reflect writestrategy="Lazy" -private
|cfd "data", noderef; description="x" -reflect writestrategy="Lazy" -private
|cfd "dataFloat", float; description="x" -reflect writestrategy="Lazy" -private
|cfd "dataID", id; description="x" -reflect writestrategy="Lazy" -private
|cfd "dataStr", string; description="x" -reflect writestrategy="Lazy" -private
|cfd "mustBeDestroyed", boolean; description="x" -reflect writestrategy="Lazy" -private
|cfd "timeEnd", datetime; description="x" -reflect writestrategy="Lazy" -private
|cfd "timeStart", datetime; description="x" -reflect writestrategy="Lazy" -private
|cfd "timerCallbackMethod", string; description="x" -reflect writestrategy="Lazy" -private
|cfd "timerCallbackNodeID", id; description="x" -reflect writestrategy="Lazy" -private
|cfd "timerCallbackScript", scriptref; description="x" -reflect writestrategy="Lazy" -private
|cfd "timerProgress", float; description="x" -reflect writestrategy="Lazy" -private
After executing this commands, you may check, if that fields appear in Client DOM editor. Often DOM Editor do not catch them just after, so you need push Refresh button.

2. Define new client class SmartTimer, with these fields & field myTimer which exist in HeroEngine by default.
Execute command in console:
Code: [Select]
|ccd "SmartTimer", "data"; description="x" "continuous" "data" "dataFloat" "dataID" "dataStr" "mustBeDestroyed" "timeEnd" "timeStart" "timerProgress" "timerCallbackMethod" "timerCallbackNodeID" "timerCallbackScript" "myTimer"

Press Refresh in Client DOM editor, find and select yours new class SmartTimer

3. Press Open Script, than Make Empty Script, than OK.


4. Insert this implementation code to the SmartTimerClassMethods:
Code: [Select]
method startSmartTimer(fireRate as TimeInterval, duration as TimeInterval, callbackNode as NodeRef, callbackScript as ScriptRef, callbackMethod as String)
  if callbackNode != None and not hasMethod(callbackNode,callbackMethod)
    println("$R[ERROR] Node {" + callbackNode + "} has no method '" + callbackMethod + "'")
    return
  .
 
  me.mustBeDestroyed = false
  me.continuous = (duration == 0:0:0)
  me.timeStart = SYSTEM.TIME.NOW
  me.timeEnd = me.timeStart + duration
  me.timerCallbackNodeID = callbackNode
  me.timerCallbackScript = callbackScript
  me.timerCallbackMethod = callbackMethod
 
  if firerate == 0:0:0
    RegisterForPerFrameMethodCallback(me)   
  else
    me.myTimer.script = SYSTEM.EXEC.THISSCRIPT
    me.myTimer.fireRate = fireRate
    me.myTimer.realTime = true
    me.myTimer.start()
  .
.

method tick()
  if (me.mustBeDestroyed)
    me._destroy()
    return
  .   
 
  if not me.continuous
    me.timerProgress = (SYSTEM.TIME.NOW - me.timeStart) / ( me.timeEnd - me.timeStart )
    if me.timerProgress > 1 or (SYSTEM.TIME.NOW + 0.5*me.myTimer.fireRate > me.timeEnd)
      me.timerProgress = 1
    .
  .
 
  _nodeRef as NodeRef = me.timerCallbackNodeID // node can be destroyed already
  callDone as Boolean = false
  if (_nodeRef != None)
    CallMethod(_nodeRef,me.timerCallbackMethod,me)
    callDone = true
  else if (me.timerCallbackScript != None)
    CallScript(me.timerCallbackScript,me.timerCallbackMethod,me)
    callDone = true
  .
 
  if (not callDone) or (me.mustBeDestroyed) or (not me.continuous and me.timerProgress==1)
    me._destroy()
  .
.

method onFrameUpdate(elapsed as TimeInterval)
  me.tick()
.

method stop()
  me.mustBeDestroyed = true
.

method _destroy()
  if me.myTimer.firerate == 0:0:0
    UnRegisterForPerFrameMethodCallback(me)   
  else
    me.myTimer.stop()
  .
  DestroyNode(me) 
.

function myTimer_tick()
  where me is kindof FarmTimer
    me.tick()
  .
.

Press F7, than Shift-F7, Submit.

5. Create also new empty client script with name util


6. Insert the code to util script in HeroEngine script editor:
Code: [Select]
//////////////////////////////////
// Timer

public function MakeTimer( fireRate as TimeInterval, duration as TimeInterval, callbackNode as NodeRef, callbackMethod as String ) as NodeRef of Class SmartTimer
  t as NodeRef of Class SmartTimer = CreateNodeFromClass("SmartTimer")
  if (callbackNode != None)
    AddAssociation( callbackNode, "timer", t)
  .
  t.startSmartTimer(fireRate, duration, callbackNode, None, callbackMethod)
  return t
.

public function MakeTimerWithData( fireRate as TimeInterval, duration as TimeInterval, callbackNode as NodeRef, callbackMethod as String, data as NodeRef ) as NodeRef of Class SmartTimer
  var timer = MakeTimer(fireRate, duration, callbackNode, callbackMethod )
  timer.data = data
  return timer
.

public function MakeTimerOnScript( fireRate as TimeInterval, duration as TimeInterval, callbackScript as ScriptRef, callbackFunc as String ) as NodeRef of Class SmartTimer
  t as NodeRef of Class SmartTimer = CreateNodeFromClass("SmartTimer")
  t.startSmartTimer(fireRate, duration, None, callbackScript, callbackFunc)
  return t
.


public function GetTimer(callbackNode as NodeRef, callbackMethod as String) as NodeRef of Class SmartTimer
  foreach t in QueryAssociationTargetsByClass( callbackNode, "timer", "SmartTimer" )
    where t is kindof SmartTimer
      if (t.timerCallbackMethod == callbackMethod or callbackMethod == "")
        return t
      .
    .   
  .
  return None
.

public function RemoveTimers(callbackNode as NodeRef, callbackMethod as String)
  foreach t in QueryAssociationTargetsByClass( callbackNode, "timer", "SmartTimer" )
    where t is kindof SmartTimer
      if (t.timerCallbackMethod == callbackMethod or callbackMethod == "")
        t.myTimer.stop()
        DestroyNode(t)
      .
    .   
  .
.

// ~ Timer
//////////////////////////////////

Press F7, than Shift-F7, Submit.




How to use new timer system

Timers are ready to be used!
So, now you need not to make classes and objects, include fields and type plenty of code to use timers!
You just need to define callback and call just one function MakeTimer* from util. Thats all!
util:MakeTimer* arguments
1: fire rate of timer (period for callbacks)
2: duration of timer (after which it will be stopped and destroyed, passing 0:0:0 means infinite)
3-4: callback function or method


1st case: Fixed fire rate  (JavaScript setInterval() analog)

Let's make a test:
1. Create script test, and insert there the code:
Code: [Select]
function testTimer()
   util:MakeTimerOnScript( 0:0:2, 0:0:10, SYSTEM.EXEC.THISSCRIPT, "testTimerCallback" )
.
shared function testTimerCallback(t as NodeRef of Class SmartTimer)
   t.dataFloat += 1
   println("Timer: this is call #" + t.dataFloat + ". TimerProgress: " + t.timerProgress)
.

Compile and sumbit (F7 -> Shift+F7 -> "Submit").

2. Open console and type command, and press Enter
call test testTimer
Here what we see:


If you want to make a self destroyable timer, you may destroy it even from callback by setting field .mustBeDestroyed=true
Setting second argument (duration) of MakeTimerOnScript to 0:0:0 means the timer became infinite.

Here is another example:
Code: [Select]
function testTimer2()
  var t = util:MakeTimerOnScript( 0:0:2, 0:0:0, SYSTEM.EXEC.THISSCRIPT, "testTimer2Callback" )
  t.dataStr = " timer #2 :) "
.
shared function testTimer2Callback(t as NodeRef of Class SmartTimer)
  t.dataFloat += 1
  println(t.dataStr + " this is call #" + t.dataFloat + ". TimerProgress: " + t.timerProgress)
  if (t.dataFloat >= 10)
    t.mustBeDestroyed = true
  .
.

call test testTimer2

And here is the result. TimerProgress is always 0, because timer is infinite. We just stop it by ourselves, when we want to (after 10 calls for example)



2nd case: One shot timer (JavaScript setTimeout() analog)

Just call util:MakeTimerOnScript with same fireRate and duration
For example util:MakeTimerOnScript(0:0:10,0:0:10,...)
It will be same as in JavaScript setTimeout(...,10000)


3rd case: Per frame fire

Let's make another test. As in 1st example, but pass 0:0:0 to fire rate, and 250ms to duration. Zero-firerate causes timer to fire at each frame of game.
Code: [Select]
function testTimer3()
   util:MakeTimerOnScript( 0:0:0, 0:0:0.250, SYSTEM.EXEC.THISSCRIPT, "testTimer3Callback" )
.
shared function testTimer3Callback(t as NodeRef of Class SmartTimer)
   t.dataFloat += 1
   println("Timer3: this is call #" + t.dataFloat + ". TimerProgress: " + t.timerProgress)
.

Compile and sumbit (F7 -> Shift+F7 -> "Submit").

call test testTimer3

Here is the result:


Per frame firing Smart Timer is useful for example when you want to FadeIn/FadeOut some visual node. You just need to set DiffuseColor alpha of visualNode equal to timerProgress.


User data passed to Smart Timers

All fields in SmartTimer with prefix data* are used for user defined data. You can save there some node, or ID, or string. You are welcome also to add new data fields to SmartTimer class and use it.
In second example I passed string data to callback. In that way you can pass any data.


Callback to methods of nodes (instead of functions)

Callback can be not only a shared/public function, but it could also be as method of your node. In this way you need to call util:MakeTimer instead of util:MakeTimerOnScript and pass NodeID and name of the method.
You can attach any timers on your node and remove them from node, using the function util:RemoveTimers


Destroying timer before timer finish

From callback. If you want to destroy timer in timer's callback function or method, set variable .mustBeDestroyed=true to noderef of class SmartTimer which is the argument of callback, and timer will be stopped and destroyed just after current callback call finished.

From any place. When you create a timer using util:MakeTimer or util:MakeTimerOnScript, these functions returns you the reference to SmartTimer node. You can save the reference, and set .mustBeDestroyed=true and timer will be stopped and destroyed just before next fire, no further callback will be called. If you want to destroy SmartTimer node immediately and free it from memory just now for some reason, call it's method ._destroy() (but it is prohibited to call this method from callback of it's timer).


Server side smart timers

You can also upgrade your server side timers to smart timers, but be careful to use them! If you will launch an infinite timer, it can hangs the server, and it will be difficult to find it in memory and stop it.
Besides server can't use per frame firing, so you need to truncate a functionality on server side util & SmartTimerClassMethods: remove all what concerns per frame actions.



Enjoy!

And feel free to ask questions and live feedback :)





« Last Edit: Nov 20, 15, 12:46:37 PM by DanMorEx »
Logged

keeperofstars

  • General Accounts
  • *
  • Posts: 998
    • View Profile
    • StarKeeper Online
Re: Smart Timers (such as in JavaScript)
« Reply #1 on: Nov 19, 15, 05:32:06 PM »

Impressive work but while screen debugging your code, i found several things that could be issues? Feel free to correct me on this. Just wanted to talk though them though.

One while I appreciate making a simplistic timer class it lacks some key funcationality such as making a call to stop the timer. I know you can set the value for when to stop it, but you have to track a variable versus making a call to stop it. This makes it a problem cause you are pushing stopping the timer to the ability to update a variable, this will leave it super easy to hack, client side, on server side verification you just added a latency aspect to the replication factor. Meaning if the server has a change that needs to stop the timer on the client, instead of a marshalled command, it's now sending a sizable variable to the client.


Also you are using perframecallback to drive your timer, but not managing the elapsed time between frames. This means your tick will fire at what we hope is a miminal of 30 times a second only. upwards of 150ish times a second.

So on average user your best percision is 1/30 of a second upwards of 1/150ish of a second, with no control variable to manage the differences, so having a faster machine will create more resource spam for that machine. Also it can create either an advantage or disadvantage to a person cause their timers will fire more frequently or less frequently.

Lastly You need to make sure System.time.now happens in the stack before your tick happens, or your time won't work. Meaning  SYSTEM.TIME.NOW happens on per frame as well. If your tick gets into a race condition with System.time.now process, (hopefully it wouldn't, but it could especially with a slower or farther off system aka latency, that is slowing down those  SYSTEM.TIME.NOW updates on perframe, then your timer could be ticking faster than time is updating for the client. This could cause a problem.

I could be wrong or might just need to re-read / study yoru setup but just a few things I spotted. Wanted to know if you had thought of them?

Logged
[img]http://screencast.com/t/x7btcSSyp3h0[\img]

DanMorEx

  • General Accounts
  • *
  • Posts: 3
    • View Profile
Re: Smart Timers (such as in JavaScript)
« Reply #2 on: Nov 19, 15, 08:28:33 PM »

This timer system is not using per frame callback, in usual conditions.
It uses standard Timer of HeroEngine, so all will be work as in usual way.
And in usual way it is not needed to make fireRate of timer less than 1/10 second.

But I forget to explain another feature (I will add it into tutorial tomorrow):
If you setup fireRate to 0:0:0, timer will work in another way: by using perframecallback.

It is needed for example when you want to react on each frame and smart timer will shows you a progress. For example you want to Fade In some visual node during 1 second, than you call
util:makeTimer(0:0:0, 0:0:1, ... )

and make opacity of that node equal to timerProgress

In this case there are no need to achieve same timer fireRate on each client. You just react according to time passed of time left at each frame.

----------

About destroying timer:
you may destroy Smart Timer by calling method ._destroy() - it will destroy it immediatly
but if you are inside timer's callback you cant destroy it in that way, in that case you should to set a pospone destroying using .mustBeDestroyed = true, it will be destroyed also immediatly after callback will be finished
« Last Edit: Nov 19, 15, 08:50:25 PM by DanMorEx »
Logged

DanMorEx

  • General Accounts
  • *
  • Posts: 3
    • View Profile
Re: Smart Timers (such as in JavaScript)
« Reply #3 on: Nov 20, 15, 09:04:51 AM »

I added explanation about per frame usage of timer
Logged

ToY-Krun

  • General Accounts
  • *
  • Posts: 677
  • Support Volunteer
    • View Profile
Re: Smart Timers (such as in JavaScript)
« Reply #4 on: Nov 20, 15, 11:44:39 AM »

Hiya,

First of all, Thats some smart scripting.  And it looks like it should work great. *Thumbs Up*

There are a number of various timers available in HE for all types of circumstance, for both
class/non class, and method/function useage  depending on your needs,  but no better
way of understanding the system than to create your own as you have here. 

Good work!