Scheduler and Function Tables - KidneyThief/TinScript1.0 GitHub Wiki

One of the benefits of many scripting languages is the ability to set up automatic script calls. We've already seen the automatic calling of ::OnCreate() and ::OnDestroy(). Taking this a step further, the idea that a script can run in parallel with the update of an object is extremely useful. One such approach is the use of co-routines, as found in other languages.

Table of Contents

Schedules and Co-routines

In TinScript, we use a scheduler. It could be argued that there are no problems that one approach can solve, that cannot be solved by the other - co-routines and schedulers are somewhat interchangeable. As such, preference may be the main factor in choosing a language that supports one over the other.

An example of each:

  • A co-routine has multiple access points, and the ability to suspend execution (pause, yield...) and to pick up where we left of, when the co-routine is resumed (a pseudo code example):
    • function MyUpdate()
          DoUpdatePhase1()
          print("end of phase 1")
          coroutine.yield()
          DoUpdatePhase2()
          print("end of phase 2")
      end
  • Conversely, the functions when used in a schedule, execute from start to finish - just like in c++, there's no "pausing" the execution of a function call - it will execute until it hits the end of the function (or a return statement). This does require the function to use, say, a state variable to achieve the same result (A TinScript example):
    • void MyUpdate()
      {
          if (self.updatePhase == 1)
          {
              DoUpdatePhase1();
              Print("End of phase 1");
              self.updatePhase = 2;
          }
          else if (self.updatePhase == 2)
          {
              DoUpdatePhase2();
              Print("End of phase 2");
          }
          schedule(self, 1, Hash("MyUpdate"));  // schedule MyUpdate() to be called again next frame
      }
  • There is one main reason why I chose to go with a scheduler for TinScript:
    • In the first example using a co-routine, the implementation reads a bit cleaner, but keep in mind that the script is responsible for the current state of the execution of the function. The local variables and the current instruction to be executed when the co-routine is resumed - all of this is owned by the script. This means, if you pause the game, and change the implementation of MyUpdate() while it is currently suspended, you'll either have find a way to also save and restore the co-routine context... *or* more likely, you'll simply destroy and recreate the object, starting the co-routine from the beginning.
    • In the second example, the function executes completely (either phase 1, or phase 2), until it schedules another call to itself. At this point, the function is stateless - if you pause the game, change the implementation of MyUpdate(), and recompile the script, nothing is lost. When the game resumes, and the scheduler dispatches the next call to MyUpdate(), it simply finds the new implementation. This, to me, allows much more flexibility in editing scripts and better supports speeding up the iteration of a runtime development approach.

schedule

  • The syntax for using the keyword schedule is:
    • schedule(<object id>, <time msec>, <function hash>, <arg1>, <arg2>, ....);
  • If the object ID is 0, then we're calling a global function, instead of an object member function. Examples:
    • schedule(0, 5000, Hash("Print"), "This statement is printed in 5 seconds.");
      • In 5 seconds (5000 msec), the function Print() is called, with the above sentence.
  • If the object ID is not 0, then we're calling the member function for an object.
    • void Foobar::PrintMyName()
      {
          Print("My name is ", self.GetObjectName());
      }
      object test_obj = create CScriptObject("Foobar");
      schedule(test_obj, 5000, Hash("PrintMyName"));
      • In 5 seconds, the method ::PrintMyName() for object test_obj is executed.

ScheduleCancel()

  • Scheduling a function call, especially from a function to itself, can generate a potentially never ending thread. Iterating on a function that schedules function calls - because this is happening at runtime, has the potential to spawn multiple "threads". This can get unwieldy, or be problematic.
  • Every call to schedule, whether it be for an object method, or a global function, returns a unique integer ID. This ID can be used to cancel a schedule, through the function:
    • ScheduleCancel(<id>);
  • In practice, it's a good idea to implement the use of schedules as:
    • void TestObject::MyUpdate()
      {
          // declare a member var to store the schedule id
          int self.update_sched_id;  // even better, do this in ::OnCreate()

          // ... update code ...

          // cancel any redundant threads
          ScheduleCancel(self.update_sched_id);

          // schedule the next call to update
          self.update_sched_id = schedule(self, 1, Hash("MyUpdate"));
      }
  • There are two more convenience functions:
    • ScheduleCancelObject(<object id>);
      • This will cancel all pending schedules for a given object. This happens automatically if the object is destroyed.
    • ListSchedules();
      • This will print out a list of pending scheduled calls, and the object that owns the schedule.

execute

  • A convenience keyword added, the performs much like 'schedule', is the command 'execute'. The only difference is, it happens immediately, and you don't specify a <time msec> as you do for a schedule()
    • execute(<object id>, <function hash>, <arg1>, <arg2>, ...);
    • execute(0, Hash("Print"), "This message prints instantly!");
      • As expected, the output is the same as if you had entered the command: Print("This message prints instantly!");
  • So why have this feature?
    • This is essentially a replacement for having functions as first-class values of the language. You can't pass a "function pointer" as an argument, but you can pass a <function hash> as an integer argument, and then execute() it.
    • In conjunction with storing the values from Hash() in an Associative Array, you can create your own function dictionary. We'll look at an example at the end of this section.

Hash() and Unhash()

  • You'll notice in all the previous examples, when we schedule a function call, we hash the string name of the function. The primary reason for this is because it allows schedules you to cache the "id" of the function, so it doesn't have to hash a string (expensive) every time you want to schedule. Hash() returns an integer, which can be stored in a variable. As mentioned above, storing the hashes of functions in an Associative Array allows you to set up a function dictionary.

Function table example

  • We'll look at an example setting up a function dictionary, using an Associative Array to store function hashes, combined with the keyword 'execute'.
    • First, we'll define some response functions, based on a "mood":
      • void MoodObject::HappyResponse()
        {
            Print("Isn't it a nice day?");
        }
        void MoodObject::SadResponse()
        {
            Print("I need a hug.");
        }
        void MoodObject::AngryResponse()
        {
            Print("Go away!");
        }
  • Now we'll set up a function table of the different responses.
    • hashtable gResponseFunctions;
      int gResponseFunctions["happy"] = Hash("HappyResponse");
      int gResponseFunctions["sad"] = Hash("SadResponse");
      int gResponseFunctions["angry"] = Hash("AngryResponse");
  • Finally, we'll set up "set mood", and a "response" function, using the function table:
    • void MoodObject::SetMood(string mood)
      {
          string self.mood = mood;
      }
      void MoodObject::OnGreeting(string greeting)
      {
          Print(greeting);
          execute(self, gResponseFunctions[self.mood]);
      }
  • Test run:
    • object moody = create CScriptObject("MoodObject");
      moody.SetMood("happy");
      moody.OnGreeting("Hi Moody, how are you?");
      • output: Hi Moody, how are you?
        output: Isn't it a nice day?
    • moody.SetMood("sad");
      moody.OnGreeting("Hi Moody, how are you?");
      • output: Hi Moody, how are you?
        output: I need a hug.
    • moody.SetMood("angry");
      moody.OnGreeting("Hi Moody, how are you?");
      • output: Hi Moody, how are you?
        output: Go away!
⚠️ **GitHub.com Fallback** ⚠️