Envision, Create, Share

Welcome to HBGames, a leading amateur game development forum and Discord server. All are welcome, and amongst our ranks you will find experts in their field from all aspects of video game design and development.

Scheduler - When you want something to happen after an amoun

Zeriab

Sponsor

Scheduler ~ Scripting Tool
Version: 1.0

Introduction

How many of you tried creating a parallel process where you put in an amount of frames and then a script call or something else afterwards?
Basically a simply way of doing something after an amount of frames. This is the principle behind the script. To schedule pieces of code to be executed after an arbitrary amount of frames. No making decrementing counters. The scheduler takes care of all that or well most of it. It is still not as simple as using the wait event command.

Script

[rgss]#==============================================================================
# ** Scheduler
#------------------------------------------------------------------------------
#  This class allows to schedule a proc or method call a given amount of frames
#  into the future with any amount of arguments
#==============================================================================
class Scheduler
  #============================================================================
  # ** Order
  #----------------------------------------------------------------------------
  #  An order is a proc, method or something else which has 'call' as a method,
  #  and the arguments to pass along.
  #============================================================================
  # Create an struct for containing the data
  Order = Struct.new:)callable, :arguments)
  # Extend the class with a call-method which calls the callable with the args
  class Order
    #------------------------------------------------------------------------
    # * Call the callable with the present arguments
    #------------------------------------------------------------------------
    def call
      callable.call(*arguments)
    end
  end
  #============================================================================
  # ** RecurringOrder
  #----------------------------------------------------------------------------
  #  An order which is recurring every specified amount of time until
  #  FalseClass is returned from the call.
  #  Note that arguments remain the same for each call
  #============================================================================
  # Create an struct for containing the data
  RecurringOrder = Struct.new:)callable, :arguments, :frames)
  # Extend the class with a call-method which calls the callable with the args
  class RecurringOrder
    #------------------------------------------------------------------------
    # * Call the callable with the present arguments
    #------------------------------------------------------------------------
    def call
      result = callable.call(*arguments)
      unless result == FalseClass
        Scheduler.schedule_recurring(frames, frames, callable, *arguments)
      end
    end
  end
 
  #============================================================================
  # ** Mapping
  #----------------------------------------------------------------------------
  # Maps an index to an array. Values can be added to these value.
  # Each array starts empty.
  #============================================================================
  class Mapping
    #------------------------------------------------------------------------
    # * Initialization
    #------------------------------------------------------------------------
    def initialize
      @mapping = {}
    end
    #------------------------------------------------------------------------
    # * Add an value to a given index
    #------------------------------------------------------------------------
    def add(index, value)
      @mapping[index] = [] if @mapping[index].nil?
      @mapping[index] << value
    end
    #------------------------------------------------------------------------
    # * Retrieve the list of values mapped to the index
    #------------------------------------------------------------------------
    def get(index)
      return [] if @mapping[index].nil?
      @mapping[index]
    end
    #------------------------------------------------------------------------
    # * Delete the array the index is mapped to. Conceptually it is now empty
    #------------------------------------------------------------------------
    def empty(index)
      @mapping.delete(index)
    end
  end
 
  #--------------------------------------------------------------------------
  # * Initialization
  #--------------------------------------------------------------------------
  def initialize
    # This maps
    @mapping = Mapping.new
    @tick = 0
  end
  #--------------------------------------------------------------------------
  # * Scheduling
  #--------------------------------------------------------------------------
  def schedule(frames, callable, *arguments)
    # Create an order
    order = Order.new(callable, arguments)
    @mapping.add(frames + @tick, order)
  end
  #--------------------------------------------------------------------------
  # * Scheduling
  #--------------------------------------------------------------------------
  def schedule_recurring(frames, frames_to_wait, callable, *arguments)
    # Create an order
    order = RecurringOrder.new(callable, arguments, frames_to_wait)
    @mapping.add(frames + @tick, order)
  end
  #--------------------------------------------------------------------------
  # * Update the scheduler
  #--------------------------------------------------------------------------
  def update
    # Get the orders for the current tick
    orders = @mapping.get(@tick)
    # Delete the mapping's reference to the list of orders
    @mapping.empty(@tick)
    # Call each order
    for order in orders
      order.call
    end
    # Advance the tick (next frame)
    @tick += 1
  end
 
  #--------------------------------------------------------------------------
  # * 'Singleton' principle used although you can easily make
  #   an extra scheduler. (Class method only works for this)
  #--------------------------------------------------------------------------
  @@instance = self.new
  def self.instance
    return @@instance
  end
  ## Class methods point to the equivalent instance methods
  def self.schedule_recurring(*args) instance.schedule_recurring(*args); end
  def self.schedule(*args) instance.schedule(*args); end
  def self.update(*args) instance.update(*args); end
end
[/rgss]

The latest version will be present here: http://sites.google.com/site/zeriabsjun ... /scheduler
You could check it if more than a month has passed since last edit. You know, just in case.
In fact you'll find all material related to the Scheduler there except forum topics with the except of support responses.

Here is a binding if you want the scheduler to work for every frame. Basically for each Graphic.update the scheduler is updated.
It doesn't matter whether you are in a menu, title screen, battle. As long as Graphic.update is called so is the scheduler. (The main scheduler)
[rgss]module Graphics
  class << self
    unless self.method_defined?:)scheduler_update)
      alias :scheduler_update :update
    end
    def update(*args)
      scheduler_update(*args)
      Scheduler.update
    end
  end
end
[/rgss]

Instructions

You can schedule in two ways.
You can do a one-time schedule which works like this: (Using the class method)
[rgss]Scheduler.schedule(frames, callable, *arguments)
# Here's an example
Scheduler.schedule(65, Proc.new {|x,y| p x,y}, "A string", 42)
[/rgss]
The 65 means that the proc will be called after 65 frames. (Or 65 ticks to be more precise. 1 update = 1 tick usually)
After 65 ticks the proc {|x,y| p x,y} will be called with the arguments x = "A string" and y = 42. (The *arguments means any number of arguments. This can also be no arguments at all)
The Scheduler uses duck typing and assumes that anything which has the .call method works properly in the context. I imagine procs and methods to be the most commonly used.

The next is that you can schedule a recurring call which works like this: (Using the class method)
[rgss]Scheduler.schedule_recurring(frames, frames_to_wait, callable, *arguments)
# Here's an example
Scheduler.schedule_recurring(65, 30, Proc.new {|x,y| p x,y}, "A string", 42)
[/rgss]
The arguments is the same as for the one-time with the addition of the frames_to_wait argument.
This specifies that after the first 65 ticks each recurring call will happen after 30 ticks.
This will continue until the callable returns FalseClass. (Mind you it's false.class and not false)

Now you have to update the schedule every frame or it won't schedule properly.
Here is a binding where the scheduler updates every time the Graphics module updates. (Made for XP. I am unsure whether this part works in VX)
[rgss]module Graphics
  class << self
    unless self.method_defined?:)scheduler_update)
      alias :scheduler_update :update
    end
    def update(*args)
      scheduler_update(*args)
      Scheduler.update
    end
  end
end
[/rgss]

Note that the class methods only work for one scheduler. I believe this should be the generally working Scheduler.

Compatibility

The Scheduler alone use only Ruby and could easily be placed in a Ruby if one wanted that.
It is highly unlikely that you will encounter any compatibility issues with the backbone alone since it is independent from RGSS/2 library.

On the other side there could potentially be problems with the bindings which makes use of the scheduler so it actually does something in game.
Currently there is only the Graphics.update binding which makes compatibility issues very unlikely.

Future Work
- Explicitly exit/stop a scheduler. The scheduled items can then be discard, executed or maybe something else.
- Error handling. (This may be an external. I.e. not embedded in the core)
- Documentation

Credits and Thanks

Credits goes to Zeriab for creating the system

I would like to thank everyone using their time to try and use my system.
I would like to thank everyone reading this topic.

Thanks.

Author's Notes

I would be delighted if you report any bug, errors or issues you find.
In fact I would be delighted if you took the time and replied even if you have nothing to report.
Suggestions are more than welcome.

Note that I will release a demo which shows a couple of ways of using this script.
Note also that I will always make a post when I have an update. (Small stuff like typos excluded)

And finally: ENJOY!

- Zeriab
 

e

Sponsor

While I probably will not use this (even though it would go well with my animations!), I have to say, nice job and, more than anything, thank you: I didn't know about the Struct class. Now I do :)
 
Hell yeah, this rocks man! You should make a Scene-based Schedueler....

Code:
# Schedueler.reoccur_in_scene(frames, scene_s, proc, arguments (, kill))
Schedueler.reoccur_in_scene(80, 'Scene_Menu', Proc.new {|c| p c}, "You're in the Main Menu, stupid!", true)

Something like this would print "You're in the Main Menu, stupid!" every 80 'ticks' while you're in Scene_Menu, and the last argument would determine if this process occurs still the next time you enter the menu. Again, you'd still call it in Graphics which constantly checks what $scene.class is.

BTW how can you call for one of your Schedueled proc's to end? There's no naming convention to them, and although there's a 'get' function how would I know which process is the one I want, and how would I kill it when I'm done with it?
 

Zeriab

Sponsor

I am glad you all like it ^_^

@etheon:
The Struct class can be quite nice ^^
The idea as I am sure you have guessed is based on structs from C though of course accordingly changed to accommodate the type system of Ruby.

You can consider using this script if you come into a situation in the future where it might be useful. It can be used for animations, but if you already have implemented a system it may not be worth the extra time. Just an extra thing to consider ^^

@Kain Nobel:
That's an interesting idea.
You can easily create a new scheduler and store it as a instance variable during the initialization of the scene. (Scheduler.new)
Any information in the scheduler would naturally be lost when the scene ends. For animation purposes this might be good.
You can use a class variable for a scheduler for stuff which should not be lost then the scene changes.
If you are doing it this way you won't have any checks running while you are in other scenes.
On the other hand your way is nice if you want the functionality in loads of different scenes. It really depends on the specific situation which is preferable. The chance of compatibility issues is lower.
The conclusion is that I will not have native support for what you are suggesting in the core scheduler, but I might make an add-on which can be used if you want the general functionality. (You could also make it if you want ^^)

Currently the only way you can end a recurring call is to let the call return FalseClass.
There is not outside way of doing it. At least not directly.
I will create options to shutdown or clear a scheduler with various options of what to do with scheduled calls.
The idea of having references to the scheduled calls is a nice idea. It is not generally needed and the extra information can be bothersome. I am thinking having a subclass where you have references could be a good solution. Hmm...

Thanks for taking your time to answer. It is much appreciated ^^

*hugs*
- Zeriab
 

Thank you for viewing

HBGames is a leading amateur video game development forum and Discord server open to all ability levels. Feel free to have a nosey around!

Discord

Join our growing and active Discord server to discuss all aspects of game making in a relaxed environment. Join Us

Content

  • Our Games
  • Games in Development
  • Emoji by Twemoji.
    Top